mirror of
https://github.com/standardnotes/server
synced 2026-02-01 05:01:11 -05:00
Compare commits
12 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f275b48770 | ||
|
|
de4fcf9a4c | ||
|
|
a1455d281f | ||
|
|
cfbe2bbac6 | ||
|
|
398c10ce4b | ||
|
|
c7d21b092d | ||
|
|
031fa71e7d | ||
|
|
948e843ad6 | ||
|
|
7b0ea0a069 | ||
|
|
8887b6e642 | ||
|
|
597ff13393 | ||
|
|
4ab61b94a4 |
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.34.5",
|
||||
"version": "2.34.7",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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.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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.88.3",
|
||||
"version": "1.89.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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: ${
|
||||
|
||||
@@ -31,6 +31,7 @@ export class GRPCSyncingServerServiceProxy {
|
||||
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) {
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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
|
||||
|
||||
45
packages/auth/bin/fix_quota.ts
Normal file
45
packages/auth/bin/fix_quota.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
11
packages/auth/docker/entrypoint-fix-quota.js
Normal file
11
packages/auth/docker/entrypoint-fix-quota.js
Normal 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
|
||||
@@ -5,43 +5,40 @@ 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.176.1",
|
||||
"version": "1.177.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
SessionCreatedEvent,
|
||||
SessionRefreshedEvent,
|
||||
AccountDeletionVerificationRequestedEvent,
|
||||
FileQuotaRecalculationRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
@@ -34,6 +35,21 @@ import { KeyParamsData } from '@standardnotes/responses'
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Auth_Timer) private timer: TimerInterface) {}
|
||||
|
||||
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 +175,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
level: string
|
||||
body: string
|
||||
subject: string
|
||||
userUuid?: string
|
||||
}): EmailRequestedEvent {
|
||||
return {
|
||||
type: 'EMAIL_REQUESTED',
|
||||
|
||||
@@ -19,11 +19,13 @@ import {
|
||||
SessionCreatedEvent,
|
||||
SessionRefreshedEvent,
|
||||
AccountDeletionVerificationRequestedEvent,
|
||||
FileQuotaRecalculationRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
|
||||
import { KeyParamsData } from '@standardnotes/responses'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent
|
||||
createWebSocketMessageRequestedEvent(dto: { userUuid: string; message: JSONString }): WebSocketMessageRequestedEvent
|
||||
createEmailRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
@@ -31,6 +33,7 @@ export interface DomainEventFactoryInterface {
|
||||
level: string
|
||||
body: string
|
||||
subject: string
|
||||
userUuid?: string
|
||||
}): EmailRequestedEvent
|
||||
createListedAccountRequestedEvent(userUuid: string, userEmail: string): ListedAccountRequestedEvent
|
||||
createUserRegisteredEvent(dto: {
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ export class UserInvitedToSharedVaultEventHandler implements DomainEventHandlerI
|
||||
subject: getSubject(),
|
||||
messageIdentifier: 'USER_INVITED_TO_SHARED_VAULT',
|
||||
userEmail: user.email,
|
||||
userUuid: user.uuid,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface FixStorageQuotaForUserDTO {
|
||||
userEmail: string
|
||||
}
|
||||
@@ -131,6 +131,7 @@ export class SignIn implements UseCaseInterface {
|
||||
),
|
||||
messageIdentifier: 'SIGN_IN',
|
||||
subject: getSubject(user.email),
|
||||
userUuid: user.uuid,
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.22.4",
|
||||
"version": "1.22.6",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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.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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.138.1",
|
||||
"version": "2.139.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -12,4 +12,5 @@ export interface EmailRequestedEventPayload {
|
||||
attachmentFileName: string
|
||||
attachmentContentType: string
|
||||
}>
|
||||
userUuid?: string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { FileQuotaRecalculatedEventPayload } from './FileQuotaRecalculatedEventPayload'
|
||||
|
||||
export interface FileQuotaRecalculatedEvent extends DomainEventInterface {
|
||||
type: 'FILE_QUOTA_RECALCULATED'
|
||||
payload: FileQuotaRecalculatedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface FileQuotaRecalculatedEventPayload {
|
||||
userUuid: string
|
||||
totalFileByteSize: number
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { FileQuotaRecalculationRequestedEventPayload } from './FileQuotaRecalculationRequestedEventPayload'
|
||||
|
||||
export interface FileQuotaRecalculationRequestedEvent extends DomainEventInterface {
|
||||
type: 'FILE_QUOTA_RECALCULATION_REQUESTED'
|
||||
payload: FileQuotaRecalculationRequestedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface FileQuotaRecalculationRequestedEventPayload {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -30,6 +30,10 @@ 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'
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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
|
||||
|
||||
@@ -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
|
||||
;;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.36.5",
|
||||
"version": "1.37.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 }[]>
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface RecalculateQuotaDTO {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,25 @@ 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}/${userUuid}/`,
|
||||
}),
|
||||
)
|
||||
|
||||
const filesList = []
|
||||
for (const object of objectsList.Contents ?? []) {
|
||||
if (!object.Key) {
|
||||
continue
|
||||
}
|
||||
filesList.push({
|
||||
name: object.Key,
|
||||
size: object.Size ?? 0,
|
||||
})
|
||||
}
|
||||
|
||||
return filesList
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.22.16",
|
||||
"version": "1.22.21",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.51.5",
|
||||
"version": "1.51.7",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.27.10",
|
||||
"version": "1.27.12",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.128.0",
|
||||
"version": "1.129.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -458,6 +458,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)
|
||||
@@ -601,6 +604,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),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -329,6 +329,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
attachmentFileName: string
|
||||
attachmentContentType: string
|
||||
}>
|
||||
userUuid?: string
|
||||
}): EmailRequestedEvent {
|
||||
return {
|
||||
type: 'EMAIL_REQUESTED',
|
||||
|
||||
@@ -74,6 +74,7 @@ export interface DomainEventFactoryInterface {
|
||||
attachmentFileName: string
|
||||
attachmentContentType: string
|
||||
}>
|
||||
userUuid?: string
|
||||
}): EmailRequestedEvent
|
||||
createDuplicateItemSyncedEvent(dto: { itemUuid: string; userUuid: string }): DuplicateItemSyncedEvent
|
||||
createItemRevisionCreationRequested(dto: { itemUuid: string; userUuid: string }): ItemRevisionCreationRequestedEvent
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -137,6 +138,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -161,6 +163,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -182,6 +185,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: true,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -204,6 +208,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -222,6 +227,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -240,10 +246,12 @@ 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',
|
||||
@@ -284,10 +292,12 @@ 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',
|
||||
@@ -310,6 +320,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -334,6 +345,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -379,6 +391,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
@@ -84,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(
|
||||
|
||||
@@ -7,4 +7,5 @@ export interface SaveItemsDTO {
|
||||
readOnlyAccess: boolean
|
||||
sessionUuid: string | null
|
||||
snjsVersion: string
|
||||
isFreeUser: boolean
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -13,4 +13,5 @@ export type SyncItemsDTO = {
|
||||
snjsVersion: string
|
||||
readOnlyAccess: boolean
|
||||
sessionUuid: string | null
|
||||
isFreeUser: boolean
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ describe('UpdateExistingItem', () => {
|
||||
timer,
|
||||
domainEventPublisher,
|
||||
domainEventFactory,
|
||||
86_400,
|
||||
5,
|
||||
determineSharedVaultOperationOnItem,
|
||||
addNotificationsForUsers,
|
||||
@@ -137,6 +138,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 +153,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash: itemHash1,
|
||||
sessionUuid: 'invalid-uuid',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -167,6 +170,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -183,6 +187,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -206,6 +211,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -224,6 +230,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -241,6 +248,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -260,6 +268,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -280,6 +289,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -300,6 +310,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -323,6 +334,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -347,6 +359,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -361,6 +374,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash: itemHash1,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: 'invalid-uuid',
|
||||
isFreeUser: false,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
@@ -379,6 +393,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 +420,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -442,6 +458,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -470,6 +487,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 +508,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
@@ -521,6 +540,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
@@ -555,6 +575,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
@@ -576,6 +597,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
@@ -595,6 +617,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 +639,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -636,6 +660,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
@@ -658,6 +683,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
isFreeUser: false,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
mock.mockRestore()
|
||||
|
||||
@@ -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,
|
||||
@@ -169,7 +170,10 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
|
||||
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)
|
||||
|
||||
@@ -6,4 +6,5 @@ export interface UpdateExistingItemDTO {
|
||||
itemHash: ItemHash
|
||||
sessionUuid: string | null
|
||||
performingUserUuid: string
|
||||
isFreeUser: boolean
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -88,6 +88,7 @@ export class SyncingServer implements ISyncingServer {
|
||||
readOnlyAccess: call.metadata.get('x-read-only-access').pop() === 'true',
|
||||
sessionUuid: call.metadata.get('x-session-uuid').pop() as string,
|
||||
sharedVaultUuids,
|
||||
isFreeUser: call.metadata.get('x-is-free-user').pop() === 'true',
|
||||
})
|
||||
if (syncResult.isFailed()) {
|
||||
const metadata = new grpc.Metadata()
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.22.3](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.22.2...@standardnotes/websockets-server@1.22.3) (2023-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.22.2](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.22.1...@standardnotes/websockets-server@1.22.2) (2023-12-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.22.1](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.22.0...@standardnotes/websockets-server@1.22.1) (2023-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/websockets-server",
|
||||
"version": "1.22.1",
|
||||
"version": "1.22.3",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user