Compare commits

...

8 Commits

Author SHA1 Message Date
standardci
8a10d201c5 chore(release): publish new version
- @standardnotes/auth-server@1.99.0
2023-05-01 10:16:44 +00:00
Karol Sójko
9d7e63a7a7 feat(auth): add sqlite lock cache for home server (#577)
* feat(auth): add sqlite lock cache for home server

* fix(auth): lock repository binding
2023-05-01 12:02:52 +02:00
standardci
87c1ae2ac0 chore(release): publish new version
- @standardnotes/auth-server@1.98.0
2023-05-01 08:03:44 +00:00
Karol Sójko
56c922e715 feat(auth): add cache entries model (#576) 2023-05-01 09:49:27 +02:00
standardci
a29ac8e68f chore(release): publish new version
- @standardnotes/revisions-server@1.13.0
2023-04-28 11:14:22 +00:00
Karol Sójko
03f9c6039c feat(revisions): add sqlite driver (#575) 2023-04-28 13:00:24 +02:00
standardci
73d81df8cb chore(release): publish new version
- @standardnotes/analytics@2.21.9
 - @standardnotes/api-gateway@1.49.12
 - @standardnotes/auth-server@1.97.0
 - @standardnotes/domain-events-infra@1.10.2
 - @standardnotes/domain-events@2.110.0
 - @standardnotes/event-store@1.7.10
 - @standardnotes/files-server@1.10.13
 - @standardnotes/revisions-server@1.12.16
 - @standardnotes/scheduler-server@1.17.13
 - @standardnotes/settings@1.21.0
 - @standardnotes/syncing-server@1.34.0
 - @standardnotes/websockets-server@1.6.14
2023-04-27 10:38:44 +00:00
Karol Sójko
484f554339 feat: remove cloud backups (#574) 2023-04-27 12:23:30 +02:00
65 changed files with 438 additions and 767 deletions

98
.pnp.cjs generated
View File

@@ -4500,6 +4500,7 @@ const RAW_RUNTIME_STATE =
["@standardnotes/responses", "npm:1.13.9"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/time", "workspace:packages/time"],\
["@types/better-sqlite3", "npm:7.6.4"],\
["@types/cors", "npm:2.8.12"],\
["@types/dotenv", "npm:8.2.0"],\
["@types/express", "npm:4.17.14"],\
@@ -4507,6 +4508,7 @@ const RAW_RUNTIME_STATE =
["@types/jest", "npm:29.1.1"],\
["@types/newrelic", "npm:9.13.0"],\
["@typescript-eslint/eslint-plugin", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:5.48.2"],\
["better-sqlite3", "npm:8.3.0"],\
["cors", "npm:2.8.5"],\
["dotenv", "npm:16.0.1"],\
["eslint", "npm:8.32.0"],\
@@ -4713,7 +4715,7 @@ const RAW_RUNTIME_STATE =
["prettyjson", "npm:1.2.5"],\
["reflect-metadata", "npm:0.1.13"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.10"],\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.10"],\
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"],\
["ua-parser-js", "npm:1.0.32"],\
["uuid", "npm:9.0.0"],\
@@ -14762,100 +14764,6 @@ const RAW_RUNTIME_STATE =
["@google-cloud/spanner", null],\
["@sap/hana-client", null],\
["@sqltools/formatter", "npm:1.2.5"],\
["@types/better-sqlite3", null],\
["@types/google-cloud__spanner", null],\
["@types/hdb-pool", null],\
["@types/ioredis", null],\
["@types/mongodb", null],\
["@types/mssql", null],\
["@types/mysql2", null],\
["@types/oracledb", null],\
["@types/pg", null],\
["@types/pg-native", null],\
["@types/pg-query-stream", null],\
["@types/redis", null],\
["@types/sap__hana-client", null],\
["@types/sql.js", null],\
["@types/sqlite3", null],\
["@types/ts-node", null],\
["@types/typeorm-aurora-data-api-driver", null],\
["app-root-path", "npm:3.1.0"],\
["better-sqlite3", null],\
["buffer", "npm:6.0.3"],\
["chalk", "npm:4.1.2"],\
["cli-highlight", "npm:2.1.11"],\
["date-fns", "npm:2.29.3"],\
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
["dotenv", "npm:16.0.3"],\
["glob", "npm:7.2.3"],\
["hdb-pool", null],\
["ioredis", null],\
["js-yaml", "npm:4.1.0"],\
["mkdirp", "npm:1.0.4"],\
["mongodb", null],\
["mssql", null],\
["mysql2", "npm:3.0.1"],\
["oracledb", null],\
["pg", null],\
["pg-native", null],\
["pg-query-stream", null],\
["redis", null],\
["reflect-metadata", "npm:0.1.13"],\
["sha.js", "npm:2.4.11"],\
["sql.js", null],\
["sqlite3", null],\
["ts-node", null],\
["tslib", "npm:2.4.0"],\
["typeorm-aurora-data-api-driver", null],\
["uuid", "npm:8.3.2"],\
["xml2js", "npm:0.4.23"],\
["yargs", "npm:17.5.1"]\
],\
"packagePeers": [\
"@google-cloud/spanner",\
"@sap/hana-client",\
"@types/better-sqlite3",\
"@types/google-cloud__spanner",\
"@types/hdb-pool",\
"@types/ioredis",\
"@types/mongodb",\
"@types/mssql",\
"@types/mysql2",\
"@types/oracledb",\
"@types/pg-native",\
"@types/pg-query-stream",\
"@types/pg",\
"@types/redis",\
"@types/sap__hana-client",\
"@types/sql.js",\
"@types/sqlite3",\
"@types/ts-node",\
"@types/typeorm-aurora-data-api-driver",\
"better-sqlite3",\
"hdb-pool",\
"ioredis",\
"mongodb",\
"mssql",\
"mysql2",\
"oracledb",\
"pg-native",\
"pg-query-stream",\
"pg",\
"redis",\
"sql.js",\
"sqlite3",\
"ts-node",\
"typeorm-aurora-data-api-driver"\
],\
"linkType": "HARD"\
}],\
["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.10", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-f86b034570/0/cache/typeorm-npm-0.3.10-4667857f33-749e1a6777.zip/node_modules/typeorm/",\
"packageDependencies": [\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.10"],\
["@google-cloud/spanner", null],\
["@sap/hana-client", null],\
["@sqltools/formatter", "npm:1.2.5"],\
["@types/better-sqlite3", "npm:7.6.4"],\
["@types/google-cloud__spanner", null],\
["@types/hdb-pool", null],\

View File

@@ -342,7 +342,7 @@ endif
quiet_cmd_regen_makefile = ACTION Regenerating $@
cmd_regen_makefile = cd $(srcdir); /Users/karolsojko/workspace/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/karolsojko/Library/Caches/node-gyp/18.15.0" "-Dnode_gyp_dir=/Users/karolsojko/workspace/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp" "-Dnode_lib_file=/Users/karolsojko/Library/Caches/node-gyp/18.15.0/<(target_arch)/node.lib" "-Dmodule_root_dir=/Users/karolsojko/workspace/server/.yarn/unplugged/better-sqlite3-npm-8.3.0-d1ef3f5776/node_modules/better-sqlite3" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/Users/karolsojko/workspace/server/.yarn/unplugged/better-sqlite3-npm-8.3.0-d1ef3f5776/node_modules/better-sqlite3/build/config.gypi -I/Users/karolsojko/workspace/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/addon.gypi -I/Users/karolsojko/Library/Caches/node-gyp/18.15.0/include/node/common.gypi "--toplevel-dir=." binding.gyp
Makefile: $(srcdir)/deps/defines.gypi $(srcdir)/../../../../../../../Library/Caches/node-gyp/18.15.0/include/node/common.gypi $(srcdir)/deps/sqlite3.gyp $(srcdir)/binding.gyp $(srcdir)/../../../node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/addon.gypi $(srcdir)/deps/common.gypi $(srcdir)/build/config.gypi
Makefile: $(srcdir)/build/config.gypi $(srcdir)/binding.gyp $(srcdir)/../../../../../../../Library/Caches/node-gyp/18.15.0/include/node/common.gypi $(srcdir)/deps/defines.gypi $(srcdir)/../../../node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/addon.gypi $(srcdir)/deps/common.gypi $(srcdir)/deps/sqlite3.gyp
$(call do_cmd,regen_makefile)
# "all" is a concatenation of the "all" targets from all the included

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.21.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.21.8...@standardnotes/analytics@2.21.9) (2023-04-27)
**Note:** Version bump only for package @standardnotes/analytics
## [2.21.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.21.7...@standardnotes/analytics@2.21.8) (2023-04-27)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.21.8",
"version": "2.21.9",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.49.12](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.49.11...@standardnotes/api-gateway@1.49.12) (2023-04-27)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.49.11](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.49.10...@standardnotes/api-gateway@1.49.11) (2023-04-27)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.49.11",
"version": "1.49.12",
"engines": {
"node": ">=18.0.0 <19.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.
# [1.99.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.98.0...@standardnotes/auth-server@1.99.0) (2023-05-01)
### Features
* **auth:** add sqlite lock cache for home server ([#577](https://github.com/standardnotes/server/issues/577)) ([9d7e63a](https://github.com/standardnotes/server/commit/9d7e63a7a78adcb9817084e460a01189012bc403))
# [1.98.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.97.0...@standardnotes/auth-server@1.98.0) (2023-05-01)
### Features
* **auth:** add cache entries model ([#576](https://github.com/standardnotes/server/issues/576)) ([56c922e](https://github.com/standardnotes/server/commit/56c922e715167935885df2c4d93c5e1d685e0298))
# [1.97.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.96.0...@standardnotes/auth-server@1.97.0) (2023-04-27)
### Features
* remove cloud backups ([#574](https://github.com/standardnotes/server/issues/574)) ([484f554](https://github.com/standardnotes/server/commit/484f55433928e5c21ee59d8fda94ab3c887cd169))
# [1.96.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.95.3...@standardnotes/auth-server@1.96.0) (2023-04-27)
### Features

View File

@@ -14,10 +14,9 @@ import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
import { MuteFailedBackupsEmailsOption, MuteFailedCloudBackupsEmailsOption, SettingName } from '@standardnotes/settings'
import { MuteFailedBackupsEmailsOption, SettingName } from '@standardnotes/settings'
import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
import { PermissionName } from '@standardnotes/features'
import { SettingServiceInterface } from '../src/Domain/Setting/SettingServiceInterface'
const inputArgs = process.argv.slice(2)
const backupProvider = inputArgs[0]
@@ -26,46 +25,13 @@ const backupFrequency = inputArgs[1]
const requestBackups = async (
settingRepository: SettingRepositoryInterface,
roleService: RoleServiceInterface,
settingService: SettingServiceInterface,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
): Promise<void> => {
let settingName: SettingName,
permissionName: PermissionName,
muteEmailsSettingName: string,
muteEmailsSettingValue: string,
providerTokenSettingName: SettingName
switch (backupProvider) {
case 'email':
settingName = SettingName.create(SettingName.NAMES.EmailBackupFrequency).getValue()
permissionName = PermissionName.DailyEmailBackup
muteEmailsSettingName = SettingName.NAMES.MuteFailedBackupsEmails
muteEmailsSettingValue = MuteFailedBackupsEmailsOption.Muted
break
case 'dropbox':
settingName = SettingName.create(SettingName.NAMES.DropboxBackupFrequency).getValue()
permissionName = PermissionName.DailyDropboxBackup
muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted
providerTokenSettingName = SettingName.create(SettingName.NAMES.DropboxBackupToken).getValue()
break
case 'one_drive':
settingName = SettingName.create(SettingName.NAMES.OneDriveBackupFrequency).getValue()
permissionName = PermissionName.DailyOneDriveBackup
muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted
providerTokenSettingName = SettingName.create(SettingName.NAMES.OneDriveBackupToken).getValue()
break
case 'google_drive':
settingName = SettingName.create(SettingName.NAMES.GoogleDriveBackupFrequency).getValue()
permissionName = PermissionName.DailyGDriveBackup
muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted
providerTokenSettingName = SettingName.create(SettingName.NAMES.GoogleDriveBackupToken).getValue()
break
default:
throw new Error(`Not handled backup provider: ${backupProvider}`)
}
const settingName = SettingName.create(SettingName.NAMES.EmailBackupFrequency).getValue()
const permissionName = PermissionName.DailyEmailBackup
const muteEmailsSettingName = SettingName.NAMES.MuteFailedBackupsEmails
const muteEmailsSettingValue = MuteFailedBackupsEmailsOption.Muted
const stream = await settingRepository.streamAllByNameAndValue(settingName, backupFrequency)
@@ -94,39 +60,14 @@ const requestBackups = async (
userHasEmailsMuted = emailsMutedSetting.value === muteEmailsSettingValue
}
if (backupProvider === 'email') {
await domainEventPublisher.publish(
domainEventFactory.createEmailBackupRequestedEvent(
setting.setting_user_uuid,
emailsMutedSetting?.uuid as string,
userHasEmailsMuted,
),
)
callback()
return
}
const cloudBackupProviderToken = await settingService.findSettingWithDecryptedValue({
settingName: providerTokenSettingName,
userUuid: setting.setting_user_uuid,
})
if (cloudBackupProviderToken === null || cloudBackupProviderToken.value === null) {
callback()
return
}
await domainEventPublisher.publish(
domainEventFactory.createCloudBackupRequestedEvent(
backupProvider.toUpperCase() as 'DROPBOX' | 'ONE_DRIVE' | 'GOOGLE_DRIVE',
cloudBackupProviderToken.value,
domainEventFactory.createEmailBackupRequestedEvent(
setting.setting_user_uuid,
emailsMutedSetting?.uuid as string,
userHasEmailsMuted,
),
)
callback()
},
}),
@@ -149,13 +90,10 @@ void container.load().then((container) => {
const settingRepository: SettingRepositoryInterface = container.get(TYPES.SettingRepository)
const roleService: RoleServiceInterface = container.get(TYPES.RoleService)
const settingService: SettingServiceInterface = container.get(TYPES.SettingService)
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
Promise.resolve(
requestBackups(settingRepository, roleService, settingService, domainEventFactory, domainEventPublisher),
)
Promise.resolve(requestBackups(settingRepository, roleService, domainEventFactory, domainEventPublisher))
.then(() => {
logger.info(`${backupFrequency} ${backupProvider} backup requesting complete`)

View File

@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class cacheEntries1682926032072 implements MigrationInterface {
name = 'cacheEntries1682926032072'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `cache_entries` (`uuid` varchar(36) NOT NULL, `key` text NOT NULL, `value` text NOT NULL, `expires_at` datetime NULL, PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP TABLE `cache_entries`')
}
}

View File

@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class cacheEntries1682925969528 implements MigrationInterface {
name = 'cacheEntries1682925969528'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE "cache_entries" ("uuid" varchar PRIMARY KEY NOT NULL, "key" text NOT NULL, "value" text NOT NULL, "expires_at" datetime)',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP TABLE "cache_entries"')
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.96.0",
"version": "1.99.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -171,6 +171,7 @@ import { SubscriptionSettingProjector } from '../Projection/SubscriptionSettingP
import { SubscriptionSettingsAssociationService } from '../Domain/Setting/SubscriptionSettingsAssociationService'
import { SubscriptionSettingsAssociationServiceInterface } from '../Domain/Setting/SubscriptionSettingsAssociationServiceInterface'
import { PKCERepositoryInterface } from '../Domain/User/PKCERepositoryInterface'
import { LockRepositoryInterface } from '../Domain/User/LockRepositoryInterface'
import { RedisPKCERepository } from '../Infra/Redis/RedisPKCERepository'
import { RoleRepositoryInterface } from '../Domain/Role/RoleRepositoryInterface'
import { RevokedSessionRepositoryInterface } from '../Domain/Session/RevokedSessionRepositoryInterface'
@@ -216,6 +217,12 @@ import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/G
import { SignInWithRecoveryCodes } from '../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
import { GetUserKeyParamsRecovery } from '../Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery'
import { CleanupExpiredSessions } from '../Domain/UseCase/CleanupExpiredSessions/CleanupExpiredSessions'
import { TypeORMCacheEntry } from '../Infra/TypeORM/TypeORMCacheEntry'
import { CacheEntryRepositoryInterface } from '../Domain/Cache/CacheEntryRepositoryInterface'
import { TypeORMCacheEntryRepository } from '../Infra/TypeORM/TypeORMCacheEntryRepository'
import { CacheEntry } from '../Domain/Cache/CacheEntry'
import { CacheEntryPersistenceMapper } from '../Mapping/CacheEntryPersistenceMapper'
import { TypeORMLockRepository } from '../Infra/TypeORM/TypeORMLockRepository'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -229,6 +236,8 @@ export class ContainerConfigLoader {
await AppDataSource.initialize()
const isConfiguredForHomeServer = env.get('DB_TYPE') === 'sqlite'
const redisUrl = env.get('REDIS_URL')
const isRedisInClusterMode = redisUrl.indexOf(',') > 0
let redis
@@ -298,6 +307,9 @@ export class ContainerConfigLoader {
TYPES.AuthenticatorChallengePersistenceMapper,
)
.toConstantValue(new AuthenticatorChallengePersistenceMapper())
container
.bind<MapperInterface<CacheEntry, TypeORMCacheEntry>>(TYPES.CacheEntryPersistenceMapper)
.toConstantValue(new CacheEntryPersistenceMapper())
// ORM
container
@@ -335,6 +347,9 @@ export class ContainerConfigLoader {
container
.bind<Repository<TypeORMAuthenticatorChallenge>>(TYPES.ORMAuthenticatorChallengeRepository)
.toConstantValue(AppDataSource.getRepository(TypeORMAuthenticatorChallenge))
container
.bind<Repository<TypeORMCacheEntry>>(TYPES.ORMCacheEntryRepository)
.toConstantValue(AppDataSource.getRepository(TypeORMCacheEntry))
// Repositories
container.bind<SessionRepositoryInterface>(TYPES.SessionRepository).to(TypeORMSessionRepository)
@@ -359,7 +374,6 @@ export class ContainerConfigLoader {
container
.bind<RedisEphemeralSessionRepository>(TYPES.EphemeralSessionRepository)
.to(RedisEphemeralSessionRepository)
container.bind<LockRepository>(TYPES.LockRepository).to(LockRepository)
container
.bind<SubscriptionTokenRepositoryInterface>(TYPES.SubscriptionTokenRepository)
.to(RedisSubscriptionTokenRepository)
@@ -394,6 +408,14 @@ export class ContainerConfigLoader {
container.get(TYPES.AuthenticatorChallengePersistenceMapper),
),
)
container
.bind<CacheEntryRepositoryInterface>(TYPES.CacheEntryRepository)
.toConstantValue(
new TypeORMCacheEntryRepository(
container.get(TYPES.ORMCacheEntryRepository),
container.get(TYPES.CacheEntryPersistenceMapper),
),
)
// Middleware
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware)
@@ -471,6 +493,21 @@ export class ContainerConfigLoader {
.bind(TYPES.READONLY_USERS)
.toConstantValue(env.get('READONLY_USERS', true) ? env.get('READONLY_USERS', true).split(',') : [])
if (isConfiguredForHomeServer) {
container
.bind<LockRepositoryInterface>(TYPES.LockRepository)
.toConstantValue(
new TypeORMLockRepository(
container.get(TYPES.CacheEntryRepository),
container.get(TYPES.Timer),
container.get(TYPES.MAX_LOGIN_ATTEMPTS),
container.get(TYPES.FAILED_LOGIN_LOCKOUT),
),
)
} else {
container.bind<LockRepositoryInterface>(TYPES.LockRepository).to(LockRepository)
}
// Services
container.bind<UAParser>(TYPES.DeviceDetector).toConstantValue(new UAParser())
container.bind<SessionService>(TYPES.SessionService).to(SessionService)

View File

@@ -14,6 +14,7 @@ import { UserSubscription } from '../Domain/Subscription/UserSubscription'
import { User } from '../Domain/User/User'
import { TypeORMAuthenticator } from '../Infra/TypeORM/TypeORMAuthenticator'
import { TypeORMAuthenticatorChallenge } from '../Infra/TypeORM/TypeORMAuthenticatorChallenge'
import { TypeORMCacheEntry } from '../Infra/TypeORM/TypeORMCacheEntry'
import { TypeORMEmergencyAccessInvitation } from '../Infra/TypeORM/TypeORMEmergencyAccessInvitation'
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
import { Env } from './Env'
@@ -68,6 +69,7 @@ const commonDataSourceOptions = {
TypeORMAuthenticator,
TypeORMAuthenticatorChallenge,
TypeORMEmergencyAccessInvitation,
TypeORMCacheEntry,
],
migrations: [`dist/migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
migrationsRun: true,

View File

@@ -8,6 +8,7 @@ const TYPES = {
AuthenticatorChallengePersistenceMapper: Symbol.for('AuthenticatorChallengePersistenceMapper'),
AuthenticatorPersistenceMapper: Symbol.for('AuthenticatorPersistenceMapper'),
AuthenticatorHttpMapper: Symbol.for('AuthenticatorHttpMapper'),
CacheEntryPersistenceMapper: Symbol.for('CacheEntryPersistenceMapper'),
// Controller
AuthController: Symbol.for('AuthController'),
AuthenticatorsController: Symbol.for('AuthenticatorsController'),
@@ -32,6 +33,7 @@ const TYPES = {
SessionTraceRepository: Symbol.for('SessionTraceRepository'),
AuthenticatorRepository: Symbol.for('AuthenticatorRepository'),
AuthenticatorChallengeRepository: Symbol.for('AuthenticatorChallengeRepository'),
CacheEntryRepository: Symbol.for('CacheEntryRepository'),
// ORM
ORMOfflineSettingRepository: Symbol.for('ORMOfflineSettingRepository'),
ORMOfflineUserSubscriptionRepository: Symbol.for('ORMOfflineUserSubscriptionRepository'),
@@ -46,6 +48,7 @@ const TYPES = {
ORMSessionTraceRepository: Symbol.for('ORMSessionTraceRepository'),
ORMAuthenticatorRepository: Symbol.for('ORMAuthenticatorRepository'),
ORMAuthenticatorChallengeRepository: Symbol.for('ORMAuthenticatorChallengeRepository'),
ORMCacheEntryRepository: Symbol.for('ORMCacheEntryRepository'),
// Middleware
AuthMiddleware: Symbol.for('AuthMiddleware'),
ApiGatewayAuthMiddleware: Symbol.for('ApiGatewayAuthMiddleware'),

View File

@@ -0,0 +1,17 @@
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
import { CacheEntryProps } from './CacheEntryProps'
export class CacheEntry extends Entity<CacheEntryProps> {
get id(): UniqueEntityId {
return this._id
}
private constructor(props: CacheEntryProps, id?: UniqueEntityId) {
super(props, id)
}
static create(props: CacheEntryProps, id?: UniqueEntityId): Result<CacheEntry> {
return Result.ok<CacheEntry>(new CacheEntry(props, id))
}
}

View File

@@ -0,0 +1,5 @@
export interface CacheEntryProps {
key: string
value: string
expiresAt: Date | null
}

View File

@@ -0,0 +1,7 @@
import { CacheEntry } from './CacheEntry'
export interface CacheEntryRepositoryInterface {
save(cacheEntry: CacheEntry): Promise<void>
findUnexpiredOneByKey(key: string): Promise<CacheEntry | null>
removeByKey(key: string): Promise<void>
}

View File

@@ -7,7 +7,6 @@ import {
UserRegisteredEvent,
UserRolesChangedEvent,
EmailBackupRequestedEvent,
CloudBackupRequestedEvent,
ListedAccountRequestedEvent,
UserDisabledSessionUserAgentLoggingEvent,
SharedSubscriptionInvitationCreatedEvent,
@@ -254,33 +253,6 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
}
}
createCloudBackupRequestedEvent(
cloudProvider: 'DROPBOX' | 'ONE_DRIVE' | 'GOOGLE_DRIVE',
cloudProviderToken: string,
userUuid: string,
muteEmailsSettingUuid: string,
userHasEmailsMuted: boolean,
): CloudBackupRequestedEvent {
return {
type: 'CLOUD_BACKUP_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: userUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.Auth,
},
payload: {
cloudProvider,
cloudProviderToken,
userUuid,
userHasEmailsMuted,
muteEmailsSettingUuid,
},
}
}
createEmailBackupRequestedEvent(
userUuid: string,
muteEmailsSettingUuid: string,

View File

@@ -2,7 +2,6 @@ import { ProtocolVersion, JSONString } from '@standardnotes/common'
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
import {
AccountDeletionRequestedEvent,
CloudBackupRequestedEvent,
UserRegisteredEvent,
UserRolesChangedEvent,
UserEmailChangedEvent,
@@ -42,13 +41,6 @@ export interface DomainEventFactoryInterface {
muteEmailsSettingUuid: string,
userHasEmailsMuted: boolean,
): EmailBackupRequestedEvent
createCloudBackupRequestedEvent(
cloudProvider: 'DROPBOX' | 'ONE_DRIVE' | 'GOOGLE_DRIVE',
cloudProviderToken: string,
userUuid: string,
muteEmailsSettingUuid: string,
userHasEmailsMuted: boolean,
): CloudBackupRequestedEvent
createAccountDeletionRequestedEvent(dto: {
userUuid: string
userCreatedAtTimestamp: number

View File

@@ -1,5 +1,4 @@
import {
CloudBackupRequestedEvent,
DomainEventPublisherInterface,
EmailBackupRequestedEvent,
MuteEmailsSettingChangedEvent,
@@ -9,7 +8,6 @@ import {
EmailBackupFrequency,
LogSessionUserAgentOption,
MuteMarketingEmailsOption,
OneDriveBackupFrequency,
SettingName,
} from '@standardnotes/settings'
import 'reflect-metadata'
@@ -30,8 +28,7 @@ describe('SettingInterpreter', () => {
let settingDecrypter: SettingDecrypterInterface
let logger: Logger
const createInterpreter = () =>
new SettingInterpreter(domainEventPublisher, domainEventFactory, settingRepository, settingDecrypter, logger)
const createInterpreter = () => new SettingInterpreter(domainEventPublisher, domainEventFactory, settingRepository)
beforeEach(() => {
user = {
@@ -53,9 +50,6 @@ describe('SettingInterpreter', () => {
domainEventFactory.createEmailBackupRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<EmailBackupRequestedEvent>)
domainEventFactory.createCloudBackupRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<CloudBackupRequestedEvent>)
domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<UserDisabledSessionUserAgentLoggingEvent>)
@@ -124,70 +118,6 @@ describe('SettingInterpreter', () => {
expect(domainEventFactory.createEmailBackupRequestedEvent).not.toHaveBeenCalled()
})
it('should trigger cloud backup if dropbox backup setting is created', async () => {
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
await createInterpreter().interpretSettingUpdated(SettingName.NAMES.DropboxBackupToken, user, 'test-token')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith(
'DROPBOX',
'test-token',
'4-5-6',
'',
false,
)
})
it('should trigger cloud backup if dropbox backup setting is created - muted emails', async () => {
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({
name: SettingName.NAMES.MuteFailedCloudBackupsEmails,
uuid: '6-7-8',
value: 'muted',
} as jest.Mocked<Setting>)
await createInterpreter().interpretSettingUpdated(SettingName.NAMES.DropboxBackupToken, user, 'test-token')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith(
'DROPBOX',
'test-token',
'4-5-6',
'6-7-8',
true,
)
})
it('should trigger cloud backup if google drive backup setting is created', async () => {
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
await createInterpreter().interpretSettingUpdated(SettingName.NAMES.GoogleDriveBackupToken, user, 'test-token')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith(
'GOOGLE_DRIVE',
'test-token',
'4-5-6',
'',
false,
)
})
it('should trigger cloud backup if one drive backup setting is created', async () => {
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
await createInterpreter().interpretSettingUpdated(SettingName.NAMES.OneDriveBackupToken, user, 'test-token')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith(
'ONE_DRIVE',
'test-token',
'4-5-6',
'',
false,
)
})
it('should trigger mute subscription emails rejection if mute setting changed', async () => {
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
@@ -204,51 +134,4 @@ describe('SettingInterpreter', () => {
username: 'test@test.te',
})
})
it('should trigger cloud backup if backup frequency setting is updated and a backup token setting is present', async () => {
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce({
name: SettingName.NAMES.OneDriveBackupToken,
serverEncryptionVersion: 1,
value: 'encrypted-backup-token',
sensitive: true,
} as jest.Mocked<Setting>)
await createInterpreter().interpretSettingUpdated(SettingName.NAMES.OneDriveBackupFrequency, user, 'daily')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith(
'ONE_DRIVE',
'decrypted',
'4-5-6',
'',
false,
)
})
it('should not trigger cloud backup if backup frequency setting is updated as disabled', async () => {
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce({
name: SettingName.NAMES.OneDriveBackupToken,
serverEncryptionVersion: 1,
value: 'encrypted-backup-token',
sensitive: true,
} as jest.Mocked<Setting>)
await createInterpreter().interpretSettingUpdated(
SettingName.NAMES.OneDriveBackupFrequency,
user,
OneDriveBackupFrequency.Disabled,
)
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).not.toHaveBeenCalled()
})
it('should not trigger cloud backup if backup frequency setting is updated and a backup token setting is not present', async () => {
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce(null)
await createInterpreter().interpretSettingUpdated(SettingName.NAMES.OneDriveBackupFrequency, user, 'daily')
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).not.toHaveBeenCalled()
})
})

View File

@@ -1,44 +1,21 @@
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { EmailLevel } from '@standardnotes/domain-core'
import {
DropboxBackupFrequency,
EmailBackupFrequency,
GoogleDriveBackupFrequency,
LogSessionUserAgentOption,
MuteFailedBackupsEmailsOption,
MuteFailedCloudBackupsEmailsOption,
OneDriveBackupFrequency,
SettingName,
} from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { User } from '../User/User'
import { SettingDecrypterInterface } from './SettingDecrypterInterface'
import { SettingInterpreterInterface } from './SettingInterpreterInterface'
import { SettingRepositoryInterface } from './SettingRepositoryInterface'
@injectable()
export class SettingInterpreter implements SettingInterpreterInterface {
private readonly cloudBackupTokenSettings = [
SettingName.NAMES.DropboxBackupToken,
SettingName.NAMES.GoogleDriveBackupToken,
SettingName.NAMES.OneDriveBackupToken,
]
private readonly cloudBackupFrequencySettings = [
SettingName.NAMES.DropboxBackupFrequency,
SettingName.NAMES.GoogleDriveBackupFrequency,
SettingName.NAMES.OneDriveBackupFrequency,
]
private readonly cloudBackupFrequencyDisabledValues = [
DropboxBackupFrequency.Disabled,
GoogleDriveBackupFrequency.Disabled,
OneDriveBackupFrequency.Disabled,
]
private readonly emailSettingToSubscriptionRejectionLevelMap: Map<string, string> = new Map([
[SettingName.NAMES.MuteFailedBackupsEmails, EmailLevel.LEVELS.FailedEmailBackup],
[SettingName.NAMES.MuteFailedCloudBackupsEmails, EmailLevel.LEVELS.FailedCloudBackup],
@@ -50,8 +27,6 @@ export class SettingInterpreter implements SettingInterpreterInterface {
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.SettingRepository) private settingRepository: SettingRepositoryInterface,
@inject(TYPES.SettingDecrypter) private settingDecrypter: SettingDecrypterInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
async interpretSettingUpdated(
@@ -67,10 +42,6 @@ export class SettingInterpreter implements SettingInterpreterInterface {
await this.triggerEmailBackup(user.uuid)
}
if (this.isEnablingCloudBackupSetting(updatedSettingName, unencryptedValue)) {
await this.triggerCloudBackup(updatedSettingName, user.uuid, unencryptedValue)
}
if (this.isDisablingSessionUserAgentLogging(updatedSettingName, unencryptedValue)) {
await this.triggerSessionUserAgentCleanup(user)
}
@@ -109,16 +80,6 @@ export class SettingInterpreter implements SettingInterpreterInterface {
)
}
private isEnablingCloudBackupSetting(settingName: string, newValue: string | null): boolean {
return (
(this.cloudBackupFrequencySettings.includes(settingName) ||
this.cloudBackupTokenSettings.includes(settingName)) &&
!this.cloudBackupFrequencyDisabledValues.includes(
newValue as DropboxBackupFrequency | OneDriveBackupFrequency | GoogleDriveBackupFrequency,
)
)
}
private isDisablingSessionUserAgentLogging(settingName: string, newValue: string | null): boolean {
return SettingName.NAMES.LogSessionUserAgent === settingName && LogSessionUserAgentOption.Disabled === newValue
}
@@ -145,67 +106,4 @@ export class SettingInterpreter implements SettingInterpreterInterface {
}),
)
}
private async triggerCloudBackup(
settingName: string,
userUuid: string,
unencryptedValue: string | null,
): Promise<void> {
let cloudProvider
let tokenSettingName
switch (settingName) {
case SettingName.NAMES.DropboxBackupToken:
case SettingName.NAMES.DropboxBackupFrequency:
cloudProvider = 'DROPBOX'
tokenSettingName = SettingName.NAMES.DropboxBackupToken
break
case SettingName.NAMES.GoogleDriveBackupToken:
case SettingName.NAMES.GoogleDriveBackupFrequency:
cloudProvider = 'GOOGLE_DRIVE'
tokenSettingName = SettingName.NAMES.GoogleDriveBackupToken
break
case SettingName.NAMES.OneDriveBackupToken:
case SettingName.NAMES.OneDriveBackupFrequency:
cloudProvider = 'ONE_DRIVE'
tokenSettingName = SettingName.NAMES.OneDriveBackupToken
break
}
let backupToken = null
if (this.cloudBackupFrequencySettings.includes(settingName)) {
const tokenSetting = await this.settingRepository.findLastByNameAndUserUuid(tokenSettingName as string, userUuid)
if (tokenSetting !== null) {
backupToken = await this.settingDecrypter.decryptSettingValue(tokenSetting, userUuid)
}
} else {
backupToken = unencryptedValue
}
if (!backupToken) {
this.logger.error(`Could not trigger backup. Missing backup token for user ${userUuid}`)
return
}
let userHasEmailsMuted = false
let muteEmailsSettingUuid = ''
const muteFailedCloudBackupSetting = await this.settingRepository.findOneByNameAndUserUuid(
SettingName.NAMES.MuteFailedCloudBackupsEmails,
userUuid,
)
if (muteFailedCloudBackupSetting !== null) {
userHasEmailsMuted = muteFailedCloudBackupSetting.value === MuteFailedCloudBackupsEmailsOption.Muted
muteEmailsSettingUuid = muteFailedCloudBackupSetting.uuid
}
await this.domainEventPublisher.publish(
this.domainEventFactory.createCloudBackupRequestedEvent(
cloudProvider as 'DROPBOX' | 'GOOGLE_DRIVE' | 'ONE_DRIVE',
backupToken,
userUuid,
muteEmailsSettingUuid,
userHasEmailsMuted,
),
)
}
}

View File

@@ -0,0 +1,26 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'cache_entries' })
export class TypeORMCacheEntry {
@PrimaryGeneratedColumn('uuid')
declare uuid: string
@Column({
name: 'key',
type: 'text',
})
declare key: string
@Column({
name: 'value',
type: 'text',
})
declare value: string
@Column({
name: 'expires_at',
type: 'datetime',
nullable: true,
})
declare expiresAt: Date | null
}

View File

@@ -0,0 +1,40 @@
import { MapperInterface } from '@standardnotes/domain-core'
import { Repository } from 'typeorm'
import { CacheEntry } from '../../Domain/Cache/CacheEntry'
import { CacheEntryRepositoryInterface } from '../../Domain/Cache/CacheEntryRepositoryInterface'
import { TypeORMCacheEntry } from './TypeORMCacheEntry'
export class TypeORMCacheEntryRepository implements CacheEntryRepositoryInterface {
constructor(
private ormRepository: Repository<TypeORMCacheEntry>,
private mapper: MapperInterface<CacheEntry, TypeORMCacheEntry>,
) {}
async save(cacheEntry: CacheEntry): Promise<void> {
const persistence = this.mapper.toProjection(cacheEntry)
await this.ormRepository.save(persistence)
}
async findUnexpiredOneByKey(key: string): Promise<CacheEntry | null> {
const persistence = await this.ormRepository
.createQueryBuilder('cache')
.where('cache.key = :key', {
key,
})
.andWhere('cache.expires_at > :now', {
now: new Date(),
})
.getOne()
if (persistence === null) {
return null
}
return this.mapper.toDomain(persistence)
}
async removeByKey(key: string): Promise<void> {
await this.ormRepository.createQueryBuilder().delete().where('key = :key', { key }).execute()
}
}

View File

@@ -0,0 +1,85 @@
import { TimerInterface } from '@standardnotes/time'
import { CacheEntry } from '../../Domain/Cache/CacheEntry'
import { CacheEntryRepositoryInterface } from '../../Domain/Cache/CacheEntryRepositoryInterface'
import { LockRepositoryInterface } from '../../Domain/User/LockRepositoryInterface'
export class TypeORMLockRepository implements LockRepositoryInterface {
private readonly PREFIX = 'lock'
private readonly OTP_PREFIX = 'otp-lock'
constructor(
private cacheEntryRepository: CacheEntryRepositoryInterface,
private timer: TimerInterface,
private maxLoginAttempts: number,
private failedLoginLockout: number,
) {}
async lockSuccessfullOTP(userIdentifier: string, otp: string): Promise<void> {
const cacheEntryOrError = CacheEntry.create({
key: `${this.OTP_PREFIX}:${userIdentifier}`,
value: otp,
expiresAt: this.timer.getUTCDateNSecondsAhead(60),
})
if (cacheEntryOrError.isFailed()) {
throw new Error('Could not create cache entry')
}
await this.cacheEntryRepository.save(cacheEntryOrError.getValue())
}
async isOTPLocked(userIdentifier: string, otp: string): Promise<boolean> {
const lock = await this.cacheEntryRepository.findUnexpiredOneByKey(`${this.OTP_PREFIX}:${userIdentifier}`)
if (!lock) {
return false
}
return lock.props.value === otp
}
async resetLockCounter(userIdentifier: string): Promise<void> {
await this.cacheEntryRepository.removeByKey(`${this.PREFIX}:${userIdentifier}`)
}
async updateLockCounter(userIdentifier: string, counter: number): Promise<void> {
let cacheEntry = await this.cacheEntryRepository.findUnexpiredOneByKey(`${this.PREFIX}:${userIdentifier}`)
if (!cacheEntry) {
cacheEntry = CacheEntry.create({
key: `${this.PREFIX}:${userIdentifier}`,
value: counter.toString(),
expiresAt: this.timer.getUTCDateNSecondsAhead(this.failedLoginLockout),
}).getValue()
} else {
cacheEntry.props.value = counter.toString()
cacheEntry.props.expiresAt = this.timer.getUTCDateNSecondsAhead(this.failedLoginLockout)
}
await this.cacheEntryRepository.save(cacheEntry)
}
async getLockCounter(userIdentifier: string): Promise<number> {
const counter = await this.cacheEntryRepository.findUnexpiredOneByKey(`${this.PREFIX}:${userIdentifier}`)
if (!counter) {
return 0
}
return +counter.props.value
}
async lockUser(userIdentifier: string): Promise<void> {
const cacheEntry = await this.cacheEntryRepository.findUnexpiredOneByKey(`${this.PREFIX}:${userIdentifier}`)
if (cacheEntry !== null) {
cacheEntry.props.expiresAt = this.timer.getUTCDateNSecondsAhead(this.failedLoginLockout)
await this.cacheEntryRepository.save(cacheEntry)
}
}
async isUserLocked(userIdentifier: string): Promise<boolean> {
const counter = await this.getLockCounter(userIdentifier)
return counter >= this.maxLoginAttempts
}
}

View File

@@ -0,0 +1,33 @@
import { MapperInterface, UniqueEntityId } from '@standardnotes/domain-core'
import { CacheEntry } from '../Domain/Cache/CacheEntry'
import { TypeORMCacheEntry } from '../Infra/TypeORM/TypeORMCacheEntry'
export class CacheEntryPersistenceMapper implements MapperInterface<CacheEntry, TypeORMCacheEntry> {
toDomain(projection: TypeORMCacheEntry): CacheEntry {
const cacheEntryOrError = CacheEntry.create(
{
key: projection.key,
value: projection.value,
expiresAt: projection.expiresAt,
},
new UniqueEntityId(projection.uuid),
)
if (cacheEntryOrError.isFailed()) {
throw new Error(`CacheEntryPersistenceMapper.toDomain: ${cacheEntryOrError.getError()}`)
}
return cacheEntryOrError.getValue()
}
toProjection(domain: CacheEntry): TypeORMCacheEntry {
const typeorm = new TypeORMCacheEntry()
typeorm.uuid = domain.id.toString()
typeorm.key = domain.props.key
typeorm.value = domain.props.value
typeorm.expiresAt = domain.props.expiresAt
return typeorm
}
}

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.10.1...@standardnotes/domain-events-infra@1.10.2) (2023-04-27)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.10.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.10.0...@standardnotes/domain-events-infra@1.10.1) (2023-04-21)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.10.1",
"version": "1.10.2",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.110.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.109.0...@standardnotes/domain-events@2.110.0) (2023-04-27)
### Features
* remove cloud backups ([#574](https://github.com/standardnotes/server/issues/574)) ([484f554](https://github.com/standardnotes/server/commit/484f55433928e5c21ee59d8fda94ab3c887cd169))
# [2.109.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.108.1...@standardnotes/domain-events@2.109.0) (2023-04-21)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.109.0",
"version": "2.110.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -1,7 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { CloudBackupRequestedEventPayload } from './CloudBackupRequestedEventPayload'
export interface CloudBackupRequestedEvent extends DomainEventInterface {
type: 'CLOUD_BACKUP_REQUESTED'
payload: CloudBackupRequestedEventPayload
}

View File

@@ -1,7 +0,0 @@
export interface CloudBackupRequestedEventPayload {
cloudProvider: 'DROPBOX' | 'ONE_DRIVE' | 'GOOGLE_DRIVE'
cloudProviderToken: string
userUuid: string
userHasEmailsMuted: boolean
muteEmailsSettingUuid: string
}

View File

@@ -1,7 +1,5 @@
export * from './Event/AccountDeletionRequestedEvent'
export * from './Event/AccountDeletionRequestedEventPayload'
export * from './Event/CloudBackupRequestedEvent'
export * from './Event/CloudBackupRequestedEventPayload'
export * from './Event/DiscountApplyRequestedEvent'
export * from './Event/DiscountApplyRequestedEventPayload'
export * from './Event/DiscountWithdrawRequestedEvent'

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.7.10](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.7.9...@standardnotes/event-store@1.7.10) (2023-04-27)
**Note:** Version bump only for package @standardnotes/event-store
## [1.7.9](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.7.8...@standardnotes/event-store@1.7.9) (2023-04-21)
**Note:** Version bump only for package @standardnotes/event-store

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.7.9",
"version": "1.7.10",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.13](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.10.12...@standardnotes/files-server@1.10.13) (2023-04-27)
**Note:** Version bump only for package @standardnotes/files-server
## [1.10.12](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.10.11...@standardnotes/files-server@1.10.12) (2023-04-27)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.10.12",
"version": "1.10.13",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -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.13.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.12.16...@standardnotes/revisions-server@1.13.0) (2023-04-28)
### Features
* **revisions:** add sqlite driver ([#575](https://github.com/standardnotes/server/issues/575)) ([03f9c60](https://github.com/standardnotes/server/commit/03f9c6039c309fde1a762a010e70e8189f5a8e15))
## [1.12.16](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.12.15...@standardnotes/revisions-server@1.12.16) (2023-04-27)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.12.15](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.12.14...@standardnotes/revisions-server@1.12.15) (2023-04-27)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class initialBoilerplate1682678053275 implements MigrationInterface {
name = 'initialBoilerplate1682678053275'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE "revisions" ("uuid" varchar PRIMARY KEY NOT NULL, "item_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36), "content" text, "content_type" varchar(255), "items_key_id" varchar(255), "enc_item_key" text, "auth_hash" varchar(255), "creation_date" date, "created_at" datetime(6), "updated_at" datetime(6))',
)
await queryRunner.query('CREATE INDEX "item_uuid" ON "revisions" ("item_uuid") ')
await queryRunner.query('CREATE INDEX "user_uuid" ON "revisions" ("user_uuid") ')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX "user_uuid"')
await queryRunner.query('DROP INDEX "item_uuid"')
await queryRunner.query('DROP TABLE "revisions"')
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.12.15",
"version": "1.13.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -35,6 +35,7 @@
"@standardnotes/responses": "^1.13.9",
"@standardnotes/security": "workspace:^",
"@standardnotes/time": "workspace:^",
"better-sqlite3": "^8.3.0",
"cors": "2.8.5",
"dotenv": "^16.0.1",
"express": "^4.18.2",
@@ -47,6 +48,7 @@
"winston": "^3.8.1"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.4",
"@types/cors": "^2.8.9",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.14",

View File

@@ -2,10 +2,11 @@ import { MapperInterface } from '@standardnotes/domain-core'
import { Container, interfaces } from 'inversify'
import { Repository } from 'typeorm'
import * as winston from 'winston'
import { Revision } from '../Domain/Revision/Revision'
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
import { RevisionRepositoryInterface } from '../Domain/Revision/RevisionRepositoryInterface'
import { MySQLRevisionRepository } from '../Infra/MySQL/MySQLRevisionRepository'
import { TypeORMRevisionRepository } from '../Infra/TypeORM/TypeORMRevisionRepository'
import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision'
import { RevisionMetadataPersistenceMapper } from '../Mapping/RevisionMetadataPersistenceMapper'
import { RevisionPersistenceMapper } from '../Mapping/RevisionPersistenceMapper'
@@ -67,7 +68,7 @@ export class CommonContainerConfigLoader {
container
.bind<RevisionRepositoryInterface>(TYPES.RevisionRepository)
.toDynamicValue((context: interfaces.Context) => {
return new MySQLRevisionRepository(
return new TypeORMRevisionRepository(
context.container.get(TYPES.ORMRevisionRepository),
context.container.get(TYPES.RevisionMetadataPersistenceMapper),
context.container.get(TYPES.RevisionPersistenceMapper),

View File

@@ -1,4 +1,6 @@
import { DataSource, LoggerOptions } from 'typeorm'
import { BetterSqlite3ConnectionOptions } from 'typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions'
import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'
import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision'
@@ -7,6 +9,8 @@ import { Env } from './Env'
const env: Env = new Env()
env.load()
const isConfiguredForMySQL = env.get('DB_TYPE') === 'mysql'
const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
@@ -34,22 +38,32 @@ const replicationConfig = {
restoreNodeTimeout: 5,
}
const dataSource = new DataSource({
const commonDataSourceOptions = {
maxQueryExecutionTime,
entities: [TypeORMRevision],
migrations: [`dist/migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
migrationsRun: true,
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),
}
const mySQLDataSourceOptions: MysqlConnectionOptions = {
...commonDataSourceOptions,
type: 'mysql',
charset: 'utf8mb4',
supportBigNumbers: true,
bigNumberStrings: false,
maxQueryExecutionTime,
replication: inReplicaMode ? replicationConfig : undefined,
host: inReplicaMode ? undefined : env.get('DB_HOST'),
port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')),
username: inReplicaMode ? undefined : env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : env.get('DB_DATABASE'),
entities: [TypeORMRevision],
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
migrationsRun: true,
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),
})
}
export const AppDataSource = dataSource
const sqliteDataSourceOptions: BetterSqlite3ConnectionOptions = {
...commonDataSourceOptions,
type: 'better-sqlite3',
database: `data/${env.get('DB_DATABASE')}.sqlite`,
}
export const AppDataSource = new DataSource(isConfiguredForMySQL ? mySQLDataSourceOptions : sqliteDataSourceOptions)

View File

@@ -22,7 +22,7 @@ export class TypeORMRevision {
declare userUuid: string | null
@Column({
type: 'mediumtext',
type: 'text',
nullable: true,
})
declare content: string | null

View File

@@ -5,9 +5,9 @@ import { Logger } from 'winston'
import { Revision } from '../../Domain/Revision/Revision'
import { RevisionMetadata } from '../../Domain/Revision/RevisionMetadata'
import { RevisionRepositoryInterface } from '../../Domain/Revision/RevisionRepositoryInterface'
import { TypeORMRevision } from '../TypeORM/TypeORMRevision'
import { TypeORMRevision } from './TypeORMRevision'
export class MySQLRevisionRepository implements RevisionRepositoryInterface {
export class TypeORMRevisionRepository implements RevisionRepositoryInterface {
constructor(
private ormRepository: Repository<TypeORMRevision>,
private revisionMetadataMapper: MapperInterface<RevisionMetadata, TypeORMRevision>,
@@ -97,7 +97,7 @@ export class MySQLRevisionRepository implements RevisionRepositoryInterface {
const simplifiedRevisions = await queryBuilder.getRawMany()
this.logger.debug(
`Found ${simplifiedRevisions.length} revisions MySQL entries for item ${itemUuid.value}`,
`Found ${simplifiedRevisions.length} revisions entries for item ${itemUuid.value}`,
simplifiedRevisions,
)

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.17.13](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.17.12...@standardnotes/scheduler-server@1.17.13) (2023-04-27)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.17.12](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.17.11...@standardnotes/scheduler-server@1.17.12) (2023-04-27)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.17.12",
"version": "1.17.13",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.21.0](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.20.2...@standardnotes/settings@1.21.0) (2023-04-27)
### Features
* remove cloud backups ([#574](https://github.com/standardnotes/server/issues/574)) ([484f554](https://github.com/standardnotes/server/commit/484f55433928e5c21ee59d8fda94ab3c887cd169))
## [1.20.2](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.20.1...@standardnotes/settings@1.20.2) (2023-04-27)
**Note:** Version bump only for package @standardnotes/settings

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/settings",
"version": "1.20.2",
"version": "1.21.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -1,5 +0,0 @@
export enum CloudProvider {
Dropbox = 'Dropbox',
Google = 'Google Drive',
OneDrive = 'OneDrive',
}

View File

@@ -1,5 +0,0 @@
export enum DropboxBackupFrequency {
Disabled = 'disabled',
Daily = 'daily',
Weekly = 'weekly',
}

View File

@@ -1,5 +0,0 @@
export enum GoogleDriveBackupFrequency {
Disabled = 'disabled',
Daily = 'daily',
Weekly = 'weekly',
}

View File

@@ -1,4 +0,0 @@
export enum MuteFailedCloudBackupsEmailsOption {
Muted = 'muted',
NotMuted = 'not_muted',
}

View File

@@ -1,5 +0,0 @@
export enum OneDriveBackupFrequency {
Disabled = 'disabled',
Daily = 'daily',
Weekly = 'weekly',
}

View File

@@ -1,13 +1,8 @@
export * from './CloudProvider/CloudProvider'
export * from './DropboxBackupFrequency/DropboxBackupFrequency'
export * from './EmailBackupFrequency/EmailBackupFrequency'
export * from './GoogleDriveBackupFrequency/GoogleDriveBackupFrequency'
export * from './ListedAuthorSecretsData/ListedAuthorSecretsData'
export * from './LogSessionUserAgent/LogSessionUserAgentOption'
export * from './MuteFailedBackupsEmails/MuteFailedBackupsEmailsOption'
export * from './MuteFailedCloudBackupsEmails/MuteFailedCloudBackupsEmailsOption'
export * from './MuteMarketingEmails/MuteMarketingEmailsOption'
export * from './MuteSignInEmails/MuteSignInEmailsOption'
export * from './OneDriveBackupFrequency/OneDriveBackupFrequency'
export * from './Setting/SettingName'
export * from './Setting/SettingNameProps'

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.34.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.33.0...@standardnotes/syncing-server@1.34.0) (2023-04-27)
### Features
* remove cloud backups ([#574](https://github.com/standardnotes/syncing-server-js/issues/574)) ([484f554](https://github.com/standardnotes/syncing-server-js/commit/484f55433928e5c21ee59d8fda94ab3c887cd169))
# [1.33.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.32.8...@standardnotes/syncing-server@1.33.0) (2023-04-27)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.33.0",
"version": "1.34.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -42,7 +42,6 @@ const TYPES = {
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
DuplicateItemSyncedEventHandler: Symbol.for('DuplicateItemSyncedEventHandler'),
EmailBackupRequestedEventHandler: Symbol.for('EmailBackupRequestedEventHandler'),
CloudBackupRequestedEventHandler: Symbol.for('CloudBackupRequestedEventHandler'),
ItemRevisionCreationRequestedEventHandler: Symbol.for('ItemRevisionCreationRequestedEventHandler'),
// Services
ContentDecoder: Symbol.for('ContentDecoder'),

View File

@@ -27,7 +27,6 @@ import {
SQSNewRelicEventMessageHandler,
} from '@standardnotes/domain-events-infra'
import { EmailBackupRequestedEventHandler } from '../Domain/Handler/EmailBackupRequestedEventHandler'
import { CloudBackupRequestedEventHandler } from '../Domain/Handler/CloudBackupRequestedEventHandler'
import { ItemRevisionCreationRequestedEventHandler } from '../Domain/Handler/ItemRevisionCreationRequestedEventHandler'
import { FSItemBackupService } from '../Infra/FS/FSItemBackupService'
import { CommonContainerConfigLoader } from './CommonContainerConfigLoader'
@@ -122,18 +121,6 @@ export class WorkerContainerConfigLoader extends CommonContainerConfigLoader {
context.container.get(TYPES.Logger),
)
})
container
.bind<CloudBackupRequestedEventHandler>(TYPES.CloudBackupRequestedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new CloudBackupRequestedEventHandler(
context.container.get(TYPES.ItemRepository),
context.container.get(TYPES.AuthHttpService),
context.container.get(TYPES.ExtensionsHttpService),
context.container.get(TYPES.ItemBackupService),
context.container.get(TYPES.EXTENSIONS_SERVER_URL),
context.container.get(TYPES.Logger),
)
})
container
.bind<ItemRevisionCreationRequestedEventHandler>(TYPES.ItemRevisionCreationRequestedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
@@ -194,7 +181,6 @@ export class WorkerContainerConfigLoader extends CommonContainerConfigLoader {
['DUPLICATE_ITEM_SYNCED', context.container.get(TYPES.DuplicateItemSyncedEventHandler)],
['ACCOUNT_DELETION_REQUESTED', context.container.get(TYPES.AccountDeletionRequestedEventHandler)],
['EMAIL_BACKUP_REQUESTED', context.container.get(TYPES.EmailBackupRequestedEventHandler)],
['CLOUD_BACKUP_REQUESTED', context.container.get(TYPES.CloudBackupRequestedEventHandler)],
['ITEM_REVISION_CREATION_REQUESTED', context.container.get(TYPES.ItemRevisionCreationRequestedEventHandler)],
])

View File

@@ -1,177 +0,0 @@
import 'reflect-metadata'
import { CloudBackupRequestedEvent } from '@standardnotes/domain-events'
import { AuthHttpServiceInterface } from '../Auth/AuthHttpServiceInterface'
import { Item } from '../Item/Item'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
import { CloudBackupRequestedEventHandler } from './CloudBackupRequestedEventHandler'
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
import { ExtensionsHttpServiceInterface } from '../Extension/ExtensionsHttpServiceInterface'
import { Logger } from 'winston'
describe('CloudBackupRequestedEventHandler', () => {
let itemRepository: ItemRepositoryInterface
let authHttpService: AuthHttpServiceInterface
let extensionsHttpService: ExtensionsHttpServiceInterface
let itemBackupService: ItemBackupServiceInterface
const extensionsServerUrl = 'https://extensions-server'
let event: CloudBackupRequestedEvent
let item: Item
let logger: Logger
const createHandler = () =>
new CloudBackupRequestedEventHandler(
itemRepository,
authHttpService,
extensionsHttpService,
itemBackupService,
extensionsServerUrl,
logger,
)
beforeEach(() => {
item = {} as jest.Mocked<Item>
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findAll = jest.fn().mockReturnValue([item])
authHttpService = {} as jest.Mocked<AuthHttpServiceInterface>
authHttpService.getUserKeyParams = jest.fn().mockReturnValue({ foo: 'bar' })
extensionsHttpService = {} as jest.Mocked<ExtensionsHttpServiceInterface>
extensionsHttpService.triggerCloudBackupOnExtensionsServer = jest.fn()
event = {} as jest.Mocked<CloudBackupRequestedEvent>
event.createdAt = new Date(1)
event.payload = {
cloudProvider: 'DROPBOX',
cloudProviderToken: 'test-token',
userUuid: '1-2-3',
muteEmailsSettingUuid: '2-3-4',
userHasEmailsMuted: false,
}
itemBackupService = {} as jest.Mocked<ItemBackupServiceInterface>
itemBackupService.backup = jest.fn().mockReturnValue(['backup-file-name'])
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
logger.warn = jest.fn()
})
it('should trigger cloud backup on extensions server - dropbox', async () => {
await createHandler().handle(event)
expect(itemRepository.findAll).toHaveBeenCalledWith({
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
userUuid: '1-2-3',
deleted: false,
})
expect(extensionsHttpService.triggerCloudBackupOnExtensionsServer).toHaveBeenCalledWith({
authParams: {
foo: 'bar',
},
backupFilename: 'backup-file-name',
cloudProvider: 'DROPBOX',
extensionsServerUrl: 'https://extensions-server/dropbox/items/sync?type=sf&dbt=test-token',
muteEmailsSettingUuid: '2-3-4',
forceMute: false,
userUuid: '1-2-3',
})
})
it('should trigger cloud backup on extensions server - google drive', async () => {
event.payload.cloudProvider = 'GOOGLE_DRIVE'
await createHandler().handle(event)
expect(extensionsHttpService.triggerCloudBackupOnExtensionsServer).toHaveBeenCalledWith({
authParams: {
foo: 'bar',
},
backupFilename: 'backup-file-name',
cloudProvider: 'GOOGLE_DRIVE',
extensionsServerUrl: 'https://extensions-server/gdrive/sync?key=test-token',
muteEmailsSettingUuid: '2-3-4',
forceMute: false,
userUuid: '1-2-3',
})
})
it('should trigger cloud backup on extensions server - one drive', async () => {
event.payload.cloudProvider = 'ONE_DRIVE'
await createHandler().handle(event)
expect(extensionsHttpService.triggerCloudBackupOnExtensionsServer).toHaveBeenCalledWith({
authParams: {
foo: 'bar',
},
backupFilename: 'backup-file-name',
cloudProvider: 'ONE_DRIVE',
extensionsServerUrl: 'https://extensions-server/onedrive/sync?type=sf&key=test-token',
muteEmailsSettingUuid: '2-3-4',
forceMute: false,
userUuid: '1-2-3',
})
})
it('should not trigger cloud backup on extensions server - unknown', async () => {
event.payload.cloudProvider = 'test' as 'DROPBOX' | 'GOOGLE_DRIVE' | 'ONE_DRIVE'
let expectedError = null
try {
await createHandler().handle(event)
} catch (error) {
expectedError = error
}
expect(extensionsHttpService.triggerCloudBackupOnExtensionsServer).not.toHaveBeenCalled()
expect(expectedError).not.toBeNull()
})
it('should not trigger cloud backup if backup filename is not returned', async () => {
itemBackupService.backup = jest.fn().mockReturnValue([])
await createHandler().handle(event)
expect(extensionsHttpService.triggerCloudBackupOnExtensionsServer).not.toHaveBeenCalled()
})
it('should trigger cloud backup on extensions server with muted emails', async () => {
event.payload.userHasEmailsMuted = true
await createHandler().handle(event)
expect(itemRepository.findAll).toHaveBeenCalledWith({
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
userUuid: '1-2-3',
deleted: false,
})
expect(extensionsHttpService.triggerCloudBackupOnExtensionsServer).toHaveBeenCalledWith({
authParams: {
foo: 'bar',
},
backupFilename: 'backup-file-name',
cloudProvider: 'DROPBOX',
extensionsServerUrl: 'https://extensions-server/dropbox/items/sync?type=sf&dbt=test-token',
muteEmailsSettingUuid: '2-3-4',
forceMute: true,
userUuid: '1-2-3',
})
})
it('should skip triggering cloud backups on extensions server if user key params cannot be obtained', async () => {
authHttpService.getUserKeyParams = jest.fn().mockImplementation(() => {
throw new Error('Oops!')
})
await createHandler().handle(event)
expect(extensionsHttpService.triggerCloudBackupOnExtensionsServer).not.toHaveBeenCalled()
})
})

View File

@@ -1,80 +0,0 @@
import { DomainEventHandlerInterface, CloudBackupRequestedEvent } from '@standardnotes/domain-events'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
import { ItemQuery } from '../Item/ItemQuery'
import { AuthHttpServiceInterface } from '../Auth/AuthHttpServiceInterface'
import { Item } from '../Item/Item'
import { ExtensionsHttpServiceInterface } from '../Extension/ExtensionsHttpServiceInterface'
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
import { Logger } from 'winston'
import { KeyParamsData } from '@standardnotes/responses'
export class CloudBackupRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
private itemRepository: ItemRepositoryInterface,
private authHttpService: AuthHttpServiceInterface,
private extensionsHttpService: ExtensionsHttpServiceInterface,
private itemBackupService: ItemBackupServiceInterface,
private extensionsServerUrl: string,
private logger: Logger,
) {}
async handle(event: CloudBackupRequestedEvent): Promise<void> {
const items = await this.getItemsForPostingToExtension(event)
let authParams: KeyParamsData
try {
authParams = await this.authHttpService.getUserKeyParams({
uuid: event.payload.userUuid,
authenticated: false,
})
} catch (error) {
this.logger.warn(`Could not get user key params from auth service: ${(error as Error).message}`)
return
}
const backupFilenames = await this.itemBackupService.backup(items, authParams)
if (backupFilenames.length === 0) {
this.logger.warn(`No backup files created for user ${event.payload.userUuid}`)
return
}
this.logger.debug(`Sending ${items.length} items to extensions server for user ${event.payload.userUuid}`)
await this.extensionsHttpService.triggerCloudBackupOnExtensionsServer({
cloudProvider: event.payload.cloudProvider,
authParams,
backupFilename: backupFilenames[0],
forceMute: event.payload.userHasEmailsMuted,
muteEmailsSettingUuid: event.payload.muteEmailsSettingUuid,
extensionsServerUrl: this.getExtensionsServerUrl(event),
userUuid: event.payload.userUuid,
})
}
private getExtensionsServerUrl(event: CloudBackupRequestedEvent): string {
switch (event.payload.cloudProvider) {
case 'ONE_DRIVE':
return `${this.extensionsServerUrl}/onedrive/sync?type=sf&key=${event.payload.cloudProviderToken}`
case 'GOOGLE_DRIVE':
return `${this.extensionsServerUrl}/gdrive/sync?key=${event.payload.cloudProviderToken}`
case 'DROPBOX':
return `${this.extensionsServerUrl}/dropbox/items/sync?type=sf&dbt=${event.payload.cloudProviderToken}`
default:
throw new Error(`Unsupported cloud provider ${event.payload.cloudProvider}`)
}
}
private async getItemsForPostingToExtension(event: CloudBackupRequestedEvent): Promise<Item[]> {
const itemQuery: ItemQuery = {
userUuid: event.payload.userUuid,
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
deleted: false,
}
return this.itemRepository.findAll(itemQuery)
}
}

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.6.14](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.6.13...@standardnotes/websockets-server@1.6.14) (2023-04-27)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.6.13](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.6.12...@standardnotes/websockets-server@1.6.13) (2023-04-27)
**Note:** Version bump only for package @standardnotes/websockets-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.6.13",
"version": "1.6.14",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3604,6 +3604,7 @@ __metadata:
"@standardnotes/responses": "npm:^1.13.9"
"@standardnotes/security": "workspace:^"
"@standardnotes/time": "workspace:^"
"@types/better-sqlite3": "npm:^7.6.4"
"@types/cors": "npm:^2.8.9"
"@types/dotenv": "npm:^8.2.0"
"@types/express": "npm:^4.17.14"
@@ -3611,6 +3612,7 @@ __metadata:
"@types/jest": "npm:^29.1.1"
"@types/newrelic": "npm:^9.13.0"
"@typescript-eslint/eslint-plugin": "npm:^5.48.2"
better-sqlite3: "npm:^8.3.0"
cors: "npm:2.8.5"
dotenv: "npm:^16.0.1"
eslint: "npm:^8.32.0"