mirror of
https://github.com/standardnotes/server
synced 2026-01-22 02:04:30 -05:00
Compare commits
37 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28b04e6a4a | ||
|
|
d228a86f48 | ||
|
|
0cb234aa47 | ||
|
|
6b554c28b7 | ||
|
|
8a0accd8ea | ||
|
|
d66ae62cf4 | ||
|
|
b01d1c659d | ||
|
|
751f3b2547 | ||
|
|
11514e3836 | ||
|
|
71689c1497 | ||
|
|
2742075edc | ||
|
|
7f16232f8b | ||
|
|
0b0703e6d1 | ||
|
|
3e376c44e3 | ||
|
|
bfe2d4bb4a | ||
|
|
7253a0a1d9 | ||
|
|
f2c5810023 | ||
|
|
2e5b9105b8 | ||
|
|
d14411d72e | ||
|
|
5226513b26 | ||
|
|
334449f8aa | ||
|
|
7f43d0c69d | ||
|
|
6f18276e7a | ||
|
|
9ff18a18a5 | ||
|
|
999e72fb1f | ||
|
|
4733e663a3 | ||
|
|
b48eeb16c3 | ||
|
|
0aa2584e82 | ||
|
|
eb8c704d84 | ||
|
|
e93fa14703 | ||
|
|
16a6815b69 | ||
|
|
b08e9731b8 | ||
|
|
9bd4fb2d79 | ||
|
|
647aeda1de | ||
|
|
78ff748d91 | ||
|
|
31f8cf1169 | ||
|
|
14bcf7b6c9 |
2
.github/ci.env
vendored
2
.github/ci.env
vendored
@@ -26,3 +26,5 @@ MYSQL_ROOT_PASSWORD=changeme123
|
||||
AUTH_JWT_SECRET=f95259c5e441f5a4646d76422cfb3df4c4488842901aa50b6c51b8be2e0040e9
|
||||
AUTH_SERVER_ENCRYPTION_SERVER_KEY=1087415dfde3093797f9a7ca93a49e7d7aa1861735eb0d32aae9c303b8c3d060
|
||||
VALET_TOKEN_SECRET=4b886819ebe1e908077c6cae96311b48a8416bd60cc91c03060e15bdf6b30d1f
|
||||
|
||||
SYNCING_SERVER_CONTENT_SIZE_TRANSFER_LIMIT=1000000
|
||||
|
||||
4
.github/workflows/common-docker-image.yml
vendored
4
.github/workflows/common-docker-image.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
run: yarn build
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v1
|
||||
uses: aws-actions/amazon-ecr-login@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@master
|
||||
|
||||
2
.github/workflows/common-self-hosting.yml
vendored
2
.github/workflows/common-self-hosting.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
1
.github/workflows/e2e-home-server.yml
vendored
1
.github/workflows/e2e-home-server.yml
vendored
@@ -70,6 +70,7 @@ jobs:
|
||||
echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env
|
||||
echo "REFRESH_TOKEN_AGE=10" >> packages/home-server/.env
|
||||
echo "REVISIONS_FREQUENCY=2" >> packages/home-server/.env
|
||||
echo "CONTENT_SIZE_TRANSFER_LIMIT=1000000" >> packages/home-server/.env
|
||||
echo "DB_HOST=localhost" >> packages/home-server/.env
|
||||
echo "DB_PORT=3306" >> packages/home-server/.env
|
||||
echo "DB_DATABASE=standardnotes" >> packages/home-server/.env
|
||||
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -143,7 +143,7 @@ jobs:
|
||||
git config --global user.email "ci@standardnotes.com"
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v5
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.PASSPHRASE }}
|
||||
|
||||
12
.pnp.cjs
generated
12
.pnp.cjs
generated
@@ -5566,7 +5566,7 @@ const RAW_RUNTIME_STATE =
|
||||
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
|
||||
["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.17"],\
|
||||
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
|
||||
["ua-parser-js", "npm:1.0.35"],\
|
||||
["ua-parser-js", "npm:1.0.37"],\
|
||||
["uuid", "npm:9.0.0"],\
|
||||
["winston", "npm:3.9.0"]\
|
||||
],\
|
||||
@@ -5785,7 +5785,6 @@ const RAW_RUNTIME_STATE =
|
||||
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
|
||||
["@types/cors", "npm:2.8.13"],\
|
||||
["@types/express", "npm:4.17.17"],\
|
||||
["@types/prettyjson", "npm:0.0.30"],\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
|
||||
["@typescript-eslint/parser", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
|
||||
["cors", "npm:2.8.5"],\
|
||||
@@ -5798,7 +5797,6 @@ const RAW_RUNTIME_STATE =
|
||||
["inversify", "npm:6.0.1"],\
|
||||
["inversify-express-utils", "npm:6.4.3"],\
|
||||
["prettier", "npm:3.0.3"],\
|
||||
["prettyjson", "npm:1.2.5"],\
|
||||
["reflect-metadata", "npm:0.1.13"],\
|
||||
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
|
||||
["winston", "npm:3.9.0"]\
|
||||
@@ -6080,7 +6078,7 @@ const RAW_RUNTIME_STATE =
|
||||
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
|
||||
["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.17"],\
|
||||
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
|
||||
["ua-parser-js", "npm:1.0.35"],\
|
||||
["ua-parser-js", "npm:1.0.37"],\
|
||||
["uuid", "npm:9.0.0"],\
|
||||
["winston", "npm:3.9.0"]\
|
||||
],\
|
||||
@@ -16149,10 +16147,10 @@ const RAW_RUNTIME_STATE =
|
||||
}]\
|
||||
]],\
|
||||
["ua-parser-js", [\
|
||||
["npm:1.0.35", {\
|
||||
"packageLocation": "./.yarn/cache/ua-parser-js-npm-1.0.35-38ecdb7612-b69c99c20f.zip/node_modules/ua-parser-js/",\
|
||||
["npm:1.0.37", {\
|
||||
"packageLocation": "./.yarn/cache/ua-parser-js-npm-1.0.37-b79655e1b5-56508f2428.zip/node_modules/ua-parser-js/",\
|
||||
"packageDependencies": [\
|
||||
["ua-parser-js", "npm:1.0.35"]\
|
||||
["ua-parser-js", "npm:1.0.37"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
|
||||
Binary file not shown.
BIN
.yarn/cache/ua-parser-js-npm-1.0.37-b79655e1b5-56508f2428.zip
vendored
Normal file
BIN
.yarn/cache/ua-parser-js-npm-1.0.37-b79655e1b5-56508f2428.zip
vendored
Normal file
Binary file not shown.
@@ -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.
|
||||
|
||||
## [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
|
||||
|
||||
## [2.32.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.32.4...@standardnotes/analytics@2.32.5) (2023-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove open telemetry from code ([#903](https://github.com/standardnotes/server/issues/903)) ([751f3b2](https://github.com/standardnotes/server/commit/751f3b25476c5be3d663ad8540c43266acd39493))
|
||||
|
||||
## [2.32.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.32.3...@standardnotes/analytics@2.32.4) (2023-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { OpenTelemetrySDK, OpenTelemetryTracer } from '@standardnotes/domain-events-infra'
|
||||
import { EmailLevel, ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
|
||||
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AnalyticsScheduledTask })
|
||||
sdk.start()
|
||||
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
@@ -22,6 +16,7 @@ import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/Calculat
|
||||
import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { StatisticMeasureName } from '../src/Domain/Statistics/StatisticMeasureName'
|
||||
import { EmailLevel } from '@standardnotes/domain-core'
|
||||
|
||||
const requestReport = async (
|
||||
analyticsStore: AnalyticsStoreInterface,
|
||||
@@ -275,9 +270,6 @@ void container.load().then((container) => {
|
||||
|
||||
logger.info(`Sending report to following admins: ${adminEmails}`)
|
||||
|
||||
const tracer = new OpenTelemetryTracer()
|
||||
tracer.startSpan(ServiceIdentifier.NAMES.AnalyticsScheduledTask, 'report')
|
||||
|
||||
Promise.resolve(
|
||||
requestReport(
|
||||
analyticsStore,
|
||||
@@ -293,15 +285,11 @@ void container.load().then((container) => {
|
||||
.then(() => {
|
||||
logger.info('Usage report generation complete')
|
||||
|
||||
tracer.stopSpan()
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`Could not finish usage report generation: ${error.message}`)
|
||||
|
||||
tracer.stopSpanWithError(error)
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { OpenTelemetrySDK } from '@standardnotes/domain-events-infra'
|
||||
import { ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
|
||||
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AnalyticsWorker })
|
||||
sdk.start()
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventSubscriberInterface } from '@standardnotes/domain-events'
|
||||
import * as dayjs from 'dayjs'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.32.4",
|
||||
"version": "2.32.6",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
DomainEventPublisherInterface,
|
||||
DomainEventSubscriberInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { MapperInterface, ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Mixpanel = require('mixpanel')
|
||||
|
||||
@@ -16,9 +16,9 @@ import TYPES from './Types'
|
||||
import { AppDataSource } from './DataSource'
|
||||
import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
|
||||
import {
|
||||
SNSOpenTelemetryDomainEventPublisher,
|
||||
SNSDomainEventPublisher,
|
||||
SQSDomainEventSubscriber,
|
||||
SQSEventMessageHandler,
|
||||
SQSOpenTelemetryDomainEventSubscriber,
|
||||
} from '@standardnotes/domain-events-infra'
|
||||
import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
import { PeriodKeyGeneratorInterface } from '../Domain/Time/PeriodKeyGeneratorInterface'
|
||||
@@ -139,9 +139,7 @@ export class ContainerConfigLoader {
|
||||
|
||||
container
|
||||
.bind<DomainEventPublisherInterface>(TYPES.DomainEventPublisher)
|
||||
.toConstantValue(
|
||||
new SNSOpenTelemetryDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)),
|
||||
)
|
||||
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
|
||||
if (env.get('MIXPANEL_TOKEN', true)) {
|
||||
container.bind<Mixpanel>(TYPES.MixpanelClient).toConstantValue(Mixpanel.init(env.get('MIXPANEL_TOKEN', true)))
|
||||
}
|
||||
@@ -242,8 +240,7 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<DomainEventSubscriberInterface>(TYPES.DomainEventSubscriber)
|
||||
.toConstantValue(
|
||||
new SQSOpenTelemetryDomainEventSubscriber(
|
||||
ServiceIdentifier.NAMES.AnalyticsWorker,
|
||||
new SQSDomainEventSubscriber(
|
||||
container.get<SQSClient>(TYPES.SQS),
|
||||
container.get<string>(TYPES.SQS_QUEUE_URL),
|
||||
container.get<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler),
|
||||
|
||||
@@ -3,6 +3,40 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
## [1.81.9](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.8...@standardnotes/api-gateway@1.81.9) (2023-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove open telemetry from code ([#903](https://github.com/standardnotes/api-gateway/issues/903)) ([751f3b2](https://github.com/standardnotes/api-gateway/commit/751f3b25476c5be3d663ad8540c43266acd39493))
|
||||
|
||||
## [1.81.8](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.7...@standardnotes/api-gateway@1.81.8) (2023-11-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* retry attempts on session validation and more verbose logs ([#898](https://github.com/standardnotes/api-gateway/issues/898)) ([3e376c4](https://github.com/standardnotes/api-gateway/commit/3e376c44e3a6c336dcff3d8ef5eb3ab040d9a561))
|
||||
|
||||
## [1.81.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.6...@standardnotes/api-gateway@1.81.7) (2023-10-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add fallback methods for 404 requests ([#893](https://github.com/standardnotes/api-gateway/issues/893)) ([16a6815](https://github.com/standardnotes/api-gateway/commit/16a6815b69e344573ae07682f3bac1d44d715d79))
|
||||
|
||||
## [1.81.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.5...@standardnotes/api-gateway@1.81.6) (2023-10-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** logs for errors reaching service ([14bcf7b](https://github.com/standardnotes/api-gateway/commit/14bcf7b6c9403c3413e7579f58ea17168d14dce7))
|
||||
|
||||
## [1.81.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.4...@standardnotes/api-gateway@1.81.5) (2023-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { OpenTelemetrySDK } from '@standardnotes/domain-events-infra'
|
||||
import { ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
|
||||
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.ApiGateway })
|
||||
sdk.start()
|
||||
|
||||
import '../src/Controller/LegacyController'
|
||||
import '../src/Controller/HealthCheckController'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.81.5",
|
||||
"version": "1.81.11",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -74,13 +74,16 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
|
||||
response.locals.belongsToSharedVaults = decodedToken.belongs_to_shared_vaults ?? []
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error).message
|
||||
let detailedErrorMessage = (error as Error).message
|
||||
if (error instanceof AxiosError) {
|
||||
detailedErrorMessage = `Status: ${error.status}, code: ${error.code}, message: ${error.message}`
|
||||
}
|
||||
|
||||
this.logger.error(`Could not pass the request to sessions/validate on underlying service: ${errorMessage}`)
|
||||
this.logger.error(
|
||||
`Could not pass the request to sessions/validate on underlying service: ${detailedErrorMessage}`,
|
||||
)
|
||||
|
||||
this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
|
||||
this.logger.debug(`Response error: ${JSON.stringify(error)}`)
|
||||
|
||||
if ((error as AxiosError).response?.headers['content-type']) {
|
||||
response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
|
||||
@@ -91,7 +94,14 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
? +((error as AxiosError).code as string)
|
||||
: 500
|
||||
|
||||
response.status(errorCode).send(errorMessage)
|
||||
const responseErrorMessage = (error as AxiosError).response?.data
|
||||
|
||||
response
|
||||
.status(errorCode)
|
||||
.send(
|
||||
responseErrorMessage ??
|
||||
"Unfortunately, we couldn't handle your request. Please try again or contact our support if the error persists.",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { BaseHttpController, all, controller, results } from 'inversify-express-utils'
|
||||
|
||||
@controller('')
|
||||
export class FallbackController extends BaseHttpController {
|
||||
@all('*')
|
||||
public async fallback(): Promise<results.NotFoundResult> {
|
||||
return this.notFound()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './AuthMiddleware'
|
||||
export * from './FallbackController'
|
||||
export * from './HealthCheckController'
|
||||
export * from './SubscriptionTokenAuthMiddleware'
|
||||
export * from './TokenAuthenticationMethod'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -55,10 +55,9 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
const requestTimedOut =
|
||||
'code' in (error as Record<string, unknown>) && (error as Record<string, unknown>).code === 'ETIMEDOUT'
|
||||
const requestDidNotMakeIt = this.requestTimedOutOrDidNotReachDestination(error as Record<string, unknown>)
|
||||
const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
|
||||
if (!tooManyRetryAttempts && requestTimedOut) {
|
||||
if (!tooManyRetryAttempts && requestDidNotMakeIt) {
|
||||
await this.timer.sleep(50)
|
||||
|
||||
const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
|
||||
@@ -152,7 +151,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.')
|
||||
@@ -160,18 +158,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(
|
||||
@@ -261,14 +254,15 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error).message
|
||||
let detailedErrorMessage = (error as Error).message
|
||||
if (error instanceof AxiosError) {
|
||||
detailedErrorMessage = `Status: ${error.status}, code: ${error.code}, message: ${error.message}`
|
||||
}
|
||||
|
||||
this.logger.error(
|
||||
tooManyRetryAttempts
|
||||
? `Request to ${serverUrl}/${endpointOrMethodIdentifier} timed out after ${retryAttempt} retries`
|
||||
: `Could not pass the request to ${serverUrl}/${endpointOrMethodIdentifier} on underlying service: ${errorMessage}`,
|
||||
: `Could not pass the request to ${serverUrl}/${endpointOrMethodIdentifier} on underlying service: ${detailedErrorMessage}`,
|
||||
)
|
||||
|
||||
this.logger.debug(`Response error: ${JSON.stringify(error)}`)
|
||||
@@ -282,7 +276,14 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
? +((error as AxiosError).code as string)
|
||||
: 500
|
||||
|
||||
response.status(errorCode).send(errorMessage)
|
||||
const responseErrorMessage = (error as AxiosError).response?.data
|
||||
|
||||
response
|
||||
.status(errorCode)
|
||||
.send(
|
||||
responseErrorMessage ??
|
||||
"Unfortunately, we couldn't handle your request. Please try again or contact our support if the error persists.",
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -338,7 +339,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,
|
||||
@@ -357,18 +357,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,
|
||||
@@ -50,7 +49,13 @@ export interface ServiceProxyInterface {
|
||||
endpointOrMethodIdentifier: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void>
|
||||
validateSession(headers: { authorization: string; sharedVaultOwnerContext?: string }): Promise<{
|
||||
validateSession(
|
||||
headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
},
|
||||
retryAttempt?: number,
|
||||
): Promise<{
|
||||
status: number
|
||||
data: unknown
|
||||
headers: {
|
||||
|
||||
@@ -9,10 +9,13 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
private filesServerUrl: string,
|
||||
) {}
|
||||
|
||||
async validateSession(headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
async validateSession(
|
||||
headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
},
|
||||
_retryAttempt?: number,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
const authService = this.serviceContainer.get(ServiceIdentifier.create(ServiceIdentifier.NAMES.Auth).getValue())
|
||||
if (!authService) {
|
||||
throw new Error('Auth service not found')
|
||||
|
||||
@@ -3,6 +3,70 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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
|
||||
|
||||
* account deletion event ([#904](https://github.com/standardnotes/server/issues/904)) ([d66ae62](https://github.com/standardnotes/server/commit/d66ae62cf4f413cac5f6f4eac45dc0f1ddbc9e32))
|
||||
|
||||
## [1.165.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.165.2...@standardnotes/auth-server@1.165.3) (2023-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove open telemetry from code ([#903](https://github.com/standardnotes/server/issues/903)) ([751f3b2](https://github.com/standardnotes/server/commit/751f3b25476c5be3d663ad8540c43266acd39493))
|
||||
|
||||
## [1.165.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.165.1...@standardnotes/auth-server@1.165.2) (2023-11-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** change log severity on user authentication ([7f16232](https://github.com/standardnotes/server/commit/7f16232f8b13e3736801b6dc0af799e0559a3cfa))
|
||||
|
||||
## [1.165.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.165.0...@standardnotes/auth-server@1.165.1) (2023-11-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* retry attempts on session validation and more verbose logs ([#898](https://github.com/standardnotes/server/issues/898)) ([3e376c4](https://github.com/standardnotes/server/commit/3e376c44e3a6c336dcff3d8ef5eb3ab040d9a561))
|
||||
|
||||
# [1.165.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.164.2...@standardnotes/auth-server@1.165.0) (2023-11-02)
|
||||
|
||||
### Features
|
||||
|
||||
* add shared vault invitation email notifications ([#897](https://github.com/standardnotes/server/issues/897)) ([7253a0a](https://github.com/standardnotes/server/commit/7253a0a1d92099df844c9baf6541b440bbcb0a68))
|
||||
|
||||
## [1.164.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.164.1...@standardnotes/auth-server@1.164.2) (2023-11-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.164.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.164.0...@standardnotes/auth-server@1.164.1) (2023-11-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** creating valet tokens for shared subscription users ([#895](https://github.com/standardnotes/server/issues/895)) ([b48eeb1](https://github.com/standardnotes/server/commit/b48eeb16c32031e73e9757e34c4b50ca0a3a773d))
|
||||
|
||||
# [1.164.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.163.2...@standardnotes/auth-server@1.164.0) (2023-11-01)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add sending email to old email address when the address is changed ([#894](https://github.com/standardnotes/server/issues/894)) ([eb8c704](https://github.com/standardnotes/server/commit/eb8c704d84277130dc0dc51c1fe475a7220612cd))
|
||||
|
||||
## [1.163.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.163.1...@standardnotes/auth-server@1.163.2) (2023-10-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** checking permissions to update setting only when directly performed by user ([#892](https://github.com/standardnotes/server/issues/892)) ([9bd4fb2](https://github.com/standardnotes/server/commit/9bd4fb2d794dae032286c68f23d3896b68735bdd))
|
||||
|
||||
## [1.163.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.163.0...@standardnotes/auth-server@1.163.1) (2023-10-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** add more information on the listed creation error ([78ff748](https://github.com/standardnotes/server/commit/78ff748d911a5a4063903847ef761822bbb8f4e2))
|
||||
|
||||
# [1.163.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.162.0...@standardnotes/auth-server@1.163.0) (2023-10-26)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { OpenTelemetrySDK, OpenTelemetryTracer } from '@standardnotes/domain-events-infra'
|
||||
import { ServiceIdentifier, SettingName } from '@standardnotes/domain-core'
|
||||
|
||||
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AuthScheduledTask })
|
||||
sdk.start()
|
||||
|
||||
import { Stream } from 'stream'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as utc from 'dayjs/plugin/utc'
|
||||
@@ -15,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')
|
||||
@@ -98,31 +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)
|
||||
|
||||
const tracer = new OpenTelemetryTracer()
|
||||
tracer.startSpan(ServiceIdentifier.NAMES.AuthScheduledTask, 'backup')
|
||||
|
||||
Promise.resolve(
|
||||
requestBackups(settingRepository, roleService, domainEventFactory, domainEventPublisher, getUserKeyParamsUseCase),
|
||||
const triggerEmailBackupForAllUsers: TriggerEmailBackupForAllUsers = container.get(
|
||||
TYPES.Auth_TriggerEmailBackupForAllUsers,
|
||||
)
|
||||
.then(() => {
|
||||
logger.info(`${backupFrequency} ${backupProvider} backup requesting complete`)
|
||||
|
||||
tracer.stopSpan()
|
||||
Promise.resolve(requestBackups(triggerEmailBackupForAllUsers))
|
||||
.then(() => {
|
||||
logger.info(`${backupFrequency} email backup requesting complete`)
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`Could not finish ${backupFrequency} ${backupProvider} backup requesting: ${error.message}`)
|
||||
|
||||
tracer.stopSpanWithError(error)
|
||||
logger.error(`Could not finish ${backupFrequency} email backup requesting: ${error.message}`)
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { OpenTelemetrySDK, OpenTelemetryTracer } from '@standardnotes/domain-events-infra'
|
||||
import { ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
|
||||
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AuthScheduledTask })
|
||||
sdk.start()
|
||||
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
@@ -36,22 +30,15 @@ void container.load().then((container) => {
|
||||
const cleanupSessionTraces: CleanupSessionTraces = container.get(TYPES.Auth_CleanupSessionTraces)
|
||||
const cleanupExpiredSessions: CleanupExpiredSessions = container.get(TYPES.Auth_CleanupExpiredSessions)
|
||||
|
||||
const tracer = new OpenTelemetryTracer()
|
||||
tracer.startSpan(ServiceIdentifier.NAMES.AuthScheduledTask, 'cleanup')
|
||||
|
||||
Promise.resolve(cleanup(cleanupSessionTraces, cleanupExpiredSessions))
|
||||
.then(() => {
|
||||
logger.info('Expired sessions and session traces cleaned.')
|
||||
|
||||
tracer.stopSpan()
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`Could not clean sessions and session traces: ${error.message}`)
|
||||
|
||||
tracer.stopSpanWithError(error)
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { OpenTelemetrySDK } from '@standardnotes/domain-events-infra'
|
||||
import { ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
|
||||
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.Auth })
|
||||
sdk.start()
|
||||
|
||||
import '../src/Infra/InversifyExpressUtils/AnnotatedAuthController'
|
||||
import '../src/Infra/InversifyExpressUtils/AnnotatedAuthenticatorsController'
|
||||
import '../src/Infra/InversifyExpressUtils/AnnotatedSessionsController'
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { OpenTelemetrySDK, OpenTelemetryTracer } from '@standardnotes/domain-events-infra'
|
||||
import { ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
|
||||
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AuthScheduledTask })
|
||||
sdk.start()
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
@@ -26,9 +20,6 @@ void container.load().then((container) => {
|
||||
const persistStats: PersistStatistics = container.get(TYPES.Auth_PersistStatistics)
|
||||
const timer: TimerInterface = container.get(TYPES.Auth_Timer)
|
||||
|
||||
const tracer = new OpenTelemetryTracer()
|
||||
tracer.startSpan(ServiceIdentifier.NAMES.AuthScheduledTask, 'stats')
|
||||
|
||||
Promise.resolve(
|
||||
persistStats.execute({
|
||||
sessionsInADay: timer.getUTCDateNDaysAgo(1),
|
||||
@@ -37,15 +28,11 @@ void container.load().then((container) => {
|
||||
.then(() => {
|
||||
logger.info('Stats persisted.')
|
||||
|
||||
tracer.stopSpan()
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`Could not persist stats: ${error.message}`)
|
||||
|
||||
tracer.stopSpanWithError(error)
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { OpenTelemetrySDK, OpenTelemetryTracer } from '@standardnotes/domain-events-infra'
|
||||
import { Email, ServiceIdentifier, SettingName } from '@standardnotes/domain-core'
|
||||
|
||||
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AuthScheduledTask })
|
||||
sdk.start()
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as utc from 'dayjs/plugin/utc'
|
||||
@@ -21,6 +15,7 @@ import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
|
||||
import { GetUserKeyParams } from '../src/Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
|
||||
import { Email, SettingName } from '@standardnotes/domain-core'
|
||||
|
||||
const inputArgs = process.argv.slice(2)
|
||||
const backupEmail = inputArgs[0]
|
||||
@@ -94,9 +89,6 @@ void container.load().then((container) => {
|
||||
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.Auth_DomainEventPublisher)
|
||||
const getUserKeyParamsUseCase: GetUserKeyParams = container.get(TYPES.Auth_GetUserKeyParams)
|
||||
|
||||
const tracer = new OpenTelemetryTracer()
|
||||
tracer.startSpan(ServiceIdentifier.NAMES.AuthScheduledTask, 'user_email_backup')
|
||||
|
||||
Promise.resolve(
|
||||
requestBackups(
|
||||
userRepository,
|
||||
@@ -110,15 +102,11 @@ void container.load().then((container) => {
|
||||
.then(() => {
|
||||
logger.info(`Email backup requesting complete for ${backupEmail}`)
|
||||
|
||||
tracer.stopSpan()
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`Could not finish email backup requesting for ${backupEmail}: ${error.message}`)
|
||||
|
||||
tracer.stopSpanWithError(error)
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { OpenTelemetrySDK } from '@standardnotes/domain-events-infra'
|
||||
import { ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
|
||||
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AuthWorker })
|
||||
sdk.start()
|
||||
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
|
||||
@@ -26,12 +26,12 @@ case "$COMMAND" in
|
||||
|
||||
'email-daily-backup' )
|
||||
echo "[Docker] Starting Email Daily Backup..."
|
||||
node docker/entrypoint-backup.js email daily
|
||||
node docker/entrypoint-backup.js daily
|
||||
;;
|
||||
|
||||
'email-weekly-backup' )
|
||||
echo "[Docker] Starting Email Weekly Backup..."
|
||||
node docker/entrypoint-backup.js email weekly
|
||||
node docker/entrypoint-backup.js weekly
|
||||
;;
|
||||
|
||||
'email-backup' )
|
||||
@@ -40,21 +40,6 @@ case "$COMMAND" in
|
||||
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
|
||||
;;
|
||||
|
||||
* )
|
||||
echo "[Docker] Unknown command"
|
||||
;;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.163.0",
|
||||
"version": "1.166.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -24,12 +24,9 @@
|
||||
"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"
|
||||
|
||||
@@ -79,9 +79,9 @@ import { ExtensionKeyGrantedEventHandler } from '../Domain/Handler/ExtensionKeyG
|
||||
import {
|
||||
DirectCallDomainEventPublisher,
|
||||
DirectCallEventMessageHandler,
|
||||
SNSOpenTelemetryDomainEventPublisher,
|
||||
SNSDomainEventPublisher,
|
||||
SQSDomainEventSubscriber,
|
||||
SQSEventMessageHandler,
|
||||
SQSOpenTelemetryDomainEventSubscriber,
|
||||
} from '@standardnotes/domain-events-infra'
|
||||
import { GetUserSubscription } from '../Domain/UseCase/GetUserSubscription/GetUserSubscription'
|
||||
import { ChangeCredentials } from '../Domain/UseCase/ChangeCredentials/ChangeCredentials'
|
||||
@@ -130,8 +130,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'
|
||||
@@ -170,7 +168,6 @@ import {
|
||||
ControllerContainer,
|
||||
ControllerContainerInterface,
|
||||
MapperInterface,
|
||||
ServiceIdentifier,
|
||||
SharedVaultUser,
|
||||
} from '@standardnotes/domain-core'
|
||||
import { SessionTracePersistenceMapper } from '../Mapping/SessionTracePersistenceMapper'
|
||||
@@ -274,6 +271,11 @@ import { TypeORMSetting } from '../Infra/TypeORM/TypeORMSetting'
|
||||
import { SettingPersistenceMapper } from '../Mapping/Persistence/SettingPersistenceMapper'
|
||||
import { SubscriptionSettingPersistenceMapper } from '../Mapping/Persistence/SubscriptionSettingPersistenceMapper'
|
||||
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'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
constructor(private mode: 'server' | 'worker' = 'server') {}
|
||||
@@ -377,10 +379,7 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(
|
||||
isConfiguredForHomeServer
|
||||
? directCallDomainEventPublisher
|
||||
: new SNSOpenTelemetryDomainEventPublisher(
|
||||
container.get(TYPES.Auth_SNS),
|
||||
container.get(TYPES.Auth_SNS_TOPIC_ARN),
|
||||
),
|
||||
: new SNSDomainEventPublisher(container.get(TYPES.Auth_SNS), container.get(TYPES.Auth_SNS_TOPIC_ARN)),
|
||||
)
|
||||
|
||||
// Mapping
|
||||
@@ -723,7 +722,9 @@ export class ContainerConfigLoader {
|
||||
container.bind<AuthResponseFactory20161215>(TYPES.Auth_AuthResponseFactory20161215).to(AuthResponseFactory20161215)
|
||||
container.bind<AuthResponseFactory20190520>(TYPES.Auth_AuthResponseFactory20190520).to(AuthResponseFactory20190520)
|
||||
container.bind<AuthResponseFactory20200115>(TYPES.Auth_AuthResponseFactory20200115).to(AuthResponseFactory20200115)
|
||||
container.bind<AuthResponseFactoryResolver>(TYPES.Auth_AuthResponseFactoryResolver).to(AuthResponseFactoryResolver)
|
||||
container
|
||||
.bind<AuthResponseFactoryResolverInterface>(TYPES.Auth_AuthResponseFactoryResolver)
|
||||
.to(AuthResponseFactoryResolver)
|
||||
container.bind<KeyParamsFactory>(TYPES.Auth_KeyParamsFactory).to(KeyParamsFactory)
|
||||
container
|
||||
.bind<TokenDecoderInterface<SessionTokenData>>(TYPES.Auth_SessionTokenDecoder)
|
||||
@@ -772,16 +773,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())
|
||||
@@ -791,7 +782,16 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<SubscriptionSettingsAssociationServiceInterface>(TYPES.Auth_SubscriptionSettingsAssociationService)
|
||||
.to(SubscriptionSettingsAssociationService)
|
||||
container.bind<FeatureServiceInterface>(TYPES.Auth_FeatureService).to(FeatureService)
|
||||
container
|
||||
.bind<FeatureServiceInterface>(TYPES.Auth_FeatureService)
|
||||
.toConstantValue(
|
||||
new FeatureService(
|
||||
container.get<RoleToSubscriptionMapInterface>(TYPES.Auth_RoleToSubscriptionMap),
|
||||
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SelectorInterface<boolean>>(TYPES.Auth_BooleanSelector)
|
||||
.toConstantValue(new DeterministicSelector<boolean>())
|
||||
@@ -1020,7 +1020,19 @@ export class ContainerConfigLoader {
|
||||
container.bind<GetActiveSessionsForUser>(TYPES.Auth_GetActiveSessionsForUser).to(GetActiveSessionsForUser)
|
||||
container.bind<DeleteOtherSessionsForUser>(TYPES.Auth_DeleteOtherSessionsForUser).to(DeleteOtherSessionsForUser)
|
||||
container.bind<DeleteSessionForUser>(TYPES.Auth_DeleteSessionForUser).to(DeleteSessionForUser)
|
||||
container.bind<ChangeCredentials>(TYPES.Auth_ChangeCredentials).to(ChangeCredentials)
|
||||
container
|
||||
.bind<ChangeCredentials>(TYPES.Auth_ChangeCredentials)
|
||||
.toConstantValue(
|
||||
new ChangeCredentials(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<AuthResponseFactoryResolverInterface>(TYPES.Auth_AuthResponseFactoryResolver),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
container.get<DeleteOtherSessionsForUser>(TYPES.Auth_DeleteOtherSessionsForUser),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GetSettings>(TYPES.Auth_GetSettings)
|
||||
.toConstantValue(
|
||||
@@ -1092,6 +1104,7 @@ export class ContainerConfigLoader {
|
||||
new DeleteAccount(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
|
||||
container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
@@ -1209,6 +1222,35 @@ 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),
|
||||
),
|
||||
)
|
||||
|
||||
// Controller
|
||||
container
|
||||
@@ -1434,6 +1476,15 @@ export class ContainerConfigLoader {
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<UserInvitedToSharedVaultEventHandler>(TYPES.Auth_UserInvitedToSharedVaultEventHandler)
|
||||
.toConstantValue(
|
||||
new UserInvitedToSharedVaultEventHandler(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Auth_AccountDeletionRequestedEventHandler)],
|
||||
@@ -1469,6 +1520,7 @@ export class ContainerConfigLoader {
|
||||
'USER_DESIGNATED_AS_SURVIVOR_IN_SHARED_VAULT',
|
||||
container.get(TYPES.Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler),
|
||||
],
|
||||
['USER_INVITED_TO_SHARED_VAULT', container.get(TYPES.Auth_UserInvitedToSharedVaultEventHandler)],
|
||||
])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
@@ -1488,8 +1540,7 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<DomainEventSubscriberInterface>(TYPES.Auth_DomainEventSubscriber)
|
||||
.toConstantValue(
|
||||
new SQSOpenTelemetryDomainEventSubscriber(
|
||||
ServiceIdentifier.NAMES.AuthWorker,
|
||||
new SQSDomainEventSubscriber(
|
||||
container.get<SQSClient>(TYPES.Auth_SQS),
|
||||
container.get<string>(TYPES.Auth_SQS_QUEUE_URL),
|
||||
container.get<DomainEventMessageHandlerInterface>(TYPES.Auth_DomainEventMessageHandler),
|
||||
@@ -1624,11 +1675,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),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -164,6 +164,9 @@ 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'),
|
||||
// Handlers
|
||||
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
|
||||
Auth_SubscriptionPurchasedEventHandler: Symbol.for('Auth_SubscriptionPurchasedEventHandler'),
|
||||
@@ -195,6 +198,7 @@ const TYPES = {
|
||||
Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler: Symbol.for(
|
||||
'Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler',
|
||||
),
|
||||
Auth_UserInvitedToSharedVaultEventHandler: Symbol.for('Auth_UserInvitedToSharedVaultEventHandler'),
|
||||
// Services
|
||||
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
|
||||
Auth_SessionService: Symbol.for('Auth_SessionService'),
|
||||
@@ -229,7 +233,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'),
|
||||
|
||||
9
packages/auth/src/Domain/Email/UserEmailChanged.ts
Normal file
9
packages/auth/src/Domain/Email/UserEmailChanged.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { html } from './user-email-changed.html'
|
||||
|
||||
export function getSubject(): string {
|
||||
return 'Confirmation: Your Email Address Has Been Successfully Updated'
|
||||
}
|
||||
|
||||
export function getBody(newEmail: string): string {
|
||||
return html(newEmail)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { html } from './user-invited-to-shared-vault.html'
|
||||
|
||||
export function getSubject(): string {
|
||||
return "You're Invited to a Shared Vault!"
|
||||
}
|
||||
|
||||
export function getBody(): string {
|
||||
return html()
|
||||
}
|
||||
14
packages/auth/src/Domain/Email/user-email-changed.html.ts
Normal file
14
packages/auth/src/Domain/Email/user-email-changed.html.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const html = (newEmail: string) => `
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>We are writing to inform you that your request to update your email address has been successfully processed. The email address associated with your Standard Notes account has now been changed to the following:</p>
|
||||
<p>New Email Address: ${newEmail}</p>
|
||||
|
||||
<p>From now on, you can log in to your account using your new email address. Your password and all other account details remain the same. If you did not initiate this change or have any concerns about this update, please contact our support team by visiting our <a href="https://standardnotes.com/help">help page</a>
|
||||
or by replying directly to this email.</p>
|
||||
|
||||
<p>Best regards,</p>
|
||||
<p>
|
||||
Standard Notes
|
||||
</p>
|
||||
`
|
||||
@@ -0,0 +1,21 @@
|
||||
export const html = () => `
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>You've been invited to join a shared vault. This shared workspace will help you collaborate and securely manage notes and files.</p>
|
||||
|
||||
<p>To accept this invitation and access the shared vault, please follow these steps:</p>
|
||||
|
||||
<ol>
|
||||
<li>Go to your account settings.</li>
|
||||
<li>Navigate to the "Vaults" section.</li>
|
||||
<li>You will find the invitation there — simply click to accept.</li>
|
||||
</ol>
|
||||
|
||||
<p>If you have any questions, please contact our support team by visiting our <a href="https://standardnotes.com/help">help page</a>
|
||||
or by replying directly to this email.</p>
|
||||
|
||||
<p>Best regards,</p>
|
||||
<p>
|
||||
Standard Notes
|
||||
</p>
|
||||
`
|
||||
@@ -281,9 +281,16 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
|
||||
createAccountDeletionRequestedEvent(dto: {
|
||||
userUuid: string
|
||||
email: string
|
||||
userCreatedAtTimestamp: number
|
||||
regularSubscriptionUuid: string | undefined
|
||||
roleNames: string[]
|
||||
regularSubscription?: {
|
||||
uuid: string
|
||||
ownerUuid: string
|
||||
}
|
||||
sharedSubscription?: {
|
||||
uuid: string
|
||||
ownerUuid: string
|
||||
}
|
||||
}): AccountDeletionRequestedEvent {
|
||||
return {
|
||||
type: 'ACCOUNT_DELETION_REQUESTED',
|
||||
|
||||
@@ -45,9 +45,16 @@ export interface DomainEventFactoryInterface {
|
||||
): EmailBackupRequestedEvent
|
||||
createAccountDeletionRequestedEvent(dto: {
|
||||
userUuid: string
|
||||
email: string
|
||||
userCreatedAtTimestamp: number
|
||||
regularSubscriptionUuid: string | undefined
|
||||
roleNames: string[]
|
||||
regularSubscription?: {
|
||||
uuid: string
|
||||
ownerUuid: string
|
||||
}
|
||||
sharedSubscription?: {
|
||||
uuid: string
|
||||
ownerUuid: string
|
||||
}
|
||||
}): AccountDeletionRequestedEvent
|
||||
createUserRolesChangedEvent(userUuid: string, email: string, currentRoles: string[]): UserRolesChangedEvent
|
||||
createUserEmailChangedEvent(userUuid: string, fromEmail: string, toEmail: string): UserEmailChangedEvent
|
||||
|
||||
@@ -35,6 +35,7 @@ import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/Offl
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
|
||||
describe('FeatureService', () => {
|
||||
let roleToSubscriptionMap: RoleToSubscriptionMapInterface
|
||||
@@ -52,8 +53,10 @@ describe('FeatureService', () => {
|
||||
let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface
|
||||
let timer: TimerInterface
|
||||
let offlineUserSubscription: OfflineUserSubscription
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
|
||||
const createService = () => new FeatureService(roleToSubscriptionMap, offlineUserSubscriptionRepository, timer)
|
||||
const createService = () =>
|
||||
new FeatureService(roleToSubscriptionMap, offlineUserSubscriptionRepository, timer, userSubscriptionRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
roleToSubscriptionMap = {} as jest.Mocked<RoleToSubscriptionMapInterface>
|
||||
@@ -107,7 +110,7 @@ describe('FeatureService', () => {
|
||||
renewedAt: null,
|
||||
planName: SubscriptionName.PlusPlan,
|
||||
endsAt: 555,
|
||||
user: Promise.resolve(user),
|
||||
userUuid: 'user-1-1-1',
|
||||
cancelled: false,
|
||||
subscriptionId: 1,
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
@@ -120,7 +123,7 @@ describe('FeatureService', () => {
|
||||
renewedAt: null,
|
||||
planName: SubscriptionName.ProPlan,
|
||||
endsAt: 777,
|
||||
user: Promise.resolve(user),
|
||||
userUuid: 'user-1-1-1',
|
||||
cancelled: false,
|
||||
subscriptionId: 2,
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
@@ -133,7 +136,7 @@ describe('FeatureService', () => {
|
||||
renewedAt: null,
|
||||
planName: SubscriptionName.PlusPlan,
|
||||
endsAt: 333,
|
||||
user: Promise.resolve(user),
|
||||
userUuid: 'user-1-1-1',
|
||||
cancelled: true,
|
||||
subscriptionId: 3,
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
@@ -146,7 +149,7 @@ describe('FeatureService', () => {
|
||||
renewedAt: null,
|
||||
planName: SubscriptionName.PlusPlan,
|
||||
endsAt: 333,
|
||||
user: Promise.resolve(user),
|
||||
userUuid: 'user-1-1-1',
|
||||
cancelled: true,
|
||||
subscriptionId: 4,
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
@@ -155,9 +158,11 @@ describe('FeatureService', () => {
|
||||
user = {
|
||||
uuid: 'user-1-1-1',
|
||||
roles: Promise.resolve([role1]),
|
||||
subscriptions: Promise.resolve([subscription1]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1])
|
||||
|
||||
offlineUserSubscription = {
|
||||
roles: Promise.resolve([role1]),
|
||||
uuid: 'subscription-1-1-1',
|
||||
@@ -247,9 +252,10 @@ describe('FeatureService', () => {
|
||||
user = {
|
||||
uuid: 'user-1-1-1',
|
||||
roles: Promise.resolve([role1, role2, nonSubscriptionRole]),
|
||||
subscriptions: Promise.resolve([subscription1, subscription2]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
|
||||
|
||||
expect(await createService().userIsEntitledToFeature(user, 'files-beta')).toBe(true)
|
||||
})
|
||||
|
||||
@@ -269,9 +275,12 @@ describe('FeatureService', () => {
|
||||
user = {
|
||||
uuid: 'user-1-1-1',
|
||||
roles: Promise.resolve([role1]),
|
||||
subscriptions: Promise.resolve([subscription3, subscription1, subscription4]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userSubscriptionRepository.findByUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue([subscription3, subscription1, subscription4])
|
||||
|
||||
const features = await createService().getFeaturesForUser(user)
|
||||
expect(features).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -284,14 +293,13 @@ describe('FeatureService', () => {
|
||||
})
|
||||
|
||||
it('should not return user features if a subscription could not be found', async () => {
|
||||
const subscriptions: Array<UserSubscription> = []
|
||||
|
||||
user = {
|
||||
uuid: 'user-1-1-1',
|
||||
roles: Promise.resolve([role1]),
|
||||
subscriptions: Promise.resolve(subscriptions),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([])
|
||||
|
||||
expect(await createService().getFeaturesForUser(user)).toEqual([])
|
||||
})
|
||||
|
||||
@@ -307,9 +315,12 @@ describe('FeatureService', () => {
|
||||
user = {
|
||||
uuid: 'user-1-1-1',
|
||||
roles: Promise.resolve([role1]),
|
||||
subscriptions: Promise.resolve([subscription3, subscription1, subscription4]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userSubscriptionRepository.findByUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue([subscription3, subscription1, subscription4])
|
||||
|
||||
expect(await createService().getFeaturesForUser(user)).toEqual([])
|
||||
})
|
||||
|
||||
@@ -321,7 +332,7 @@ describe('FeatureService', () => {
|
||||
renewedAt: null,
|
||||
planName: 'non existing plan name' as SubscriptionName,
|
||||
endsAt: 555,
|
||||
user: Promise.resolve(user),
|
||||
userUuid: 'user-1-1-1',
|
||||
cancelled: false,
|
||||
subscriptionId: 1,
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
@@ -330,9 +341,10 @@ describe('FeatureService', () => {
|
||||
user = {
|
||||
uuid: 'user-1-1-1',
|
||||
roles: Promise.resolve([role1]),
|
||||
subscriptions: Promise.resolve([subscription1]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1])
|
||||
|
||||
expect(await createService().getFeaturesForUser(user)).toEqual([])
|
||||
})
|
||||
|
||||
@@ -351,9 +363,10 @@ describe('FeatureService', () => {
|
||||
user = {
|
||||
uuid: 'user-1-1-1',
|
||||
roles: Promise.resolve([role1, role2]),
|
||||
subscriptions: Promise.resolve([subscription1, subscription2]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
|
||||
|
||||
const features = await createService().getFeaturesForUser(user)
|
||||
expect(features).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -409,9 +422,10 @@ describe('FeatureService', () => {
|
||||
user = {
|
||||
uuid: 'user-1-1-1',
|
||||
roles: Promise.resolve([role1, role2, nonSubscriptionRole]),
|
||||
subscriptions: Promise.resolve([subscription1, subscription2]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
|
||||
|
||||
const features = await createService().getFeaturesForUser(user)
|
||||
expect(features).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -445,9 +459,10 @@ describe('FeatureService', () => {
|
||||
user = {
|
||||
uuid: 'user-1-1-1',
|
||||
roles: Promise.resolve([role1, role2]),
|
||||
subscriptions: Promise.resolve([subscription1, subscription2]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
|
||||
|
||||
const longestExpireAt = 777
|
||||
|
||||
const features = await createService().getFeaturesForUser(user)
|
||||
@@ -482,9 +497,10 @@ describe('FeatureService', () => {
|
||||
user = {
|
||||
uuid: 'user-1-1-1',
|
||||
roles: Promise.resolve([role1, role2]),
|
||||
subscriptions: Promise.resolve([subscription1, subscription2]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
|
||||
|
||||
const features = await createService().getFeaturesForUser(user)
|
||||
expect(features).toEqual(
|
||||
expect.arrayContaining([
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { FeatureDescription, GetFeatures } from '@standardnotes/features'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { RoleToSubscriptionMapInterface } from '../Role/RoleToSubscriptionMapInterface'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { RoleToSubscriptionMapInterface } from '../Role/RoleToSubscriptionMapInterface'
|
||||
import { User } from '../User/User'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { FeatureServiceInterface } from './FeatureServiceInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { Role } from '../Role/Role'
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class FeatureService implements FeatureServiceInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_RoleToSubscriptionMap) private roleToSubscriptionMap: RoleToSubscriptionMapInterface,
|
||||
@inject(TYPES.Auth_OfflineUserSubscriptionRepository)
|
||||
private roleToSubscriptionMap: RoleToSubscriptionMapInterface,
|
||||
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
private timer: TimerInterface,
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async userIsEntitledToFeature(user: User, featureIdentifier: string): Promise<boolean> {
|
||||
@@ -61,7 +59,7 @@ export class FeatureService implements FeatureServiceInterface {
|
||||
}
|
||||
|
||||
async getFeaturesForUser(user: User): Promise<Array<FeatureDescription>> {
|
||||
const userSubscriptions = await user.subscriptions
|
||||
const userSubscriptions = await this.userSubscriptionRepository.findByUserUuid(user.uuid)
|
||||
|
||||
return this.getFeaturesForSubscriptions(userSubscriptions, await user.roles)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export class ListedAccountCreatedEventHandler implements DomainEventHandlerInter
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not update listed author secrets for user with uuid ${user.uuid}`)
|
||||
this.logger.error(`Could not update listed author secrets for user with uuid ${user.uuid}: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { Username, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -48,7 +48,22 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
||||
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
|
||||
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
|
||||
for (const userSubscription of userSubscriptions) {
|
||||
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
|
||||
const userUuidOrError = Uuid.create(userSubscription.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
this.logger.warn(`Could not remove role from user with uuid: ${userUuidOrError.getError()}`)
|
||||
|
||||
continue
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
if (user === null) {
|
||||
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
await this.roleService.removeUserRoleBasedOnSubscription(user, subscriptionName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
): Promise<UserSubscription> {
|
||||
const subscription = new UserSubscription()
|
||||
subscription.planName = subscriptionName
|
||||
subscription.user = Promise.resolve(user)
|
||||
subscription.userUuid = user.uuid
|
||||
subscription.createdAt = timestamp
|
||||
subscription.updatedAt = timestamp
|
||||
subscription.endsAt = subscriptionExpiresAt
|
||||
|
||||
@@ -80,7 +80,7 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
|
||||
): Promise<UserSubscription> {
|
||||
const subscription = new UserSubscription()
|
||||
subscription.planName = subscriptionName
|
||||
subscription.user = Promise.resolve(user)
|
||||
subscription.userUuid = user.uuid
|
||||
subscription.createdAt = timestamp
|
||||
subscription.updatedAt = timestamp
|
||||
subscription.endsAt = subscriptionExpiresAt
|
||||
|
||||
@@ -7,7 +7,7 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { Username, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionRefundedEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -48,7 +48,22 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
||||
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
|
||||
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
|
||||
for (const userSubscription of userSubscriptions) {
|
||||
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
|
||||
const userUuidOrError = Uuid.create(userSubscription.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
this.logger.warn(`Could not remove role from user with uuid: ${userUuidOrError.getError()}`)
|
||||
|
||||
continue
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
if (user === null) {
|
||||
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
await this.roleService.removeUserRoleBasedOnSubscription(user, subscriptionName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { Logger } from 'winston'
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { Username, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -71,7 +71,20 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
||||
private async addRoleToSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
|
||||
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
|
||||
for (const userSubscription of userSubscriptions) {
|
||||
const user = await userSubscription.user
|
||||
const userUuidOrError = Uuid.create(userSubscription.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
this.logger.warn(`Could not add role to user with uuid: ${userUuidOrError.getError()}`)
|
||||
|
||||
continue
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
if (user === null) {
|
||||
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
|
||||
}
|
||||
|
||||
subscription.planName = subscriptionName
|
||||
subscription.user = Promise.resolve(user)
|
||||
subscription.userUuid = user.uuid
|
||||
subscription.createdAt = timestamp
|
||||
subscription.updatedAt = timestamp
|
||||
subscription.endsAt = subscriptionExpiresAt
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventPublisherInterface,
|
||||
UserInvitedToSharedVaultEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { EmailLevel, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { getBody, getSubject } from '../Email/UserInvitedToSharedVault'
|
||||
|
||||
export class UserInvitedToSharedVaultEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: UserInvitedToSharedVaultEvent): Promise<void> {
|
||||
const userUuidOrError = Uuid.create(event.payload.invite.user_uuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createEmailRequestedEvent({
|
||||
body: getBody(),
|
||||
level: EmailLevel.LEVELS.System,
|
||||
subject: getSubject(),
|
||||
messageIdentifier: 'USER_INVITED_TO_SHARED_VAULT',
|
||||
userEmail: user.email,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { User } from '../User/User'
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { UserSubscriptionType } from './UserSubscriptionType'
|
||||
|
||||
@Entity({ name: 'user_subscriptions' })
|
||||
@@ -63,14 +62,9 @@ export class UserSubscription {
|
||||
})
|
||||
declare subscriptionType: UserSubscriptionType
|
||||
|
||||
@ManyToOne(
|
||||
/* istanbul ignore next */
|
||||
() => User,
|
||||
/* istanbul ignore next */
|
||||
(user) => user.subscriptions,
|
||||
/* istanbul ignore next */
|
||||
{ onDelete: 'CASCADE', nullable: false, lazy: true, eager: false },
|
||||
)
|
||||
@JoinColumn({ name: 'user_uuid', referencedColumnName: 'uuid' })
|
||||
declare user: Promise<User>
|
||||
@Column({
|
||||
name: 'user_uuid',
|
||||
length: 36,
|
||||
})
|
||||
declare userUuid: string
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
subscriptionId: 3,
|
||||
subscriptionType: 'shared',
|
||||
updatedAt: 1,
|
||||
user: Promise.resolve(invitee),
|
||||
userUuid: '123',
|
||||
})
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(applyDefaultSubscriptionSettings.execute).toHaveBeenCalled()
|
||||
@@ -145,7 +145,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
subscriptionId: 3,
|
||||
subscriptionType: 'shared',
|
||||
updatedAt: 3,
|
||||
user: Promise.resolve(invitee),
|
||||
userUuid: '123',
|
||||
})
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(applyDefaultSubscriptionSettings.execute).toHaveBeenCalled()
|
||||
|
||||
@@ -115,7 +115,7 @@ export class AcceptSharedSubscriptionInvitation implements UseCaseInterface {
|
||||
): Promise<UserSubscription> {
|
||||
const subscription = new UserSubscription()
|
||||
subscription.planName = subscriptionName
|
||||
subscription.user = Promise.resolve(user)
|
||||
subscription.userUuid = user.uuid
|
||||
const timestamp = this.timer.getTimestampInMicroseconds()
|
||||
subscription.createdAt = timestamp
|
||||
subscription.updatedAt = timestamp
|
||||
|
||||
@@ -53,7 +53,7 @@ export class ActivatePremiumFeatures implements UseCaseInterface<string> {
|
||||
|
||||
const subscription = new UserSubscription()
|
||||
subscription.planName = subscriptionPlanName.value
|
||||
subscription.user = Promise.resolve(user)
|
||||
subscription.userUuid = user.uuid
|
||||
subscription.createdAt = timestamp
|
||||
subscription.updatedAt = timestamp
|
||||
subscription.endsAt = this.timer.convertDateToMicroseconds(endsAt)
|
||||
|
||||
@@ -16,7 +16,7 @@ export class AuthenticateRequest implements UseCaseInterface {
|
||||
|
||||
async execute(dto: AuthenticateRequestDTO): Promise<AuthenticateRequestResponse> {
|
||||
if (!dto.authorizationHeader) {
|
||||
this.logger.debug('Authorization header not provided.')
|
||||
this.logger.debug('[authenticate-request] Authorization header not provided.')
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@@ -32,7 +32,9 @@ export class AuthenticateRequest implements UseCaseInterface {
|
||||
token: dto.authorizationHeader.replace('Bearer ', ''),
|
||||
})
|
||||
} catch (error) {
|
||||
this.logger.error('Error occurred during authentication of a user %o', error)
|
||||
this.logger.error(
|
||||
`[authenticate-request] Error occurred during authentication of a user ${(error as Error).message}`,
|
||||
)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -23,6 +23,8 @@ describe('AuthenticateUser', () => {
|
||||
beforeEach(() => {
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
|
||||
user = {} as jest.Mocked<User>
|
||||
user.supportsSessions = jest.fn().mockReturnValue(false)
|
||||
|
||||
@@ -24,7 +24,7 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
async execute(dto: AuthenticateUserDTO): Promise<AuthenticateUserResponse> {
|
||||
const authenticationMethod = await this.authenticationMethodResolver.resolve(dto.token)
|
||||
if (!authenticationMethod) {
|
||||
this.logger.debug('No authentication method found for token.')
|
||||
this.logger.debug(`[authenticate-user] No authentication method found for token: ${dto.token}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@@ -33,6 +33,8 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
}
|
||||
|
||||
if (authenticationMethod.type === 'revoked') {
|
||||
this.logger.debug(`[authenticate-user] Session has been revoked: ${dto.token}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
failureType: 'REVOKED_SESSION',
|
||||
@@ -41,7 +43,7 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
|
||||
const user = authenticationMethod.user
|
||||
if (!user) {
|
||||
this.logger.debug('No user found for authentication method.')
|
||||
this.logger.debug(`[authenticate-user] No user found for authentication method. Token: ${dto.token}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@@ -50,7 +52,9 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
}
|
||||
|
||||
if (authenticationMethod.type == 'jwt' && user.supportsSessions()) {
|
||||
this.logger.debug('User supports sessions but is trying to authenticate with a JWT.')
|
||||
this.logger.debug(
|
||||
`[authenticate-user][${user.uuid}] User supports sessions but is trying to authenticate with a JWT.`,
|
||||
)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@@ -64,7 +68,7 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
const encryptedPasswordDigest = crypto.createHash('sha256').update(user.encryptedPassword).digest('hex')
|
||||
|
||||
if (!pwHash || !crypto.timingSafeEqual(Buffer.from(pwHash), Buffer.from(encryptedPasswordDigest))) {
|
||||
this.logger.debug('Password hash does not match.')
|
||||
this.logger.debug(`[authenticate-user][${user.uuid}] Password hash does not match.`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@@ -76,7 +80,7 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
case 'session_token': {
|
||||
const session = authenticationMethod.session
|
||||
if (!session) {
|
||||
this.logger.debug('No session found for authentication method.')
|
||||
this.logger.debug(`[authenticate-user][${user.uuid}] No session found for authentication method.`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@@ -85,7 +89,7 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
}
|
||||
|
||||
if (session.refreshExpiration < this.timer.getUTCDate()) {
|
||||
this.logger.debug('Session refresh token has expired.')
|
||||
this.logger.debug(`[authenticate-user][${user.uuid}] Session refresh token has expired.`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@@ -94,6 +98,8 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
}
|
||||
|
||||
if (this.sessionIsExpired(session)) {
|
||||
this.logger.debug(`[authenticate-user][${user.uuid}] Session access token has expired.`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
failureType: 'EXPIRED_TOKEN',
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([inviterSubscription])
|
||||
userSubscriptionRepository.findOneByUserUuidAndSubscriptionId = jest
|
||||
.fn()
|
||||
.mockReturnValue({ user: Promise.resolve(invitee) } as jest.Mocked<UserSubscription>)
|
||||
.mockReturnValue({ userUuid: '123' } as jest.Mocked<UserSubscription>)
|
||||
userSubscriptionRepository.save = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
@@ -120,7 +120,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
})
|
||||
expect(userSubscriptionRepository.save).toHaveBeenCalledWith({
|
||||
endsAt: 1,
|
||||
user: Promise.resolve(invitee),
|
||||
userUuid: '123',
|
||||
})
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Result, Username } from '@standardnotes/domain-core'
|
||||
import { DeleteOtherSessionsForUser } from '../DeleteOtherSessionsForUser'
|
||||
import { ApiVersion } from '../../Api/ApiVersion'
|
||||
import { Session } from '../../Session/Session'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
describe('ChangeCredentials', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
@@ -25,6 +26,7 @@ describe('ChangeCredentials', () => {
|
||||
let timer: TimerInterface
|
||||
let user: User
|
||||
let deleteOtherSessionsForUser: DeleteOtherSessionsForUser
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () =>
|
||||
new ChangeCredentials(
|
||||
@@ -34,9 +36,13 @@ describe('ChangeCredentials', () => {
|
||||
domainEventFactory,
|
||||
timer,
|
||||
deleteOtherSessionsForUser,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
|
||||
authResponseFactory = {} as jest.Mocked<AuthResponseFactoryInterface>
|
||||
authResponseFactory.createResponse = jest
|
||||
.fn()
|
||||
@@ -51,7 +57,7 @@ describe('ChangeCredentials', () => {
|
||||
user.email = 'test@test.te'
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.save = jest.fn()
|
||||
userRepository.save = jest.fn().mockImplementation((user: User) => Promise.resolve(user))
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import * as bcrypt from 'bcryptjs'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { DomainEventPublisherInterface, UserEmailChangedEvent } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Result, UseCaseInterface, Username } from '@standardnotes/domain-core'
|
||||
import { EmailLevel, Result, UseCaseInterface, Username } from '@standardnotes/domain-core'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { AuthResponseFactoryResolverInterface } from '../../Auth/AuthResponseFactoryResolverInterface'
|
||||
import { User } from '../../User/User'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
@@ -14,18 +12,18 @@ import { DeleteOtherSessionsForUser } from '../DeleteOtherSessionsForUser'
|
||||
import { AuthResponse20161215 } from '../../Auth/AuthResponse20161215'
|
||||
import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
|
||||
import { Session } from '../../Session/Session'
|
||||
import { getBody, getSubject } from '../../Email/UserEmailChanged'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
@injectable()
|
||||
export class ChangeCredentials implements UseCaseInterface<AuthResponse20161215 | AuthResponse20200115> {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_AuthResponseFactoryResolver)
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private authResponseFactoryResolver: AuthResponseFactoryResolverInterface,
|
||||
@inject(TYPES.Auth_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.Auth_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Auth_DeleteOtherSessionsForUser)
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private timer: TimerInterface,
|
||||
private deleteOtherSessionsForUserUseCase: DeleteOtherSessionsForUser,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: ChangeCredentialsDTO): Promise<Result<AuthResponse20161215 | AuthResponse20200115>> {
|
||||
@@ -41,6 +39,7 @@ export class ChangeCredentials implements UseCaseInterface<AuthResponse20161215
|
||||
user.encryptedPassword = await bcrypt.hash(dto.newPassword, User.PASSWORD_HASH_COST)
|
||||
|
||||
let userEmailChangedEvent: UserEmailChangedEvent | undefined = undefined
|
||||
const existingEmailAddress = user.email
|
||||
if (dto.newEmail !== undefined) {
|
||||
const newUsernameOrError = Username.create(dto.newEmail)
|
||||
if (newUsernameOrError.isFailed()) {
|
||||
@@ -78,6 +77,8 @@ export class ChangeCredentials implements UseCaseInterface<AuthResponse20161215
|
||||
|
||||
if (userEmailChangedEvent !== undefined) {
|
||||
await this.domainEventPublisher.publish(userEmailChangedEvent)
|
||||
|
||||
await this.sendEmailChangedNotification(existingEmailAddress, updatedUser.email)
|
||||
}
|
||||
|
||||
const authResponseFactory = this.authResponseFactoryResolver.resolveAuthResponseFactoryVersion(dto.apiVersion)
|
||||
@@ -113,4 +114,21 @@ export class ChangeCredentials implements UseCaseInterface<AuthResponse20161215
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async sendEmailChangedNotification(oldEmail: string, newEmail: string): Promise<void> {
|
||||
try {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createEmailRequestedEvent({
|
||||
userEmail: oldEmail,
|
||||
level: EmailLevel.LEVELS.System,
|
||||
body: getBody(newEmail),
|
||||
messageIdentifier: 'EMAIL_CHANGED',
|
||||
subject: getSubject(),
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
/* istanbul ignore next */
|
||||
this.logger.error(`Could not publish email changed request for email: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { TokenEncoderInterface, ValetTokenData, ValetTokenOperation } from '@sta
|
||||
|
||||
import { CreateValetToken } from './CreateValetToken'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { User } from '../../User/User'
|
||||
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
|
||||
import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface'
|
||||
import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
|
||||
@@ -25,7 +24,6 @@ describe('CreateValetToken', () => {
|
||||
const valetTokenTTL = 123
|
||||
let regularSubscription: UserSubscription
|
||||
let sharedSubscription: UserSubscription
|
||||
let user: User
|
||||
|
||||
const createUseCase = () =>
|
||||
new CreateValetToken(
|
||||
@@ -59,20 +57,16 @@ describe('CreateValetToken', () => {
|
||||
subscriptionSettingsAssociationService = {} as jest.Mocked<SubscriptionSettingsAssociationServiceInterface>
|
||||
subscriptionSettingsAssociationService.getFileUploadLimit = jest.fn().mockReturnValue(5_368_709_120)
|
||||
|
||||
user = {
|
||||
uuid: '123',
|
||||
} as jest.Mocked<User>
|
||||
|
||||
regularSubscription = {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
user: Promise.resolve(user),
|
||||
userUuid: '123',
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
sharedSubscription = {
|
||||
uuid: '2-3-4',
|
||||
subscriptionType: UserSubscriptionType.Shared,
|
||||
user: Promise.resolve(user),
|
||||
userUuid: '123',
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
getRegularSubscription = {} as jest.Mocked<GetRegularSubscriptionForUser>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/
|
||||
import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser'
|
||||
import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting'
|
||||
import { SettingName } from '@standardnotes/domain-core'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
|
||||
export class CreateValetToken implements UseCaseInterface {
|
||||
constructor(
|
||||
@@ -27,8 +28,17 @@ export class CreateValetToken implements UseCaseInterface {
|
||||
async execute(dto: CreateValetTokenDTO): Promise<CreateValetTokenResponseData> {
|
||||
const { userUuid, ...payload } = dto
|
||||
|
||||
let sharedSubscription: UserSubscription | undefined
|
||||
const sharedSubscriptionOrError = await this.getSharedSubscription.execute({
|
||||
userUuid,
|
||||
})
|
||||
if (!sharedSubscriptionOrError.isFailed()) {
|
||||
sharedSubscription = sharedSubscriptionOrError.getValue()
|
||||
}
|
||||
|
||||
const regularSubscriptionOrError = await this.getRegularSubscription.execute({
|
||||
userUuid: dto.userUuid,
|
||||
userUuid: sharedSubscription ? undefined : dto.userUuid,
|
||||
subscriptionId: sharedSubscription ? (sharedSubscription.subscriptionId as number) : undefined,
|
||||
})
|
||||
if (regularSubscriptionOrError.isFailed()) {
|
||||
return {
|
||||
@@ -77,22 +87,13 @@ export class CreateValetToken implements UseCaseInterface {
|
||||
uploadBytesLimit = +(overwriteWithUserUploadBytesLimitSetting.setting.props.value as string)
|
||||
}
|
||||
|
||||
let sharedSubscriptionUuid = undefined
|
||||
const sharedSubscriptionOrError = await this.getSharedSubscription.execute({
|
||||
userUuid,
|
||||
})
|
||||
if (!sharedSubscriptionOrError.isFailed()) {
|
||||
const sharedSubscription = sharedSubscriptionOrError.getValue()
|
||||
sharedSubscriptionUuid = sharedSubscription.uuid
|
||||
}
|
||||
|
||||
const tokenData: ValetTokenData = {
|
||||
userUuid: dto.userUuid,
|
||||
permittedOperation: dto.operation,
|
||||
permittedResources: dto.resources,
|
||||
uploadBytesUsed,
|
||||
uploadBytesLimit,
|
||||
sharedSubscriptionUuid,
|
||||
sharedSubscriptionUuid: sharedSubscription ? sharedSubscription.uuid : undefined,
|
||||
regularSubscriptionUuid: regularSubscription.uuid,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,29 +11,46 @@ import { TimerInterface } from '@standardnotes/time'
|
||||
import { Result, RoleName } from '@standardnotes/domain-core'
|
||||
import { Role } from '../../Role/Role'
|
||||
import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
|
||||
import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser'
|
||||
|
||||
describe('DeleteAccount', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let getRegularSubscription: GetRegularSubscriptionForUser
|
||||
let getSharedSubscription: GetSharedSubscriptionForUser
|
||||
let user: User
|
||||
let regularSubscription: UserSubscription
|
||||
let sharedSubscription: UserSubscription
|
||||
let timer: TimerInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new DeleteAccount(userRepository, getRegularSubscription, domainEventPublisher, domainEventFactory, timer)
|
||||
new DeleteAccount(
|
||||
userRepository,
|
||||
getRegularSubscription,
|
||||
getSharedSubscription,
|
||||
domainEventPublisher,
|
||||
domainEventFactory,
|
||||
timer,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '1-2-3',
|
||||
email: 'test@test.te',
|
||||
} as jest.Mocked<User>
|
||||
user.roles = Promise.resolve([{ name: RoleName.NAMES.CoreUser } as jest.Mocked<Role>])
|
||||
|
||||
regularSubscription = {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
user: Promise.resolve(user),
|
||||
userUuid: 'u-1-2-3',
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
sharedSubscription = {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: UserSubscriptionType.Shared,
|
||||
userUuid: 'u-1-2-3',
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
@@ -43,6 +60,9 @@ describe('DeleteAccount', () => {
|
||||
getRegularSubscription = {} as jest.Mocked<GetRegularSubscriptionForUser>
|
||||
getRegularSubscription.execute = jest.fn().mockReturnValue(Result.ok(regularSubscription))
|
||||
|
||||
getSharedSubscription = {} as jest.Mocked<GetSharedSubscriptionForUser>
|
||||
getSharedSubscription.execute = jest.fn().mockReturnValue(Result.ok(sharedSubscription))
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
@@ -58,6 +78,7 @@ describe('DeleteAccount', () => {
|
||||
describe('when user uuid is provided', () => {
|
||||
it('should trigger account deletion - no subscription', async () => {
|
||||
getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
|
||||
getSharedSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
|
||||
|
||||
const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
|
||||
|
||||
@@ -66,12 +87,11 @@ describe('DeleteAccount', () => {
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: undefined,
|
||||
roleNames: ['CORE_USER'],
|
||||
email: 'test@test.te',
|
||||
})
|
||||
})
|
||||
|
||||
it('should trigger account deletion - subscription present', async () => {
|
||||
it('should trigger account deletion - shared subscription present', async () => {
|
||||
const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -79,9 +99,35 @@ describe('DeleteAccount', () => {
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
email: 'test@test.te',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
roleNames: ['CORE_USER'],
|
||||
regularSubscription: {
|
||||
ownerUuid: 'u-1-2-3',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
sharedSubscription: {
|
||||
ownerUuid: 'u-1-2-3',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should trigger account deletion - regular subscription present', async () => {
|
||||
getSharedSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
|
||||
|
||||
const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
email: 'test@test.te',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscription: {
|
||||
ownerUuid: 'u-1-2-3',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -109,6 +155,7 @@ describe('DeleteAccount', () => {
|
||||
describe('when username is provided', () => {
|
||||
it('should trigger account deletion - no subscription', async () => {
|
||||
getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
|
||||
getSharedSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
|
||||
|
||||
const result = await createUseCase().execute({ username: 'test@test.te' })
|
||||
|
||||
@@ -116,13 +163,12 @@ describe('DeleteAccount', () => {
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
email: 'test@test.te',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: undefined,
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
})
|
||||
|
||||
it('should trigger account deletion - subscription present', async () => {
|
||||
it('should trigger account deletion - shared subscription present', async () => {
|
||||
const result = await createUseCase().execute({ username: 'test@test.te' })
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -130,9 +176,35 @@ describe('DeleteAccount', () => {
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
email: 'test@test.te',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
roleNames: ['CORE_USER'],
|
||||
regularSubscription: {
|
||||
ownerUuid: 'u-1-2-3',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
sharedSubscription: {
|
||||
ownerUuid: 'u-1-2-3',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should trigger account deletion - regular subscription present', async () => {
|
||||
getSharedSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
|
||||
|
||||
const result = await createUseCase().execute({ username: 'test@test.te' })
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
email: 'test@test.te',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscription: {
|
||||
ownerUuid: 'u-1-2-3',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -8,11 +8,14 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { DeleteAccountDTO } from './DeleteAccountDTO'
|
||||
import { User } from '../../User/User'
|
||||
import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser'
|
||||
|
||||
export class DeleteAccount implements UseCaseInterface<string> {
|
||||
constructor(
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private getRegularSubscription: GetRegularSubscriptionForUser,
|
||||
private getSharedSubscription: GetSharedSubscriptionForUser,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private timer: TimerInterface,
|
||||
@@ -44,23 +47,39 @@ export class DeleteAccount implements UseCaseInterface<string> {
|
||||
return Result.ok('User already deleted.')
|
||||
}
|
||||
|
||||
const roles = await user.roles
|
||||
|
||||
let regularSubscriptionUuid: string | undefined
|
||||
const result = await this.getRegularSubscription.execute({
|
||||
let sharedSubscription: UserSubscription | undefined
|
||||
const sharedSubscriptionOrError = await this.getSharedSubscription.execute({
|
||||
userUuid: user.uuid,
|
||||
})
|
||||
if (!result.isFailed()) {
|
||||
const regularSubscription = result.getValue()
|
||||
regularSubscriptionUuid = regularSubscription.uuid
|
||||
if (!sharedSubscriptionOrError.isFailed()) {
|
||||
sharedSubscription = sharedSubscriptionOrError.getValue()
|
||||
}
|
||||
|
||||
let regularSubscription: UserSubscription | undefined
|
||||
const regularSubscriptionOrError = await this.getRegularSubscription.execute({
|
||||
userUuid: user.uuid,
|
||||
})
|
||||
if (!regularSubscriptionOrError.isFailed()) {
|
||||
regularSubscription = regularSubscriptionOrError.getValue()
|
||||
}
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createAccountDeletionRequestedEvent({
|
||||
userUuid: user.uuid,
|
||||
email: user.email,
|
||||
userCreatedAtTimestamp: this.timer.convertDateToMicroseconds(user.createdAt),
|
||||
regularSubscriptionUuid,
|
||||
roleNames: roles.map((role) => role.name),
|
||||
regularSubscription: regularSubscription
|
||||
? {
|
||||
ownerUuid: regularSubscription.userUuid,
|
||||
uuid: regularSubscription.uuid,
|
||||
}
|
||||
: undefined,
|
||||
sharedSubscription: sharedSubscription
|
||||
? {
|
||||
ownerUuid: sharedSubscription.userUuid,
|
||||
uuid: sharedSubscription.uuid,
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('GetRegularSubscriptionForUser', () => {
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns regular subscription when user subscription is regular', async () => {
|
||||
it('returns regular subscription for user uuid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
|
||||
@@ -43,4 +43,31 @@ describe('GetRegularSubscriptionForUser', () => {
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(result.getValue()).toBe(regularSubscription)
|
||||
})
|
||||
|
||||
it('returns regular subscription for shared subscription id', async () => {
|
||||
const useCase = createUseCase()
|
||||
userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockResolvedValue([regularSubscription])
|
||||
|
||||
const result = await useCase.execute({ subscriptionId: 1 })
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(result.getValue()).toBe(regularSubscription)
|
||||
})
|
||||
|
||||
it('returns error if subscription for shared subscription id is not found', async () => {
|
||||
const useCase = createUseCase()
|
||||
userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockResolvedValue([])
|
||||
|
||||
const result = await useCase.execute({ subscriptionId: 1 })
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns error if no parameters are specified', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,18 @@ export class GetRegularSubscriptionForUser implements UseCaseInterface<UserSubsc
|
||||
constructor(private userSubscriptionRepository: UserSubscriptionRepositoryInterface) {}
|
||||
|
||||
async execute(dto: GetRegularSubscriptionForUserDTO): Promise<Result<UserSubscription>> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (dto.userUuid !== undefined) {
|
||||
return this.getRegularSubscriptionForUser(dto.userUuid)
|
||||
}
|
||||
if (dto.subscriptionId !== undefined) {
|
||||
return this.getRegularSubscriptionForSharedSubscription(dto.subscriptionId)
|
||||
}
|
||||
|
||||
return Result.fail('Invalid parameters.')
|
||||
}
|
||||
|
||||
private async getRegularSubscriptionForUser(userUuidString: string): Promise<Result<UserSubscription>> {
|
||||
const userUuidOrError = Uuid.create(userUuidString)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(`Could not get regular subscription for user: ${userUuidOrError.getError()}`)
|
||||
}
|
||||
@@ -25,4 +36,16 @@ export class GetRegularSubscriptionForUser implements UseCaseInterface<UserSubsc
|
||||
|
||||
return Result.ok(userSubscription)
|
||||
}
|
||||
|
||||
private async getRegularSubscriptionForSharedSubscription(subscriptionId: number): Promise<Result<UserSubscription>> {
|
||||
const userSubscription = await this.userSubscriptionRepository.findBySubscriptionIdAndType(
|
||||
subscriptionId,
|
||||
UserSubscriptionType.Regular,
|
||||
)
|
||||
if (userSubscription.length === 0) {
|
||||
return Result.fail(`User subscription for shared subscription ${subscriptionId} not found.`)
|
||||
}
|
||||
|
||||
return Result.ok(userSubscription[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export interface GetRegularSubscriptionForUserDTO {
|
||||
userUuid: string
|
||||
userUuid?: string
|
||||
subscriptionId?: number
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ describe('SetSettingValue', () => {
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
settingName: SettingName.NAMES.ListedAuthorSecrets,
|
||||
value: 'value',
|
||||
checkUserPermissions: true,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
@@ -108,6 +109,7 @@ describe('SetSettingValue', () => {
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
settingName: SettingName.NAMES.MfaSecret,
|
||||
value: 'value',
|
||||
checkUserPermissions: true,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
@@ -140,6 +142,20 @@ describe('SetSettingValue', () => {
|
||||
expect(settingRepository.update).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should create a setting with checking user permissions', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
settingName: SettingName.NAMES.MfaSecret,
|
||||
value: 'value',
|
||||
checkUserPermissions: true,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(settingRepository.insert).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should insert a new setting if one does not exist', async () => {
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found'))
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export class SetSettingValue implements UseCaseInterface<Setting> {
|
||||
return Result.fail(`Setting ${settingName.value} is a subscription setting!`)
|
||||
}
|
||||
|
||||
if (!(await this.userHasPermissionToUpdateSetting(userUuid, settingName))) {
|
||||
if (dto.checkUserPermissions && !(await this.userHasPermissionToUpdateSetting(userUuid, settingName))) {
|
||||
return Result.fail(`User ${userUuid.value} does not have permission to update setting ${settingName.value}.`)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,5 @@ export interface SetSettingValueDTO {
|
||||
settingName: string
|
||||
userUuid: string
|
||||
value: string | null
|
||||
checkUserPermissions?: 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
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { DomainEventPublisherInterface, EmailBackupRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { Result, SettingName, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
|
||||
import { GetSetting } from '../GetSetting/GetSetting'
|
||||
import { GetUserKeyParams } from '../GetUserKeyParams/GetUserKeyParams'
|
||||
import { TriggerEmailBackupForUser } from './TriggerEmailBackupForUser'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { Setting } from '../../Setting/Setting'
|
||||
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
|
||||
|
||||
describe('TriggerEmailBackupForUser', () => {
|
||||
let roleService: RoleServiceInterface
|
||||
let getSetting: GetSetting
|
||||
let getUserKeyParamsUseCase: GetUserKeyParams
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new TriggerEmailBackupForUser(
|
||||
roleService,
|
||||
getSetting,
|
||||
getUserKeyParamsUseCase,
|
||||
domainEventPublisher,
|
||||
domainEventFactory,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.userHasPermission = jest.fn().mockResolvedValue(true)
|
||||
|
||||
const setting = Setting.create({
|
||||
name: SettingName.NAMES.ListedAuthorSecrets,
|
||||
value: null,
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sensitive: false,
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
getSetting = {} as jest.Mocked<GetSetting>
|
||||
getSetting.execute = jest.fn().mockResolvedValue(Result.ok({ setting, decryptedValue: 'not_muted' }))
|
||||
|
||||
getUserKeyParamsUseCase = {} as jest.Mocked<GetUserKeyParams>
|
||||
getUserKeyParamsUseCase.execute = jest.fn().mockResolvedValue({ keyParams: {} })
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createEmailBackupRequestedEvent = jest.fn().mockReturnValue({} as EmailBackupRequestedEvent)
|
||||
})
|
||||
|
||||
it('publishes EmailBackupRequestedEvent', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns error if user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({ userUuid: 'invalid-uuid' })
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns error if user is not permitted for email backups', async () => {
|
||||
roleService.userHasPermission = jest.fn().mockResolvedValue(false)
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Result, SettingName, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
|
||||
import { TriggerEmailBackupForUserDTO } from './TriggerEmailBackupForUserDTO'
|
||||
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
|
||||
import { GetSetting } from '../GetSetting/GetSetting'
|
||||
import { MuteFailedBackupsEmailsOption } from '@standardnotes/settings'
|
||||
import { GetUserKeyParams } from '../GetUserKeyParams/GetUserKeyParams'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
|
||||
export class TriggerEmailBackupForUser implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private roleService: RoleServiceInterface,
|
||||
private getSetting: GetSetting,
|
||||
private getUserKeyParamsUseCase: GetUserKeyParams,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: TriggerEmailBackupForUserDTO): Promise<Result<void>> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const userIsPermittedForEmailBackups = await this.roleService.userHasPermission(
|
||||
userUuid.value,
|
||||
PermissionName.DailyEmailBackup,
|
||||
)
|
||||
|
||||
if (!userIsPermittedForEmailBackups) {
|
||||
return Result.fail(`User ${userUuid.value} is not permitted for email backups`)
|
||||
}
|
||||
|
||||
let userHasEmailsMuted = false
|
||||
const emailsMutedSettingOrError = await this.getSetting.execute({
|
||||
allowSensitiveRetrieval: true,
|
||||
decrypted: true,
|
||||
settingName: SettingName.NAMES.MuteFailedBackupsEmails,
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
let emailsMutedSetting = null
|
||||
if (!emailsMutedSettingOrError.isFailed()) {
|
||||
emailsMutedSetting = emailsMutedSettingOrError.getValue()
|
||||
userHasEmailsMuted = emailsMutedSetting.decryptedValue === MuteFailedBackupsEmailsOption.Muted
|
||||
}
|
||||
|
||||
const keyParamsResponse = await this.getUserKeyParamsUseCase.execute({
|
||||
userUuid: userUuid.value,
|
||||
authenticated: false,
|
||||
})
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createEmailBackupRequestedEvent(
|
||||
userUuid.value,
|
||||
emailsMutedSetting?.setting.id.toString() as string,
|
||||
userHasEmailsMuted,
|
||||
keyParamsResponse.keyParams,
|
||||
),
|
||||
)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface TriggerEmailBackupForUserDTO {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
DomainEventPublisherInterface,
|
||||
EmailBackupRequestedEvent,
|
||||
MuteEmailsSettingChangedEvent,
|
||||
UserDisabledSessionUserAgentLoggingEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { EmailBackupFrequency, LogSessionUserAgentOption, MuteMarketingEmailsOption } from '@standardnotes/settings'
|
||||
import { SettingName, Result } from '@standardnotes/domain-core'
|
||||
|
||||
import { TriggerPostSettingUpdateActions } from './TriggerPostSettingUpdateActions'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { TriggerEmailBackupForUser } from '../TriggerEmailBackupForUser/TriggerEmailBackupForUser'
|
||||
|
||||
describe('TriggerPostSettingUpdateActions', () => {
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let triggerEmailBackupForUser: TriggerEmailBackupForUser
|
||||
|
||||
const createUseCase = () =>
|
||||
new TriggerPostSettingUpdateActions(domainEventPublisher, domainEventFactory, triggerEmailBackupForUser)
|
||||
|
||||
beforeEach(() => {
|
||||
triggerEmailBackupForUser = {} as jest.Mocked<TriggerEmailBackupForUser>
|
||||
triggerEmailBackupForUser.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
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>)
|
||||
})
|
||||
|
||||
it('should trigger session cleanup if user is disabling session user agent logging', async () => {
|
||||
await createUseCase().execute({
|
||||
updatedSettingName: SettingName.NAMES.LogSessionUserAgent,
|
||||
userUuid: '4-5-6',
|
||||
userEmail: 'test@test.te',
|
||||
unencryptedValue: 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 createUseCase().execute({
|
||||
updatedSettingName: SettingName.NAMES.EmailBackupFrequency,
|
||||
userUuid: '4-5-6',
|
||||
userEmail: 'test@test.te',
|
||||
unencryptedValue: EmailBackupFrequency.Daily,
|
||||
})
|
||||
|
||||
expect(triggerEmailBackupForUser.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should trigger backup if email backup setting is created - emails muted', async () => {
|
||||
await createUseCase().execute({
|
||||
updatedSettingName: SettingName.NAMES.EmailBackupFrequency,
|
||||
userUuid: '4-5-6',
|
||||
userEmail: 'test@test.te',
|
||||
unencryptedValue: EmailBackupFrequency.Daily,
|
||||
})
|
||||
|
||||
expect(triggerEmailBackupForUser.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not trigger backup if email backup setting is disabled', async () => {
|
||||
await createUseCase().execute({
|
||||
updatedSettingName: SettingName.NAMES.EmailBackupFrequency,
|
||||
userUuid: '4-5-6',
|
||||
userEmail: 'test@test.te',
|
||||
unencryptedValue: EmailBackupFrequency.Disabled,
|
||||
})
|
||||
|
||||
expect(triggerEmailBackupForUser.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should trigger mute subscription emails rejection if mute setting changed', async () => {
|
||||
await createUseCase().execute({
|
||||
updatedSettingName: SettingName.NAMES.MuteMarketingEmails,
|
||||
userUuid: '4-5-6',
|
||||
userEmail: 'test@test.te',
|
||||
unencryptedValue: MuteMarketingEmailsOption.Muted,
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
expect(domainEventFactory.createMuteEmailsSettingChangedEvent).toHaveBeenCalledWith({
|
||||
emailSubscriptionRejectionLevel: 'MARKETING',
|
||||
mute: true,
|
||||
username: 'test@test.te',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { EmailLevel, Result, SettingName, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { EmailBackupFrequency, LogSessionUserAgentOption } from '@standardnotes/settings'
|
||||
|
||||
import { TriggerPostSettingUpdateActionsDTO } from './TriggerPostSettingUpdateActionsDTO'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { TriggerEmailBackupForUser } from '../TriggerEmailBackupForUser/TriggerEmailBackupForUser'
|
||||
|
||||
export class TriggerPostSettingUpdateActions implements UseCaseInterface<void> {
|
||||
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 triggerEmailBackupForUser: TriggerEmailBackupForUser,
|
||||
) {}
|
||||
|
||||
async execute(dto: TriggerPostSettingUpdateActionsDTO): Promise<Result<void>> {
|
||||
if (this.isChangingMuteEmailsSetting(dto.updatedSettingName)) {
|
||||
await this.triggerEmailSubscriptionChange(dto.userEmail, dto.updatedSettingName, dto.unencryptedValue)
|
||||
}
|
||||
|
||||
if (this.isEnablingEmailBackupSetting(dto.updatedSettingName, dto.unencryptedValue)) {
|
||||
await this.triggerEmailBackupForUser.execute({
|
||||
userUuid: dto.userUuid,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.isDisablingSessionUserAgentLogging(dto.updatedSettingName, dto.unencryptedValue)) {
|
||||
await this.triggerSessionUserAgentCleanup(dto.userEmail, dto.userUuid)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
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(
|
||||
userEmail: string,
|
||||
settingName: string,
|
||||
unencryptedValue: string | null,
|
||||
): Promise<void> {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createMuteEmailsSettingChangedEvent({
|
||||
username: userEmail,
|
||||
mute: unencryptedValue === 'muted',
|
||||
emailSubscriptionRejectionLevel: this.emailSettingToSubscriptionRejectionLevelMap.get(settingName) as string,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private async triggerSessionUserAgentCleanup(userEmail: string, userUuid: string) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent({
|
||||
userUuid,
|
||||
email: userEmail,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface TriggerPostSettingUpdateActionsDTO {
|
||||
updatedSettingName: string
|
||||
userUuid: string
|
||||
userEmail: string
|
||||
unencryptedValue: string | null
|
||||
}
|
||||
@@ -45,13 +45,13 @@ describe('UpdateStorageQuotaUsedForUser', () => {
|
||||
regularSubscription = {
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
user: Promise.resolve(user),
|
||||
userUuid: '123',
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
sharedSubscription = {
|
||||
uuid: '2-3-4',
|
||||
subscriptionType: UserSubscriptionType.Shared,
|
||||
user: Promise.resolve(user),
|
||||
userUuid: '123',
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
getSharedSubscription = {} as jest.Mocked<GetSharedSubscriptionForUser>
|
||||
|
||||
@@ -31,9 +31,19 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
|
||||
return Result.fail(`Could not find user with uuid: ${userUuid.value}`)
|
||||
}
|
||||
|
||||
const regularSubscriptionOrError = await this.getRegularSubscription.execute({
|
||||
const sharedSubscriptionOrError = await this.getSharedSubscription.execute({
|
||||
userUuid: user.uuid,
|
||||
})
|
||||
let sharedSubscription: UserSubscription | undefined
|
||||
if (!sharedSubscriptionOrError.isFailed()) {
|
||||
sharedSubscription = sharedSubscriptionOrError.getValue()
|
||||
await this.updateUploadBytesUsedSetting(sharedSubscription, dto.bytesUsed)
|
||||
}
|
||||
|
||||
const regularSubscriptionOrError = await this.getRegularSubscription.execute({
|
||||
userUuid: sharedSubscription ? undefined : user.uuid,
|
||||
subscriptionId: sharedSubscription ? (sharedSubscription.subscriptionId as number) : undefined,
|
||||
})
|
||||
if (regularSubscriptionOrError.isFailed()) {
|
||||
return Result.fail(`Could not find regular user subscription for user with uuid: ${userUuid.value}`)
|
||||
}
|
||||
@@ -41,20 +51,11 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
|
||||
|
||||
await this.updateUploadBytesUsedSetting(regularSubscription, dto.bytesUsed)
|
||||
|
||||
const sharedSubscriptionOrError = await this.getSharedSubscription.execute({
|
||||
userUuid: user.uuid,
|
||||
})
|
||||
if (!sharedSubscriptionOrError.isFailed()) {
|
||||
const sharedSubscription = sharedSubscriptionOrError.getValue()
|
||||
await this.updateUploadBytesUsedSetting(sharedSubscription, dto.bytesUsed)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async updateUploadBytesUsedSetting(subscription: UserSubscription, bytesUsed: number): Promise<void> {
|
||||
let bytesAlreadyUsed = '0'
|
||||
const subscriptionUser = await subscription.user
|
||||
|
||||
const bytesUsedSettingExists = await this.getSubscriptionSetting.execute({
|
||||
userSubscriptionUuid: subscription.uuid,
|
||||
@@ -75,7 +76,7 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not set file upload bytes used for user ${subscriptionUser.uuid}`)
|
||||
this.logger.error(`Could not set file upload bytes used for subscription ${subscription.uuid}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Column, Entity, Index, JoinTable, ManyToMany, OneToMany, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { RevokedSession } from '../Session/RevokedSession'
|
||||
import { Role } from '../Role/Role'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { TypeORMEmergencyAccessInvitation } from '../../Infra/TypeORM/TypeORMEmergencyAccessInvitation'
|
||||
|
||||
@@ -161,16 +160,6 @@ export class User {
|
||||
})
|
||||
declare roles: Promise<Array<Role>>
|
||||
|
||||
@OneToMany(
|
||||
/* istanbul ignore next */
|
||||
() => UserSubscription,
|
||||
/* istanbul ignore next */
|
||||
(subscription) => subscription.user,
|
||||
/* istanbul ignore next */
|
||||
{ lazy: true, eager: false },
|
||||
)
|
||||
declare subscriptions: Promise<UserSubscription[]>
|
||||
|
||||
@OneToMany(
|
||||
/* istanbul ignore next */
|
||||
() => TypeORMEmergencyAccessInvitation,
|
||||
|
||||
@@ -19,6 +19,8 @@ import { SubscriptionSetting } from '../../Domain/Setting/SubscriptionSetting'
|
||||
import { SettingHttpRepresentation } from '../../Mapping/Http/SettingHttpRepresentation'
|
||||
import { SubscriptionSettingHttpRepresentation } from '../../Mapping/Http/SubscriptionSettingHttpRepresentation'
|
||||
import { GetAllSettingsForUser } from '../../Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser'
|
||||
import { TriggerPostSettingUpdateActions } from '../../Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
@controller('/users/:userUuid')
|
||||
export class AnnotatedSettingsController extends BaseSettingsController {
|
||||
@@ -26,18 +28,23 @@ export class AnnotatedSettingsController extends BaseSettingsController {
|
||||
@inject(TYPES.Auth_GetAllSettingsForUser) override doGetSettings: GetAllSettingsForUser,
|
||||
@inject(TYPES.Auth_GetSetting) override doGetSetting: GetSetting,
|
||||
@inject(TYPES.Auth_SetSettingValue) override setSettingValue: SetSettingValue,
|
||||
@inject(TYPES.Auth_TriggerPostSettingUpdateActions)
|
||||
override triggerPostSettingUpdateActions: TriggerPostSettingUpdateActions,
|
||||
@inject(TYPES.Auth_DeleteSetting) override doDeleteSetting: DeleteSetting,
|
||||
@inject(TYPES.Auth_SettingHttpMapper) settingHttMapper: MapperInterface<Setting, SettingHttpRepresentation>,
|
||||
@inject(TYPES.Auth_SubscriptionSettingHttpMapper)
|
||||
subscriptionSettingHttpMapper: MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>,
|
||||
@inject(TYPES.Auth_Logger) logger: Logger,
|
||||
) {
|
||||
super(
|
||||
doGetSettings,
|
||||
doGetSetting,
|
||||
setSettingValue,
|
||||
triggerPostSettingUpdateActions,
|
||||
doDeleteSetting,
|
||||
settingHttMapper,
|
||||
subscriptionSettingHttpMapper,
|
||||
logger,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ControllerContainerInterface, MapperInterface } from '@standardnotes/do
|
||||
import { BaseHttpController, results } from 'inversify-express-utils'
|
||||
import { ErrorTag } from '@standardnotes/responses'
|
||||
import { Request, Response } from 'express'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { DeleteSetting } from '../../../Domain/UseCase/DeleteSetting/DeleteSetting'
|
||||
import { GetSetting } from '../../../Domain/UseCase/GetSetting/GetSetting'
|
||||
@@ -11,18 +12,21 @@ import { Setting } from '../../../Domain/Setting/Setting'
|
||||
import { SubscriptionSetting } from '../../../Domain/Setting/SubscriptionSetting'
|
||||
import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation'
|
||||
import { SettingHttpRepresentation } from '../../../Mapping/Http/SettingHttpRepresentation'
|
||||
import { TriggerPostSettingUpdateActions } from '../../../Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions'
|
||||
|
||||
export class BaseSettingsController extends BaseHttpController {
|
||||
constructor(
|
||||
protected doGetSettings: GetAllSettingsForUser,
|
||||
protected doGetSetting: GetSetting,
|
||||
protected setSettingValue: SetSettingValue,
|
||||
protected triggerPostSettingUpdateActions: TriggerPostSettingUpdateActions,
|
||||
protected doDeleteSetting: DeleteSetting,
|
||||
protected settingHttMapper: MapperInterface<Setting, SettingHttpRepresentation>,
|
||||
protected subscriptionSettingHttpMapper: MapperInterface<
|
||||
SubscriptionSetting,
|
||||
SubscriptionSettingHttpRepresentation
|
||||
>,
|
||||
protected logger: Logger,
|
||||
private controllerContainer?: ControllerContainerInterface,
|
||||
) {
|
||||
super()
|
||||
@@ -160,6 +164,7 @@ export class BaseSettingsController extends BaseHttpController {
|
||||
settingName: name,
|
||||
value,
|
||||
userUuid: response.locals.user.uuid,
|
||||
checkUserPermissions: true,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -174,6 +179,16 @@ export class BaseSettingsController extends BaseHttpController {
|
||||
}
|
||||
const setting = result.getValue()
|
||||
|
||||
const triggerResult = await this.triggerPostSettingUpdateActions.execute({
|
||||
updatedSettingName: setting.props.name,
|
||||
userUuid: response.locals.user.uuid,
|
||||
userEmail: response.locals.user.email,
|
||||
unencryptedValue: value,
|
||||
})
|
||||
if (triggerResult.isFailed()) {
|
||||
this.logger.error(`Failed to trigger post setting update actions: ${triggerResult.getError()}`)
|
||||
}
|
||||
|
||||
return this.json({
|
||||
success: true,
|
||||
setting: setting.props.sensitive ? undefined : this.settingHttMapper.toProjection(setting),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReadStream } from 'fs'
|
||||
import { Repository } from 'typeorm'
|
||||
|
||||
import { Setting } from '../../Domain/Setting/Setting'
|
||||
@@ -13,6 +12,36 @@ export class TypeORMSettingRepository implements SettingRepositoryInterface {
|
||||
private mapper: MapperInterface<Setting, TypeORMSetting>,
|
||||
) {}
|
||||
|
||||
async countAllByNameAndValue(dto: { name: SettingName; value: string }): Promise<number> {
|
||||
return this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.where('name = :name AND value = :value', {
|
||||
name: dto.name.value,
|
||||
value: dto.value,
|
||||
})
|
||||
.getCount()
|
||||
}
|
||||
|
||||
async findAllByNameAndValue(dto: {
|
||||
name: SettingName
|
||||
value: string
|
||||
offset: number
|
||||
limit: number
|
||||
}): Promise<Setting[]> {
|
||||
const persistence = await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.where('name = :name AND value = :value', {
|
||||
name: dto.name.value,
|
||||
value: dto.value,
|
||||
})
|
||||
.orderBy('created_at', 'ASC')
|
||||
.take(dto.limit)
|
||||
.skip(dto.offset)
|
||||
.getMany()
|
||||
|
||||
return persistence.map((p) => this.mapper.toDomain(p))
|
||||
}
|
||||
|
||||
async insert(setting: Setting): Promise<void> {
|
||||
const persistence = this.mapper.toProjection(setting)
|
||||
|
||||
@@ -42,27 +71,6 @@ export class TypeORMSettingRepository implements SettingRepositoryInterface {
|
||||
return this.mapper.toDomain(persistence)
|
||||
}
|
||||
|
||||
async streamAllByName(name: SettingName): Promise<ReadStream> {
|
||||
return this.ormRepository
|
||||
.createQueryBuilder('setting')
|
||||
.where('setting.name = :name', {
|
||||
name: name.value,
|
||||
})
|
||||
.orderBy('updated_at', 'ASC')
|
||||
.stream()
|
||||
}
|
||||
|
||||
async streamAllByNameAndValue(name: SettingName, value: string): Promise<ReadStream> {
|
||||
return this.ormRepository
|
||||
.createQueryBuilder('setting')
|
||||
.where('setting.name = :name AND setting.value = :value', {
|
||||
name: name.value,
|
||||
value,
|
||||
})
|
||||
.orderBy('updated_at', 'ASC')
|
||||
.stream()
|
||||
}
|
||||
|
||||
async findOneByUuid(uuid: string): Promise<Setting | null> {
|
||||
const persistence = await this.ormRepository
|
||||
.createQueryBuilder('setting')
|
||||
|
||||
@@ -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.20.4](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.20.3...@standardnotes/domain-events-infra@1.20.4) (2023-11-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.20.3](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.20.2...@standardnotes/domain-events-infra@1.20.3) (2023-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove open telemetry from code ([#903](https://github.com/standardnotes/server/issues/903)) ([751f3b2](https://github.com/standardnotes/server/commit/751f3b25476c5be3d663ad8540c43266acd39493))
|
||||
|
||||
## [1.20.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.20.1...@standardnotes/domain-events-infra@1.20.2) (2023-10-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.20.2",
|
||||
"version": "1.20.4",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Consumer } from 'sqs-consumer'
|
||||
import { Message, SQSClient } from '@aws-sdk/client-sqs'
|
||||
import { DomainEventSubscriberInterface, DomainEventMessageHandlerInterface } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
export class SQSDomainEventSubscriber implements DomainEventSubscriberInterface {
|
||||
constructor(
|
||||
private sqs: SQSClient,
|
||||
private queueUrl: string,
|
||||
private domainEventMessageHandler: DomainEventMessageHandlerInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
start(): void {
|
||||
const sqsConsumer = Consumer.create({
|
||||
attributeNames: ['All'],
|
||||
messageAttributeNames: ['All'],
|
||||
queueUrl: this.queueUrl,
|
||||
sqs: this.sqs,
|
||||
handleMessage: this.handleMessage.bind(this),
|
||||
})
|
||||
|
||||
sqsConsumer.on('error', this.handleError.bind(this))
|
||||
sqsConsumer.on('processing_error', this.handleError.bind(this))
|
||||
|
||||
sqsConsumer.start()
|
||||
}
|
||||
|
||||
async handleMessage(message: Message): Promise<void> {
|
||||
await this.domainEventMessageHandler.handleMessage(<string>message.Body)
|
||||
}
|
||||
|
||||
handleError(error: Error): void {
|
||||
this.logger.error('Error occured while handling SQS message: %O', error)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export * from './SNS/SNSDomainEventPublisher'
|
||||
export * from './SNS/SNSOpenTelemetryDomainEventPublisher'
|
||||
|
||||
export * from './SQS/SQSBounceNotificiationHandler'
|
||||
export * from './SQS/SQSDomainEventSubscriber'
|
||||
export * from './SQS/SQSDomainEventSubscriberFactory'
|
||||
export * from './SQS/SQSEventMessageHandler'
|
||||
export * from './SQS/SQSOpenTelemetryDomainEventSubscriber'
|
||||
|
||||
@@ -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.133.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.133.0...@standardnotes/domain-events@2.133.1) (2023-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* account deletion event ([#904](https://github.com/standardnotes/server/issues/904)) ([d66ae62](https://github.com/standardnotes/server/commit/d66ae62cf4f413cac5f6f4eac45dc0f1ddbc9e32))
|
||||
|
||||
# [2.133.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.132.3...@standardnotes/domain-events@2.133.0) (2023-10-19)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.133.0",
|
||||
"version": "2.133.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
export interface AccountDeletionRequestedEventPayload {
|
||||
userUuid: string
|
||||
email: string
|
||||
userCreatedAtTimestamp: number
|
||||
regularSubscriptionUuid: string | undefined
|
||||
regularSubscription?: {
|
||||
uuid: string
|
||||
ownerUuid: string
|
||||
}
|
||||
sharedSubscription?: {
|
||||
uuid: string
|
||||
ownerUuid: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export interface FileRemovedEventPayload {
|
||||
userUuid: string
|
||||
regularSubscriptionUuid: string
|
||||
fileByteSize: number
|
||||
filePath: string
|
||||
fileName: string
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.13.19](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.13.18...@standardnotes/event-store@1.13.19) (2023-11-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.13.18](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.13.17...@standardnotes/event-store@1.13.18) (2023-11-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.13.17](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.13.16...@standardnotes/event-store@1.13.17) (2023-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.13.17",
|
||||
"version": "1.13.19",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -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.32.5](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.32.4...@standardnotes/files-server@1.32.5) (2023-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* account deletion event ([#904](https://github.com/standardnotes/files/issues/904)) ([d66ae62](https://github.com/standardnotes/files/commit/d66ae62cf4f413cac5f6f4eac45dc0f1ddbc9e32))
|
||||
|
||||
## [1.32.4](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.32.3...@standardnotes/files-server@1.32.4) (2023-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove open telemetry from code ([#903](https://github.com/standardnotes/files/issues/903)) ([751f3b2](https://github.com/standardnotes/files/commit/751f3b25476c5be3d663ad8540c43266acd39493))
|
||||
|
||||
## [1.32.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.32.2...@standardnotes/files-server@1.32.3) (2023-10-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add fallback methods for 404 requests ([#893](https://github.com/standardnotes/files/issues/893)) ([16a6815](https://github.com/standardnotes/files/commit/16a6815b69e344573ae07682f3bac1d44d715d79))
|
||||
|
||||
## [1.32.2](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.32.1...@standardnotes/files-server@1.32.2) (2023-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user