Compare commits

..

20 Commits

Author SHA1 Message Date
standardci e6d8e5c5f2 chore(release): publish new version
- @standardnotes/analytics@2.33.0
 - @standardnotes/api-gateway@1.82.0
 - @standardnotes/auth-server@1.168.0
 - @standardnotes/domain-events-infra@1.21.0
 - @standardnotes/domain-events@2.134.0
 - @standardnotes/event-store@1.14.0
 - @standardnotes/files-server@1.33.0
 - @standardnotes/home-server@1.19.0
 - @standardnotes/revisions-server@1.48.0
 - @standardnotes/scheduler-server@1.27.0
 - @standardnotes/syncing-server@1.121.0
 - @standardnotes/websockets-server@1.18.0
2023-11-10 11:46:24 +00:00
Karol Sójko c24353cc24 feat: add graceful shutdown procedures upon SIGTERM (#923) 2023-11-10 12:20:21 +01:00
standardci 4855e1d5f5 chore(release): publish new version
- @standardnotes/api-gateway@1.81.14
 - @standardnotes/home-server@1.18.32
2023-11-10 10:04:31 +00:00
Karol Sójko 5d3fb9a537 fix(api-gateway): add logs about calling web sockets with minimal format 2023-11-10 10:32:47 +01:00
standardci b55d80a7cd chore(release): publish new version
- @standardnotes/api-gateway@1.81.13
 - @standardnotes/home-server@1.18.31
2023-11-09 14:29:28 +00:00
Karol Sójko 16f92bdc99 fix(api-gateway): add possibility to configure keep-alive timeout (#920) 2023-11-09 15:02:32 +01:00
standardci 4c5738416a chore(release): publish new version
- @standardnotes/home-server@1.18.30
 - @standardnotes/syncing-server@1.120.5
 - @standardnotes/websockets-server@1.17.8
2023-11-09 13:26:30 +00:00
Karol Sójko 45d4920e0f fix: remove unused axios dep in subservices 2023-11-09 14:03:44 +01:00
standardci 94e738532a chore(release): publish new version
- @standardnotes/api-gateway@1.81.12
 - @standardnotes/home-server@1.18.29
 - @standardnotes/websockets-server@1.17.7
2023-11-09 11:18:35 +00:00
Karol Sójko c4ae12d53f fix: reduce websockets api communication data (#919) 2023-11-09 11:44:31 +01:00
standardci 4ff78452f9 chore(release): publish new version
- @standardnotes/auth-server@1.167.2
 - @standardnotes/home-server@1.18.28
 - @standardnotes/syncing-server@1.120.4
2023-11-08 15:38:47 +00:00
Karol Sójko 9465f2ecd8 fix: add logs about sending websocket events to clients 2023-11-08 16:10:44 +01:00
standardci 93c2f1f12f chore(release): publish new version
- @standardnotes/auth-server@1.167.1
 - @standardnotes/home-server@1.18.27
2023-11-08 12:15:35 +00:00
Karol Sójko ca8a3fc77d fix(auth): path to delete accounts script 2023-11-08 12:30:12 +01:00
standardci 00936e06bc chore(release): publish new version
- @standardnotes/auth-server@1.167.0
 - @standardnotes/home-server@1.18.26
2023-11-08 10:48:51 +00:00
Karol Sójko a6dea50d74 feat: script to mass delete accounts from CSV source (#913) 2023-11-08 11:21:00 +01:00
standardci 28b04e6a4a chore(release): publish new version
- @standardnotes/auth-server@1.166.0
 - @standardnotes/home-server@1.18.25
2023-11-07 14:49:54 +00:00
Karol Sójko d228a86f48 feat(auth): add triggering post setting update actions (#905)
* feat(auth): add triggering post setting update actions

* feat(auth): refactor email backups

* fix: add extra logs for backups

* fix: specs
2023-11-07 15:14:51 +01:00
standardci 0cb234aa47 chore(release): publish new version
- @standardnotes/api-gateway@1.81.11
 - @standardnotes/home-server@1.18.24
2023-11-07 11:28:36 +00:00
Karol Sójko 6b554c28b7 fix(api-gateway): remove calling both auth and payments on account deletion request 2023-11-07 12:02:40 +01:00
180 changed files with 3101 additions and 1298 deletions
Generated
+904 -2
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6
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.33.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.32.6...@standardnotes/analytics@2.33.0) (2023-11-10)
### Features
* add graceful shutdown procedures upon SIGTERM ([#923](https://github.com/standardnotes/server/issues/923)) ([c24353c](https://github.com/standardnotes/server/commit/c24353cc24ebf4b40ff9a2cec8e37cfdef109e37))
## [2.32.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.32.5...@standardnotes/analytics@2.32.6) (2023-11-07)
**Note:** Version bump only for package @standardnotes/analytics
+6
View File
@@ -22,5 +22,11 @@ void container.load().then((container) => {
const subscriber = container.get<DomainEventSubscriberInterface>(TYPES.DomainEventSubscriber)
process.on('SIGTERM', () => {
logger.info('SIGTERM received. Stopping worker...')
subscriber.stop()
logger.info('Worker stopped.')
})
subscriber.start()
})
+2 -2
View File
@@ -6,12 +6,12 @@ COMMAND=$1 && shift 1
case "$COMMAND" in
'start-worker' )
echo "[Docker] Starting Worker..."
node docker/entrypoint-worker.js
exec node docker/entrypoint-worker.js
;;
'report' )
echo "[Docker] Starting Usage Report Generation..."
node docker/entrypoint-report.js
exec node docker/entrypoint-report.js
;;
* )
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.32.6",
"version": "2.33.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.82.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.14...@standardnotes/api-gateway@1.82.0) (2023-11-10)
### Features
* add graceful shutdown procedures upon SIGTERM ([#923](https://github.com/standardnotes/api-gateway/issues/923)) ([c24353c](https://github.com/standardnotes/api-gateway/commit/c24353cc24ebf4b40ff9a2cec8e37cfdef109e37))
## [1.81.14](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.13...@standardnotes/api-gateway@1.81.14) (2023-11-10)
### Bug Fixes
* **api-gateway:** add logs about calling web sockets with minimal format ([5d3fb9a](https://github.com/standardnotes/api-gateway/commit/5d3fb9a537f6971cfe8ae3c5ea449806cc4de8a0))
## [1.81.13](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.12...@standardnotes/api-gateway@1.81.13) (2023-11-09)
### Bug Fixes
* **api-gateway:** add possibility to configure keep-alive timeout ([#920](https://github.com/standardnotes/api-gateway/issues/920)) ([16f92bd](https://github.com/standardnotes/api-gateway/commit/16f92bdc990ded5c3f1fe5af1e6e4a113a9954de))
## [1.81.12](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.11...@standardnotes/api-gateway@1.81.12) (2023-11-09)
### Bug Fixes
* reduce websockets api communication data ([#919](https://github.com/standardnotes/api-gateway/issues/919)) ([c4ae12d](https://github.com/standardnotes/api-gateway/commit/c4ae12d53fc166879f90a4c5dbad1ab1cb4797e2))
## [1.81.11](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.10...@standardnotes/api-gateway@1.81.11) (2023-11-07)
### Bug Fixes
* **api-gateway:** remove calling both auth and payments on account deletion request ([6b554c2](https://github.com/standardnotes/api-gateway/commit/6b554c28b731a9080d7ad2942d3fa05c01dcabf2))
## [1.81.10](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.9...@standardnotes/api-gateway@1.81.10) (2023-11-07)
**Note:** Version bump only for package @standardnotes/api-gateway
+11 -2
View File
@@ -102,9 +102,18 @@ void container.load().then((container) => {
})
})
const serverInstance = server.build()
const serverInstance = server.build().listen(env.get('PORT'))
serverInstance.listen(env.get('PORT'))
const keepAliveTimeout = env.get('KEEP_ALIVE_TIMEOUT', true) ? +env.get('KEEP_ALIVE_TIMEOUT', true) : 5000
serverInstance.keepAliveTimeout = keepAliveTimeout
process.on('SIGTERM', () => {
logger.info('SIGTERM signal received: closing HTTP server')
serverInstance.close(() => {
logger.info('HTTP server closed')
})
})
logger.info(`Server started on port ${process.env.PORT}`)
})
+1 -1
View File
@@ -6,7 +6,7 @@ COMMAND=$1 && shift 1
case "$COMMAND" in
'start-web' )
echo "Starting Web..."
node docker/entrypoint-server.js
exec node docker/entrypoint-server.js
;;
* )
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.81.10",
"version": "1.82.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -23,7 +23,6 @@ export class UsersController extends BaseHttpController {
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
@inject(TYPES.ApiGateway_IS_CONFIGURED_FOR_HOME_SERVER) private isConfiguredForHomeServer: boolean,
) {
super()
}
@@ -238,10 +237,6 @@ export class UsersController extends BaseHttpController {
@httpDelete('/:userUuid', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async deleteUser(request: Request, response: Response): Promise<void> {
if (!this.isConfiguredForHomeServer) {
await this.httpService.callPaymentsServer(request, response, 'api/account', request.body, true)
}
await this.httpService.callAuthServer(
request,
response,
@@ -143,7 +143,21 @@ export class HttpServiceProxy implements ServiceProxyInterface {
return
}
await this.callServer(this.webSocketServerUrl, request, response, endpointOrMethodIdentifier, payload)
const isARequestComingFromApiGatewayAndShouldBeKeptInMinimalFormat = request.headers.connectionid !== undefined
this.logger.info(
`Calling websockets service: ${endpointOrMethodIdentifier}. Format is minimal: ${isARequestComingFromApiGatewayAndShouldBeKeptInMinimalFormat}`,
)
if (isARequestComingFromApiGatewayAndShouldBeKeptInMinimalFormat) {
await this.callServerWithLegacyFormat(
this.webSocketServerUrl,
request,
response,
endpointOrMethodIdentifier,
payload,
)
} else {
await this.callServer(this.webSocketServerUrl, request, response, endpointOrMethodIdentifier, payload)
}
}
async callPaymentsServer(
@@ -151,7 +165,6 @@ export class HttpServiceProxy implements ServiceProxyInterface {
response: Response,
endpointOrMethodIdentifier: string,
payload?: Record<string, unknown> | string,
returnRawResponse?: boolean,
): Promise<void | Response<unknown, Record<string, unknown>>> {
if (!this.paymentsServerUrl) {
this.logger.debug('Payments Server URL not defined. Skipped request to Payments API.')
@@ -159,18 +172,13 @@ export class HttpServiceProxy implements ServiceProxyInterface {
return
}
const rawResponse = await this.callServerWithLegacyFormat(
await this.callServerWithLegacyFormat(
this.paymentsServerUrl,
request,
response,
endpointOrMethodIdentifier,
payload,
returnRawResponse,
)
if (returnRawResponse) {
return rawResponse
}
}
async callAuthServerWithLegacyFormat(
@@ -345,7 +353,6 @@ export class HttpServiceProxy implements ServiceProxyInterface {
response: Response,
endpointOrMethodIdentifier: string,
payload?: Record<string, unknown> | string,
returnRawResponse?: boolean,
): Promise<void | Response<unknown, Record<string, unknown>>> {
const serviceResponse = await this.getServerResponse(
serverUrl,
@@ -364,18 +371,10 @@ export class HttpServiceProxy implements ServiceProxyInterface {
if (serviceResponse.request._redirectable._redirectCount > 0) {
response.status(302)
if (returnRawResponse) {
return response
}
response.redirect(serviceResponse.request.res.responseUrl)
} else {
response.status(serviceResponse.status)
if (returnRawResponse) {
return response
}
response.send(serviceResponse.data)
}
}
@@ -42,7 +42,6 @@ export interface ServiceProxyInterface {
response: Response,
endpointOrMethodIdentifier: string,
payload?: Record<string, unknown> | string,
returnRawResponse?: boolean,
): Promise<void | Response<unknown, Record<string, unknown>>>
callWebSocketServer(
request: Request,
@@ -6,4 +6,4 @@ sh supervisor/wait-for.sh localhost $AUTH_SERVER_PORT
sh supervisor/wait-for.sh localhost $FILES_SERVER_PORT
sh supervisor/wait-for.sh localhost $REVISIONS_SERVER_PORT
sh supervisor/wait-for.sh localhost $SYNCING_SERVER_PORT
node docker/entrypoint-server.js
exec node docker/entrypoint-server.js
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.168.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.167.2...@standardnotes/auth-server@1.168.0) (2023-11-10)
### Features
* add graceful shutdown procedures upon SIGTERM ([#923](https://github.com/standardnotes/server/issues/923)) ([c24353c](https://github.com/standardnotes/server/commit/c24353cc24ebf4b40ff9a2cec8e37cfdef109e37))
## [1.167.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.167.1...@standardnotes/auth-server@1.167.2) (2023-11-08)
### Bug Fixes
* add logs about sending websocket events to clients ([9465f2e](https://github.com/standardnotes/server/commit/9465f2ecd8e8f0bf3ebeeb3976227b1b105aded0))
## [1.167.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.167.0...@standardnotes/auth-server@1.167.1) (2023-11-08)
### Bug Fixes
* **auth:** path to delete accounts script ([ca8a3fc](https://github.com/standardnotes/server/commit/ca8a3fc77d91410f0dee8c3ddef29c09947c9cf5))
# [1.167.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.166.0...@standardnotes/auth-server@1.167.0) (2023-11-08)
### Features
* script to mass delete accounts from CSV source ([#913](https://github.com/standardnotes/server/issues/913)) ([a6dea50](https://github.com/standardnotes/server/commit/a6dea50d745ff6f051fd9ede168aef27036159c3))
# [1.166.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.165.4...@standardnotes/auth-server@1.166.0) (2023-11-07)
### Features
* **auth:** add triggering post setting update actions ([#905](https://github.com/standardnotes/server/issues/905)) ([d228a86](https://github.com/standardnotes/server/commit/d228a86f48c9ff62b7810244c347abf7770e2b9f))
## [1.165.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.165.3...@standardnotes/auth-server@1.165.4) (2023-11-07)
### Bug Fixes
+11 -84
View File
@@ -1,9 +1,5 @@
import 'reflect-metadata'
import { SettingName } from '@standardnotes/domain-core'
import { Stream } from 'stream'
import { Logger } from 'winston'
import * as dayjs from 'dayjs'
import * as utc from 'dayjs/plugin/utc'
@@ -11,78 +7,13 @@ import * as utc from 'dayjs/plugin/utc'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
import { MuteFailedBackupsEmailsOption } from '@standardnotes/settings'
import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
import { PermissionName } from '@standardnotes/features'
import { GetUserKeyParams } from '../src/Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
import { TriggerEmailBackupForAllUsers } from '../src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers'
const inputArgs = process.argv.slice(2)
const backupProvider = inputArgs[0]
const backupFrequency = inputArgs[1]
const backupFrequency = inputArgs[0]
const requestBackups = async (
settingRepository: SettingRepositoryInterface,
roleService: RoleServiceInterface,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
getUserKeyParamsUseCase: GetUserKeyParams,
): Promise<void> => {
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)
return new Promise((resolve, reject) => {
stream
.pipe(
new Stream.Transform({
objectMode: true,
transform: async (setting, _encoding, callback) => {
const userIsPermittedForEmailBackups = await roleService.userHasPermission(
setting.setting_user_uuid,
permissionName,
)
if (!userIsPermittedForEmailBackups) {
callback()
return
}
let userHasEmailsMuted = false
const emailsMutedSetting = await settingRepository.findOneByNameAndUserUuid(
muteEmailsSettingName,
setting.setting_user_uuid,
)
if (emailsMutedSetting !== null && emailsMutedSetting.props.value !== null) {
userHasEmailsMuted = emailsMutedSetting.props.value === muteEmailsSettingValue
}
const keyParamsResponse = await getUserKeyParamsUseCase.execute({
userUuid: setting.setting_user_uuid,
authenticated: false,
})
await domainEventPublisher.publish(
domainEventFactory.createEmailBackupRequestedEvent(
setting.setting_user_uuid,
emailsMutedSetting?.id.toString() as string,
userHasEmailsMuted,
keyParamsResponse.keyParams,
),
)
callback()
},
}),
)
.on('finish', resolve)
.on('error', reject)
})
const requestBackups = async (triggerEmailBackupForAllUsers: TriggerEmailBackupForAllUsers): Promise<void> => {
await triggerEmailBackupForAllUsers.execute({ backupFrequency })
}
const container = new ContainerConfigLoader('worker')
@@ -94,24 +25,20 @@ void container.load().then((container) => {
const logger: Logger = container.get(TYPES.Auth_Logger)
logger.info(`Starting ${backupFrequency} ${backupProvider} backup requesting...`)
logger.info(`Starting ${backupFrequency} email backup requesting...`)
const settingRepository: SettingRepositoryInterface = container.get(TYPES.Auth_SettingRepository)
const roleService: RoleServiceInterface = container.get(TYPES.Auth_RoleService)
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.Auth_DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.Auth_DomainEventPublisher)
const getUserKeyParamsUseCase: GetUserKeyParams = container.get(TYPES.Auth_GetUserKeyParams)
Promise.resolve(
requestBackups(settingRepository, roleService, domainEventFactory, domainEventPublisher, getUserKeyParamsUseCase),
const triggerEmailBackupForAllUsers: TriggerEmailBackupForAllUsers = container.get(
TYPES.Auth_TriggerEmailBackupForAllUsers,
)
Promise.resolve(requestBackups(triggerEmailBackupForAllUsers))
.then(() => {
logger.info(`${backupFrequency} ${backupProvider} backup requesting complete`)
logger.info(`${backupFrequency} email backup requesting complete`)
process.exit(0)
})
.catch((error) => {
logger.error(`Could not finish ${backupFrequency} ${backupProvider} backup requesting: ${error.message}`)
logger.error(`Could not finish ${backupFrequency} email backup requesting: ${error.message}`)
process.exit(1)
})
+43
View File
@@ -0,0 +1,43 @@
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 { DeleteAccountsFromCSVFile } from '../src/Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile'
const inputArgs = process.argv.slice(2)
const fileName = inputArgs[0]
const mode = inputArgs[1]
const deleteAccounts = async (deleteAccountsFromCSVFile: DeleteAccountsFromCSVFile): Promise<void> => {
await deleteAccountsFromCSVFile.execute({
fileName,
dryRun: mode !== 'delete',
})
}
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 mass accounts deletion from CSV file')
const deleteAccountsFromCSVFile = container.get<DeleteAccountsFromCSVFile>(TYPES.Auth_DeleteAccountsFromCSVFile)
Promise.resolve(deleteAccounts(deleteAccountsFromCSVFile))
.then(() => {
logger.info('Accounts deleted.')
process.exit(0)
})
.catch((error) => {
logger.error(`Could not delete accounts: ${error.message}`)
process.exit(1)
})
})
+7 -2
View File
@@ -64,9 +64,14 @@ void container.load().then((container) => {
})
})
const serverInstance = server.build()
const serverInstance = server.build().listen(env.get('PORT'))
serverInstance.listen(env.get('PORT'))
process.on('SIGTERM', () => {
logger.info('SIGTERM signal received: closing HTTP server')
serverInstance.close(() => {
logger.info('HTTP server closed')
})
})
logger.info(`Server started on port ${process.env.PORT}`)
})
+6
View File
@@ -22,5 +22,11 @@ void container.load().then((container) => {
const subscriber = container.get<DomainEventSubscriberInterface>(TYPES.Auth_DomainEventSubscriber)
process.on('SIGTERM', () => {
logger.info('SIGTERM received. Stopping worker...')
subscriber.stop()
logger.info('Worker stopped.')
})
subscriber.start()
})
@@ -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/delete_accounts.js')))
Object.defineProperty(exports, '__esModule', { value: true })
exports.default = index
+12 -20
View File
@@ -6,53 +6,45 @@ COMMAND=$1 && shift 1
case "$COMMAND" in
'start-web' )
echo "[Docker] Starting Web..."
node docker/entrypoint-server.js
exec node docker/entrypoint-server.js
;;
'start-worker' )
echo "[Docker] Starting Worker..."
node docker/entrypoint-worker.js
exec node docker/entrypoint-worker.js
;;
'cleanup' )
echo "[Docker] Starting Cleanup..."
node docker/entrypoint-cleanup.js
exec node docker/entrypoint-cleanup.js
;;
'stats' )
echo "[Docker] Starting Persisting Stats..."
node docker/entrypoint-stats.js
exec node docker/entrypoint-stats.js
;;
'email-daily-backup' )
echo "[Docker] Starting Email Daily Backup..."
node docker/entrypoint-backup.js email daily
exec node docker/entrypoint-backup.js daily
;;
'email-weekly-backup' )
echo "[Docker] Starting Email Weekly Backup..."
node docker/entrypoint-backup.js email weekly
exec node docker/entrypoint-backup.js weekly
;;
'email-backup' )
echo "[Docker] Starting Email Backup For Single User..."
EMAIL=$1 && shift 1
node docker/entrypoint-user-email-backup.js $EMAIL
exec node docker/entrypoint-user-email-backup.js $EMAIL
;;
'dropbox-daily-backup' )
echo "[Docker] Starting Dropbox Daily Backup..."
node docker/entrypoint-backup.js dropbox daily
;;
'google-drive-daily-backup' )
echo "[Docker] Starting Google Drive Daily Backup..."
node docker/entrypoint-backup.js google_drive daily
;;
'one-drive-daily-backup' )
echo "[Docker] Starting One Drive Daily Backup..."
node docker/entrypoint-backup.js one_drive daily
'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
;;
* )
+4 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.165.4",
"version": "1.168.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -24,17 +24,15 @@
"worker": "yarn node dist/bin/worker.js",
"cleanup": "yarn node dist/bin/cleanup.js",
"stats": "yarn node dist/bin/stats.js",
"daily-backup:email": "yarn node dist/bin/backup.js email daily",
"daily-backup:email": "yarn node dist/bin/backup.js daily",
"user-email-backup": "yarn node dist/bin/user_email_backup.js",
"daily-backup:dropbox": "yarn node dist/bin/backup.js dropbox daily",
"daily-backup:google_drive": "yarn node dist/bin/backup.js google_drive daily",
"daily-backup:one_drive": "yarn node dist/bin/backup.js one_drive daily",
"weekly-backup:email": "yarn node dist/bin/backup.js email weekly",
"weekly-backup:email": "yarn node dist/bin/backup.js weekly",
"content-recalculation": "yarn node dist/bin/content.js",
"typeorm": "typeorm-ts-node-commonjs",
"migrate": "yarn build && yarn typeorm migration:run -d dist/src/Bootstrap/DataSource.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.445.0",
"@aws-sdk/client-sns": "^3.427.0",
"@aws-sdk/client-sqs": "^3.427.0",
"@cbor-extract/cbor-extract-linux-arm64": "^2.1.1",
+62 -12
View File
@@ -2,6 +2,7 @@ import * as winston from 'winston'
import Redis from 'ioredis'
import { SNSClient, SNSClientConfig } from '@aws-sdk/client-sns'
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'
import { S3Client } from '@aws-sdk/client-s3'
import { Container } from 'inversify'
import {
DomainEventHandlerInterface,
@@ -130,8 +131,6 @@ import { ListedAccountCreatedEventHandler } from '../Domain/Handler/ListedAccoun
import { ListedAccountDeletedEventHandler } from '../Domain/Handler/ListedAccountDeletedEventHandler'
import { FileRemovedEventHandler } from '../Domain/Handler/FileRemovedEventHandler'
import { UserDisabledSessionUserAgentLoggingEventHandler } from '../Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler'
import { SettingInterpreterInterface } from '../Domain/Setting/SettingInterpreterInterface'
import { SettingInterpreter } from '../Domain/Setting/SettingInterpreter'
import { SettingCrypterInterface } from '../Domain/Setting/SettingCrypterInterface'
import { SettingCrypter } from '../Domain/Setting/SettingCrypter'
import { SharedSubscriptionInvitationRepositoryInterface } from '../Domain/SharedSubscription/SharedSubscriptionInvitationRepositoryInterface'
@@ -275,6 +274,12 @@ import { SubscriptionSettingPersistenceMapper } from '../Mapping/Persistence/Sub
import { ApplyDefaultSettings } from '../Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings'
import { AuthResponseFactoryResolverInterface } from '../Domain/Auth/AuthResponseFactoryResolverInterface'
import { UserInvitedToSharedVaultEventHandler } from '../Domain/Handler/UserInvitedToSharedVaultEventHandler'
import { TriggerPostSettingUpdateActions } from '../Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions'
import { TriggerEmailBackupForUser } from '../Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUser'
import { TriggerEmailBackupForAllUsers } from '../Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers'
import { CSVFileReaderInterface } from '../Domain/CSV/CSVFileReaderInterface'
import { S3CsvFileReader } from '../Infra/S3/S3CsvFileReader'
import { DeleteAccountsFromCSVFile } from '../Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile'
export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {}
@@ -369,6 +374,19 @@ export class ContainerConfigLoader {
}
const sqsClient = new SQSClient(sqsConfig)
container.bind<SQSClient>(TYPES.Auth_SQS).toConstantValue(sqsClient)
container.bind<S3Client>(TYPES.Auth_S3).toConstantValue(
new S3Client({
apiVersion: 'latest',
region: env.get('S3_AWS_REGION', true),
}),
)
container
.bind<CSVFileReaderInterface>(TYPES.Auth_CSVFileReader)
.toConstantValue(
new S3CsvFileReader(env.get('S3_AUTH_SCRIPTS_DATA_BUCKET', true), container.get<S3Client>(TYPES.Auth_S3)),
)
}
container.bind(TYPES.Auth_SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN', true))
@@ -772,16 +790,6 @@ export class ContainerConfigLoader {
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<SettingInterpreterInterface>(TYPES.Auth_SettingInterpreter)
.toConstantValue(
new SettingInterpreter(
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
container.get<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams),
),
)
container.bind<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService).to(OfflineSettingService)
container.bind<ContentDecoderInterface>(TYPES.Auth_ContenDecoder).toConstantValue(new ContentDecoder())
@@ -1231,6 +1239,46 @@ export class ContainerConfigLoader {
container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
),
)
container
.bind<TriggerEmailBackupForUser>(TYPES.Auth_TriggerEmailBackupForUser)
.toConstantValue(
new TriggerEmailBackupForUser(
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams),
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
),
)
container
.bind<TriggerEmailBackupForAllUsers>(TYPES.Auth_TriggerEmailBackupForAllUsers)
.toConstantValue(
new TriggerEmailBackupForAllUsers(
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
container.get<TriggerEmailBackupForUser>(TYPES.Auth_TriggerEmailBackupForUser),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<TriggerPostSettingUpdateActions>(TYPES.Auth_TriggerPostSettingUpdateActions)
.toConstantValue(
new TriggerPostSettingUpdateActions(
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
container.get<TriggerEmailBackupForUser>(TYPES.Auth_TriggerEmailBackupForUser),
),
)
if (!isConfiguredForHomeServer) {
container
.bind<DeleteAccountsFromCSVFile>(TYPES.Auth_DeleteAccountsFromCSVFile)
.toConstantValue(
new DeleteAccountsFromCSVFile(
container.get<CSVFileReaderInterface>(TYPES.Auth_CSVFileReader),
container.get<DeleteAccount>(TYPES.Auth_DeleteAccount),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
}
// Controller
container
@@ -1655,11 +1703,13 @@ export class ContainerConfigLoader {
container.get<GetAllSettingsForUser>(TYPES.Auth_GetAllSettingsForUser),
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<TriggerPostSettingUpdateActions>(TYPES.Auth_TriggerPostSettingUpdateActions),
container.get<DeleteSetting>(TYPES.Auth_DeleteSetting),
container.get<MapperInterface<Setting, SettingHttpRepresentation>>(TYPES.Auth_SettingHttpMapper),
container.get<MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>>(
TYPES.Auth_SubscriptionSettingHttpMapper,
),
container.get<winston.Logger>(TYPES.Auth_Logger),
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
),
)
+6 -1
View File
@@ -3,6 +3,7 @@ const TYPES = {
Auth_Redis: Symbol.for('Auth_Redis'),
Auth_SNS: Symbol.for('Auth_SNS'),
Auth_SQS: Symbol.for('Auth_SQS'),
Auth_S3: Symbol.for('Auth_S3'),
// Mapping
Auth_SessionTracePersistenceMapper: Symbol.for('Auth_SessionTracePersistenceMapper'),
Auth_AuthenticatorChallengePersistenceMapper: Symbol.for('Auth_AuthenticatorChallengePersistenceMapper'),
@@ -164,6 +165,10 @@ const TYPES = {
Auth_DesignateSurvivor: Symbol.for('Auth_DesignateSurvivor'),
Auth_GetSharedOrRegularSubscriptionForUser: Symbol.for('Auth_GetSharedOrRegularSubscriptionForUser'),
Auth_DisableEmailSettingBasedOnEmailSubscription: Symbol.for('Auth_DisableEmailSettingBasedOnEmailSubscription'),
Auth_TriggerPostSettingUpdateActions: Symbol.for('Auth_TriggerPostSettingUpdateActions'),
Auth_TriggerEmailBackupForUser: Symbol.for('Auth_TriggerEmailBackupForUser'),
Auth_TriggerEmailBackupForAllUsers: Symbol.for('Auth_TriggerEmailBackupForAllUsers'),
Auth_DeleteAccountsFromCSVFile: Symbol.for('Auth_DeleteAccountsFromCSVFile'),
// Handlers
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
Auth_SubscriptionPurchasedEventHandler: Symbol.for('Auth_SubscriptionPurchasedEventHandler'),
@@ -230,7 +235,6 @@ const TYPES = {
Auth_SubscriptionSettingsAssociationService: Symbol.for('Auth_SubscriptionSettingsAssociationService'),
Auth_FeatureService: Symbol.for('Auth_FeatureService'),
Auth_SettingCrypter: Symbol.for('Auth_SettingCrypter'),
Auth_SettingInterpreter: Symbol.for('Auth_SettingInterpreter'),
Auth_ProtocolVersionSelector: Symbol.for('Auth_ProtocolVersionSelector'),
Auth_BooleanSelector: Symbol.for('Auth_BooleanSelector'),
Auth_BaseAuthController: Symbol.for('Auth_BaseAuthController'),
@@ -249,6 +253,7 @@ const TYPES = {
Auth_BaseOfflineController: Symbol.for('Auth_BaseOfflineController'),
Auth_BaseListedController: Symbol.for('Auth_BaseListedController'),
Auth_BaseFeaturesController: Symbol.for('Auth_BaseFeaturesController'),
Auth_CSVFileReader: Symbol.for('Auth_CSVFileReader'),
}
export default TYPES
@@ -0,0 +1,5 @@
import { Result } from '@standardnotes/domain-core'
export interface CSVFileReaderInterface {
getValues(fileName: string): Promise<Result<string[]>>
}
@@ -1,153 +0,0 @@
import {
DomainEventPublisherInterface,
EmailBackupRequestedEvent,
MuteEmailsSettingChangedEvent,
UserDisabledSessionUserAgentLoggingEvent,
} from '@standardnotes/domain-events'
import { EmailBackupFrequency, LogSessionUserAgentOption, MuteMarketingEmailsOption } from '@standardnotes/settings'
import 'reflect-metadata'
import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { User } from '../User/User'
import { Setting } from './Setting'
import { SettingCrypterInterface } from './SettingCrypterInterface'
import { SettingInterpreter } from './SettingInterpreter'
import { SettingRepositoryInterface } from './SettingRepositoryInterface'
import { GetUserKeyParams } from '../UseCase/GetUserKeyParams/GetUserKeyParams'
import { KeyParamsData } from '@standardnotes/responses'
import { Uuid, Timestamps, UniqueEntityId, SettingName } from '@standardnotes/domain-core'
describe('SettingInterpreter', () => {
let user: User
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let settingRepository: SettingRepositoryInterface
let settingCrypter: SettingCrypterInterface
let logger: Logger
let getUserKeyParams: GetUserKeyParams
const createInterpreter = () =>
new SettingInterpreter(domainEventPublisher, domainEventFactory, settingRepository, getUserKeyParams)
beforeEach(() => {
user = {
uuid: '4-5-6',
email: 'test@test.te',
} as jest.Mocked<User>
settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(null)
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
settingCrypter = {} as jest.Mocked<SettingCrypterInterface>
settingCrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted')
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createEmailBackupRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<EmailBackupRequestedEvent>)
domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<UserDisabledSessionUserAgentLoggingEvent>)
domainEventFactory.createMuteEmailsSettingChangedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<MuteEmailsSettingChangedEvent>)
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
logger.warn = jest.fn()
logger.error = jest.fn()
getUserKeyParams = {} as jest.Mocked<GetUserKeyParams>
getUserKeyParams.execute = jest.fn().mockReturnValue({ keyParams: {} as jest.Mocked<KeyParamsData> })
})
it('should trigger session cleanup if user is disabling session user agent logging', async () => {
await createInterpreter().interpretSettingUpdated(
SettingName.NAMES.LogSessionUserAgent,
user,
LogSessionUserAgentOption.Disabled,
)
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent).toHaveBeenCalledWith({
userUuid: '4-5-6',
email: 'test@test.te',
})
})
it('should trigger backup if email backup setting is created - emails not muted', async () => {
await createInterpreter().interpretSettingUpdated(
SettingName.NAMES.EmailBackupFrequency,
user,
EmailBackupFrequency.Daily,
)
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith('4-5-6', '', false, {})
})
it('should trigger backup if email backup setting is created - emails muted', async () => {
const setting = Setting.create(
{
name: SettingName.NAMES.MuteFailedBackupsEmails,
value: 'muted',
serverEncryptionVersion: 0,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sensitive: false,
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId('7fb54003-1dd2-40bd-8900-2bacd6cf629c'),
).getValue()
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(setting)
await createInterpreter().interpretSettingUpdated(
SettingName.NAMES.EmailBackupFrequency,
user,
EmailBackupFrequency.Daily,
)
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith(
'4-5-6',
'7fb54003-1dd2-40bd-8900-2bacd6cf629c',
true,
{},
)
})
it('should not trigger backup if email backup setting is disabled', async () => {
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
await createInterpreter().interpretSettingUpdated(
SettingName.NAMES.EmailBackupFrequency,
user,
EmailBackupFrequency.Disabled,
)
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createEmailBackupRequestedEvent).not.toHaveBeenCalled()
})
it('should trigger mute subscription emails rejection if mute setting changed', async () => {
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
await createInterpreter().interpretSettingUpdated(
SettingName.NAMES.MuteMarketingEmails,
user,
MuteMarketingEmailsOption.Muted,
)
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createMuteEmailsSettingChangedEvent).toHaveBeenCalledWith({
emailSubscriptionRejectionLevel: 'MARKETING',
mute: true,
username: 'test@test.te',
})
})
})
@@ -1,113 +0,0 @@
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { EmailLevel, SettingName } from '@standardnotes/domain-core'
import { EmailBackupFrequency, LogSessionUserAgentOption, MuteFailedBackupsEmailsOption } from '@standardnotes/settings'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { User } from '../User/User'
import { SettingInterpreterInterface } from './SettingInterpreterInterface'
import { SettingRepositoryInterface } from './SettingRepositoryInterface'
import { GetUserKeyParams } from '../UseCase/GetUserKeyParams/GetUserKeyParams'
export class SettingInterpreter implements SettingInterpreterInterface {
private readonly emailSettingToSubscriptionRejectionLevelMap: Map<string, string> = new Map([
[SettingName.NAMES.MuteFailedBackupsEmails, EmailLevel.LEVELS.FailedEmailBackup],
[SettingName.NAMES.MuteFailedCloudBackupsEmails, EmailLevel.LEVELS.FailedCloudBackup],
[SettingName.NAMES.MuteMarketingEmails, EmailLevel.LEVELS.Marketing],
[SettingName.NAMES.MuteSignInEmails, EmailLevel.LEVELS.SignIn],
])
constructor(
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
private settingRepository: SettingRepositoryInterface,
private getUserKeyParams: GetUserKeyParams,
) {}
async interpretSettingUpdated(
updatedSettingName: string,
user: User,
unencryptedValue: string | null,
): Promise<void> {
if (this.isChangingMuteEmailsSetting(updatedSettingName)) {
await this.triggerEmailSubscriptionChange(user, updatedSettingName, unencryptedValue)
}
if (this.isEnablingEmailBackupSetting(updatedSettingName, unencryptedValue)) {
await this.triggerEmailBackup(user.uuid)
}
if (this.isDisablingSessionUserAgentLogging(updatedSettingName, unencryptedValue)) {
await this.triggerSessionUserAgentCleanup(user)
}
}
private async triggerEmailBackup(userUuid: string): Promise<void> {
let userHasEmailsMuted = false
let muteEmailsSettingUuid = ''
const muteFailedEmailsBackupSetting = await this.settingRepository.findOneByNameAndUserUuid(
SettingName.NAMES.MuteFailedBackupsEmails,
userUuid,
)
if (muteFailedEmailsBackupSetting !== null) {
userHasEmailsMuted = muteFailedEmailsBackupSetting.props.value === MuteFailedBackupsEmailsOption.Muted
muteEmailsSettingUuid = muteFailedEmailsBackupSetting.id.toString()
}
const keyParamsResponse = await this.getUserKeyParams.execute({
authenticated: false,
userUuid,
})
await this.domainEventPublisher.publish(
this.domainEventFactory.createEmailBackupRequestedEvent(
userUuid,
muteEmailsSettingUuid,
userHasEmailsMuted,
keyParamsResponse.keyParams,
),
)
}
private isChangingMuteEmailsSetting(settingName: string): boolean {
return [
SettingName.NAMES.MuteFailedBackupsEmails,
SettingName.NAMES.MuteFailedCloudBackupsEmails,
SettingName.NAMES.MuteMarketingEmails,
SettingName.NAMES.MuteSignInEmails,
].includes(settingName)
}
private isEnablingEmailBackupSetting(settingName: string, newValue: string | null): boolean {
return (
settingName === SettingName.NAMES.EmailBackupFrequency &&
[EmailBackupFrequency.Daily, EmailBackupFrequency.Weekly].includes(newValue as EmailBackupFrequency)
)
}
private isDisablingSessionUserAgentLogging(settingName: string, newValue: string | null): boolean {
return SettingName.NAMES.LogSessionUserAgent === settingName && LogSessionUserAgentOption.Disabled === newValue
}
private async triggerEmailSubscriptionChange(
user: User,
settingName: string,
unencryptedValue: string | null,
): Promise<void> {
await this.domainEventPublisher.publish(
this.domainEventFactory.createMuteEmailsSettingChangedEvent({
username: user.email,
mute: unencryptedValue === 'muted',
emailSubscriptionRejectionLevel: this.emailSettingToSubscriptionRejectionLevelMap.get(settingName) as string,
}),
)
}
private async triggerSessionUserAgentCleanup(user: User) {
await this.domainEventPublisher.publish(
this.domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent({
userUuid: user.uuid,
email: user.email,
}),
)
}
}
@@ -1,5 +0,0 @@
import { User } from '../User/User'
export interface SettingInterpreterInterface {
interpretSettingUpdated(updatedSettingName: string, user: User, newUnencryptedValue: string | null): Promise<void>
}
@@ -1,4 +1,3 @@
import { ReadStream } from 'fs'
import { SettingName } from '@standardnotes/domain-core'
import { DeleteSettingDto } from '../UseCase/DeleteSetting/DeleteSettingDto'
@@ -10,8 +9,8 @@ export interface SettingRepositoryInterface {
findOneByNameAndUserUuid(name: string, userUuid: string): Promise<Setting | null>
findLastByNameAndUserUuid(name: string, userUuid: string): Promise<Setting | null>
findAllByUserUuid(userUuid: string): Promise<Setting[]>
streamAllByNameAndValue(name: SettingName, value: string): Promise<ReadStream>
streamAllByName(name: SettingName): Promise<ReadStream>
countAllByNameAndValue(dto: { name: SettingName; value: string }): Promise<number>
findAllByNameAndValue(dto: { name: SettingName; value: string; offset: number; limit: number }): Promise<Setting[]>
deleteByUserUuid(dto: DeleteSettingDto): Promise<void>
insert(setting: Setting): Promise<void>
update(setting: Setting): Promise<void>
@@ -0,0 +1,72 @@
import { Logger } from 'winston'
import { Result } from '@standardnotes/domain-core'
import { CSVFileReaderInterface } from '../../CSV/CSVFileReaderInterface'
import { DeleteAccount } from '../DeleteAccount/DeleteAccount'
import { DeleteAccountsFromCSVFile } from './DeleteAccountsFromCSVFile'
describe('DeleteAccountsFromCSVFile', () => {
let csvFileReader: CSVFileReaderInterface
let deleteAccount: DeleteAccount
let logger: Logger
const createUseCase = () => new DeleteAccountsFromCSVFile(csvFileReader, deleteAccount, logger)
beforeEach(() => {
csvFileReader = {} as jest.Mocked<CSVFileReaderInterface>
csvFileReader.getValues = jest.fn().mockResolvedValue(Result.ok(['email1']))
deleteAccount = {} as jest.Mocked<DeleteAccount>
deleteAccount.execute = jest.fn().mockResolvedValue(Result.ok(''))
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
})
it('should delete accounts', async () => {
const useCase = createUseCase()
const result = await useCase.execute({ fileName: 'test.csv', dryRun: false })
expect(result.isFailed()).toBeFalsy()
})
it('should return error if csv file is invalid', async () => {
csvFileReader.getValues = jest.fn().mockResolvedValue(Result.fail('Oops'))
const useCase = createUseCase()
const result = await useCase.execute({ fileName: 'test.csv', dryRun: false })
expect(result.isFailed()).toBeTruthy()
})
it('should return error if csv file is empty', async () => {
csvFileReader.getValues = jest.fn().mockResolvedValue(Result.ok([]))
const useCase = createUseCase()
const result = await useCase.execute({ fileName: 'test.csv', dryRun: false })
expect(result.isFailed()).toBeTruthy()
})
it('should do nothing on a dry run', async () => {
const useCase = createUseCase()
const result = await useCase.execute({ fileName: 'test.csv', dryRun: true })
expect(deleteAccount.execute).not.toHaveBeenCalled()
expect(result.isFailed()).toBeFalsy()
})
it('should return error if delete account fails', async () => {
deleteAccount.execute = jest.fn().mockResolvedValue(Result.fail('Oops'))
const useCase = createUseCase()
const result = await useCase.execute({ fileName: 'test.csv', dryRun: false })
expect(result.isFailed()).toBeTruthy()
})
})
@@ -0,0 +1,47 @@
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { DeleteAccount } from '../DeleteAccount/DeleteAccount'
import { CSVFileReaderInterface } from '../../CSV/CSVFileReaderInterface'
import { DeleteAccountsFromCSVFileDTO } from './DeleteAccountsFromCSVFileDTO'
export class DeleteAccountsFromCSVFile implements UseCaseInterface<void> {
constructor(
private csvFileReader: CSVFileReaderInterface,
private deleteAccount: DeleteAccount,
private logger: Logger,
) {}
async execute(dto: DeleteAccountsFromCSVFileDTO): Promise<Result<void>> {
const emailsOrError = await this.csvFileReader.getValues(dto.fileName)
if (emailsOrError.isFailed()) {
return Result.fail(emailsOrError.getError())
}
const emails = emailsOrError.getValue()
if (emails.length === 0) {
return Result.fail(`No emails found in CSV file ${dto.fileName}`)
}
if (dto.dryRun) {
const firstTenEmails = emails.slice(0, 10)
this.logger.info(
`Dry run mode enabled. Would delete ${emails.length} accounts. First 10 emails: ${firstTenEmails}`,
)
return Result.ok()
}
for (const email of emails) {
const deleteAccountOrError = await this.deleteAccount.execute({
username: email,
})
if (deleteAccountOrError.isFailed()) {
return Result.fail(deleteAccountOrError.getError())
}
}
return Result.ok()
}
}
@@ -0,0 +1,4 @@
export interface DeleteAccountsFromCSVFileDTO {
fileName: string
dryRun: boolean
}
@@ -0,0 +1,47 @@
import { Logger } from 'winston'
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
import { TriggerEmailBackupForUser } from '../TriggerEmailBackupForUser/TriggerEmailBackupForUser'
import { TriggerEmailBackupForAllUsers } from './TriggerEmailBackupForAllUsers'
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
import { Setting } from '../../Setting/Setting'
import { Result, SettingName, Timestamps, Uuid } from '@standardnotes/domain-core'
describe('TriggerEmailBackupForAllUsers', () => {
let settingRepository: SettingRepositoryInterface
let triggerEmailBackupForUserUseCase: TriggerEmailBackupForUser
let logger: Logger
const createUseCase = () =>
new TriggerEmailBackupForAllUsers(settingRepository, triggerEmailBackupForUserUseCase, logger)
beforeEach(() => {
const setting = Setting.create({
name: SettingName.NAMES.EmailBackupFrequency,
value: null,
serverEncryptionVersion: EncryptionVersion.Default,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sensitive: false,
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
settingRepository.countAllByNameAndValue = jest.fn().mockResolvedValue(1)
settingRepository.findAllByNameAndValue = jest.fn().mockResolvedValue([setting])
triggerEmailBackupForUserUseCase = {} as jest.Mocked<TriggerEmailBackupForUser>
triggerEmailBackupForUserUseCase.execute = jest.fn().mockResolvedValue(Result.ok())
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
logger.info = jest.fn()
})
it('triggers email backup for all users', async () => {
const useCase = createUseCase()
const result = await useCase.execute({ backupFrequency: 'daily' })
expect(result.isFailed()).toBeFalsy()
})
})
@@ -0,0 +1,55 @@
import { Result, SettingName, UseCaseInterface } from '@standardnotes/domain-core'
import { TriggerEmailBackupForUser } from '../TriggerEmailBackupForUser/TriggerEmailBackupForUser'
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
import { TriggerEmailBackupForAllUsersDTO } from './TriggerEmailBackupForAllUsersDTO'
import { Logger } from 'winston'
export class TriggerEmailBackupForAllUsers implements UseCaseInterface<void> {
private PAGING_LIMIT = 100
constructor(
private settingRepository: SettingRepositoryInterface,
private triggerEmailBackupForUserUseCase: TriggerEmailBackupForUser,
private logger: Logger,
) {}
async execute(dto: TriggerEmailBackupForAllUsersDTO): Promise<Result<void>> {
const emailBackupFrequencySettingName = SettingName.create(SettingName.NAMES.EmailBackupFrequency).getValue()
const allSettingsCount = await this.settingRepository.countAllByNameAndValue({
name: emailBackupFrequencySettingName,
value: dto.backupFrequency,
})
this.logger.info(`Found ${allSettingsCount} users with email backup frequency set to ${dto.backupFrequency}`)
let failedUsers = 0
const numberOfPages = Math.ceil(allSettingsCount / this.PAGING_LIMIT)
for (let i = 0; i < numberOfPages; i++) {
const settings = await this.settingRepository.findAllByNameAndValue({
name: emailBackupFrequencySettingName,
value: dto.backupFrequency,
offset: i * this.PAGING_LIMIT,
limit: this.PAGING_LIMIT,
})
for (const setting of settings) {
const result = await this.triggerEmailBackupForUserUseCase.execute({
userUuid: setting.props.userUuid.value,
})
/* istanbul ignore next */
if (result.isFailed()) {
this.logger.error(`Failed to trigger email backup for user ${setting.props.userUuid.value}`)
failedUsers++
}
}
}
/* istanbul ignore next */
if (failedUsers > 0) {
this.logger.error(`Failed to trigger email backup for ${failedUsers} users`)
}
return Result.ok()
}
}
@@ -0,0 +1,3 @@
export interface TriggerEmailBackupForAllUsersDTO {
backupFrequency: string
}

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