mirror of
https://github.com/standardnotes/server
synced 2026-01-17 23:04:34 -05:00
Compare commits
34 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c99cd2e21 | ||
|
|
435cd2f66a | ||
|
|
372b12dfc2 | ||
|
|
3a12f5c1c4 | ||
|
|
781de224b6 | ||
|
|
eff09454c3 | ||
|
|
473feba6a8 | ||
|
|
e9f0704fb0 | ||
|
|
8c99469d88 | ||
|
|
8ec1311dfc | ||
|
|
e48cca6b45 | ||
|
|
d660721f95 | ||
|
|
c52bb93d79 | ||
|
|
ffb6bfd0c9 | ||
|
|
6e0855f9b3 | ||
|
|
ec9e9ec387 | ||
|
|
fa75aa40f0 | ||
|
|
b865953c22 | ||
|
|
2542cf6f9a | ||
|
|
cb9499b87f | ||
|
|
c351f01f67 | ||
|
|
c87561fca7 | ||
|
|
a363c143fa | ||
|
|
fb81d2b926 | ||
|
|
05b1b8f079 | ||
|
|
7848dc06d4 | ||
|
|
3a005719b7 | ||
|
|
6928988f78 | ||
|
|
a521894d7c | ||
|
|
b7fb1d9c08 | ||
|
|
5f67e45911 | ||
|
|
fddf9fccbd | ||
|
|
2bedbd7bd2 | ||
|
|
02f3c85796 |
2
.pnp.cjs
generated
2
.pnp.cjs
generated
@@ -2649,6 +2649,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@sentry/node", "npm:7.19.0"],\
|
||||
["@standardnotes/api", "npm:1.19.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
["@standardnotes/features", "npm:1.53.1"],\
|
||||
@@ -3055,6 +3056,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.19.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
["@standardnotes/predicates", "workspace:packages/predicates"],\
|
||||
|
||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120,
|
||||
"semi": false
|
||||
}
|
||||
@@ -3,6 +3,42 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.12.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.8...@standardnotes/analytics@2.12.9) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.7...@standardnotes/analytics@2.12.8) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.6...@standardnotes/analytics@2.12.7) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.5...@standardnotes/analytics@2.12.6) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.4...@standardnotes/analytics@2.12.5) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.3...@standardnotes/analytics@2.12.4) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.2...@standardnotes/analytics@2.12.3) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.1...@standardnotes/analytics@2.12.2) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.0...@standardnotes/analytics@2.12.1) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
# [2.12.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.17...@standardnotes/analytics@2.12.0) (2022-12-05)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.12.0",
|
||||
"version": "2.12.9",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.39.13](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.12...@standardnotes/api-gateway@1.39.13) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.39.12](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.11...@standardnotes/api-gateway@1.39.12) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.39.11](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.10...@standardnotes/api-gateway@1.39.11) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.39.10](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.9...@standardnotes/api-gateway@1.39.10) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.39.9](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.8...@standardnotes/api-gateway@1.39.9) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.39.8](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.7...@standardnotes/api-gateway@1.39.8) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.39.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.6...@standardnotes/api-gateway@1.39.7) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.39.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.5...@standardnotes/api-gateway@1.39.6) (2022-11-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.39.6",
|
||||
"version": "1.39.13",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,66 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.64.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.64.2...@standardnotes/auth-server@1.64.3) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.64.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.64.1...@standardnotes/auth-server@1.64.2) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.64.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.64.0...@standardnotes/auth-server@1.64.1) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.64.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.63.2...@standardnotes/auth-server@1.64.0) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** replace user signed in events with email requested ([e48cca6](https://github.com/standardnotes/server/commit/e48cca6b45b02876f2d82b726c1d2f124d90b587))
|
||||
|
||||
## [1.63.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.63.1...@standardnotes/auth-server@1.63.2) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.63.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.63.0...@standardnotes/auth-server@1.63.1) (2022-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** remove not needed event from factory ([2542cf6](https://github.com/standardnotes/server/commit/2542cf6f9a40c3a5eb4e11ead3cbbc25afefae48))
|
||||
|
||||
# [1.63.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.62.1...@standardnotes/auth-server@1.63.0) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-core:** rename email subscription rejection level to email level ([c87561f](https://github.com/standardnotes/server/commit/c87561fca782883b84f58b4f0b9f85ecc279ca50))
|
||||
|
||||
## [1.62.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.62.0...@standardnotes/auth-server@1.62.1) (2022-12-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** remove redundant specs and fix stream query ([fb81d2b](https://github.com/standardnotes/server/commit/fb81d2b9260cf7bee3e3e6911d5a6e8eb1d650e3))
|
||||
|
||||
# [1.62.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.61.0...@standardnotes/auth-server@1.62.0) (2022-12-06)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add procedure for email subscriptions sync ([7848dc0](https://github.com/standardnotes/server/commit/7848dc06d4f4fe8c380ed45c32e23ac0e62014fa))
|
||||
|
||||
# [1.61.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.17...@standardnotes/auth-server@1.61.0) (2022-12-06)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add publishing mute emails setting changed event ([6928988](https://github.com/standardnotes/server/commit/6928988f7855c939f2365e35cb6cb0ff18e5c37a))
|
||||
|
||||
## [1.60.17](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.16...@standardnotes/auth-server@1.60.17) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.60.16](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.15...@standardnotes/auth-server@1.60.16) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.60.15](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.14...@standardnotes/auth-server@1.60.15) (2022-11-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import 'newrelic'
|
||||
|
||||
import { Stream } from 'stream'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as utc from 'dayjs/plugin/utc'
|
||||
|
||||
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
import { SettingServiceInterface } from '../src/Domain/Setting/SettingServiceInterface'
|
||||
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../src/Domain/Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { MuteMarketingEmailsOption, SettingName } from '@standardnotes/settings'
|
||||
import { EmailMessageIdentifier } from '@standardnotes/common'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
const inputArgs = process.argv.slice(2)
|
||||
const emailMessageIdentifier = inputArgs[0]
|
||||
|
||||
const sendEmailCampaign = async (
|
||||
userRepository: UserRepositoryInterface,
|
||||
settingService: SettingServiceInterface,
|
||||
userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
timer: TimerInterface,
|
||||
domainEventFactory: DomainEventFactoryInterface,
|
||||
domainEventPublisher: DomainEventPublisherInterface,
|
||||
logger: Logger,
|
||||
): Promise<void> => {
|
||||
const stream = await userRepository.streamAll()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream
|
||||
.pipe(
|
||||
new Stream.Transform({
|
||||
objectMode: true,
|
||||
transform: async (rawUserData, _encoding, callback) => {
|
||||
try {
|
||||
const emailsMutedSetting = await settingService.findSettingWithDecryptedValue({
|
||||
userUuid: rawUserData.user_uuid,
|
||||
settingName: SettingName.MuteMarketingEmails,
|
||||
})
|
||||
|
||||
if (emailsMutedSetting === null || emailsMutedSetting.value === MuteMarketingEmailsOption.Muted) {
|
||||
callback()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let activeSubscription = false
|
||||
let subscriptionPlanName = null
|
||||
|
||||
const userSubscription = await userSubscriptionRepository.findOneByUserUuid(rawUserData.user_uuid)
|
||||
if (userSubscription !== null) {
|
||||
activeSubscription =
|
||||
!userSubscription.cancelled && userSubscription.endsAt > timer.getTimestampInMicroseconds()
|
||||
subscriptionPlanName = userSubscription.planName
|
||||
}
|
||||
|
||||
await domainEventPublisher.publish(
|
||||
domainEventFactory.createEmailMessageRequestedEvent({
|
||||
userEmail: rawUserData.user_email,
|
||||
messageIdentifier: emailMessageIdentifier as EmailMessageIdentifier,
|
||||
context: {
|
||||
activeSubscription,
|
||||
subscriptionPlanName,
|
||||
muteEmailsSettingUuid: emailsMutedSetting.uuid,
|
||||
},
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error(`Could not process user ${rawUserData.user_uuid}: ${(error as Error).message}`)
|
||||
}
|
||||
|
||||
callback()
|
||||
},
|
||||
}),
|
||||
)
|
||||
.on('finish', resolve)
|
||||
.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
void container.load().then((container) => {
|
||||
dayjs.extend(utc)
|
||||
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const logger: Logger = container.get(TYPES.Logger)
|
||||
|
||||
logger.info(`Starting email campaign for email ${emailMessageIdentifier} ...`)
|
||||
|
||||
if (!emailMessageIdentifier) {
|
||||
logger.error('No email message identifier passed as argument. Skipped sending.')
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const userRepository: UserRepositoryInterface = container.get(TYPES.UserRepository)
|
||||
const settingService: SettingServiceInterface = container.get(TYPES.SettingService)
|
||||
const userSubscriptionRepository: UserSubscriptionRepositoryInterface = container.get(
|
||||
TYPES.UserSubscriptionRepository,
|
||||
)
|
||||
const timer: TimerInterface = container.get(TYPES.Timer)
|
||||
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
|
||||
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
|
||||
|
||||
Promise.resolve(
|
||||
sendEmailCampaign(
|
||||
userRepository,
|
||||
settingService,
|
||||
userSubscriptionRepository,
|
||||
timer,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
logger,
|
||||
),
|
||||
)
|
||||
.then(() => {
|
||||
logger.info(`${emailMessageIdentifier} email campaign complete.`)
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`Could not finish ${emailMessageIdentifier} email campaign: ${error.message}`)
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
||||
@@ -5,64 +5,58 @@ COMMAND=$1 && shift 1
|
||||
|
||||
case "$COMMAND" in
|
||||
'start-local' )
|
||||
echo "Starting Web..."
|
||||
echo "[Docker] Starting Web..."
|
||||
yarn workspace @standardnotes/auth-server start:local
|
||||
;;
|
||||
|
||||
'start-web' )
|
||||
echo "Starting Web..."
|
||||
echo "[Docker] Starting Web..."
|
||||
yarn workspace @standardnotes/auth-server start
|
||||
;;
|
||||
|
||||
'start-worker' )
|
||||
echo "Starting Worker..."
|
||||
echo "[Docker] Starting Worker..."
|
||||
yarn workspace @standardnotes/auth-server worker
|
||||
;;
|
||||
|
||||
'email-daily-backup' )
|
||||
echo "Starting Email Daily Backup..."
|
||||
echo "[Docker] Starting Email Daily Backup..."
|
||||
yarn workspace @standardnotes/auth-server daily-backup:email
|
||||
;;
|
||||
|
||||
'email-weekly-backup' )
|
||||
echo "Starting Email Weekly Backup..."
|
||||
echo "[Docker] Starting Email Weekly Backup..."
|
||||
yarn workspace @standardnotes/auth-server weekly-backup:email
|
||||
;;
|
||||
|
||||
'email-backup' )
|
||||
echo "Starting Email Backup For Single User..."
|
||||
echo "[Docker] Starting Email Backup For Single User..."
|
||||
EMAIL=$1 && shift 1
|
||||
yarn workspace @standardnotes/auth-server user-email-backup $EMAIL
|
||||
;;
|
||||
|
||||
'dropbox-daily-backup' )
|
||||
echo "Starting Dropbox Daily Backup..."
|
||||
echo "[Docker] Starting Dropbox Daily Backup..."
|
||||
yarn workspace @standardnotes/auth-server daily-backup:dropbox
|
||||
;;
|
||||
|
||||
'google-drive-daily-backup' )
|
||||
echo "Starting Google Drive Daily Backup..."
|
||||
echo "[Docker] Starting Google Drive Daily Backup..."
|
||||
yarn workspace @standardnotes/auth-server daily-backup:google_drive
|
||||
;;
|
||||
|
||||
'one-drive-daily-backup' )
|
||||
echo "Starting One Drive Daily Backup..."
|
||||
echo "[Docker] Starting One Drive Daily Backup..."
|
||||
yarn workspace @standardnotes/auth-server daily-backup:one_drive
|
||||
;;
|
||||
|
||||
'email-campaign' )
|
||||
echo "Starting Email Campaign Sending..."
|
||||
MESSAGE_IDENTIFIER=$1 && shift 1
|
||||
yarn workspace @standardnotes/auth-server email-campaign $MESSAGE_IDENTIFIER
|
||||
;;
|
||||
|
||||
'content-recalculation' )
|
||||
echo "Starting Content Size Recalculation..."
|
||||
echo "[Docker] Starting Content Size Recalculation..."
|
||||
yarn workspace @standardnotes/auth-server content-recalculation
|
||||
;;
|
||||
|
||||
* )
|
||||
echo "Unknown command"
|
||||
echo "[Docker] Unknown command"
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@ module.exports = {
|
||||
transform: {
|
||||
...tsjPreset.transform,
|
||||
},
|
||||
coveragePathIgnorePatterns: ['/Bootstrap/', '/InversifyExpressUtils/', 'HealthCheckController', '/Infra/'],
|
||||
coveragePathIgnorePatterns: ['/Bootstrap/', '/InversifyExpressUtils/', '/Infra/', '/Projection/', '/Domain/Email/'],
|
||||
setupFilesAfterEnv: ['./test-setup.ts'],
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.60.15",
|
||||
"version": "1.64.3",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
@@ -26,7 +26,6 @@
|
||||
"daily-backup:one_drive": "yarn node dist/bin/backup.js one_drive daily",
|
||||
"weekly-backup:email": "yarn node dist/bin/backup.js email weekly",
|
||||
"content-recalculation": "yarn node dist/bin/content.js",
|
||||
"email-campaign": "yarn node dist/bin/email.js",
|
||||
"typeorm": "typeorm-ts-node-commonjs",
|
||||
"upgrade:snjs": "yarn ncu -u '@standardnotes/*'"
|
||||
},
|
||||
@@ -35,6 +34,7 @@
|
||||
"@sentry/node": "^7.19.0",
|
||||
"@standardnotes/api": "^1.19.0",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
"@standardnotes/features": "^1.52.1",
|
||||
|
||||
15
packages/auth/src/Domain/Email/UserSignedIn.ts
Normal file
15
packages/auth/src/Domain/Email/UserSignedIn.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { html } from './user-signed-in.html'
|
||||
|
||||
export function getSubject(email: string): string {
|
||||
return `New sign-in for ${email}`
|
||||
}
|
||||
|
||||
export function getBody(email: string, device: string, browser: string, date: Date): string {
|
||||
const body = html
|
||||
|
||||
return body
|
||||
.replace('%%EMAIL%%', email)
|
||||
.replace('%%DEVICE%%', device)
|
||||
.replace('%%BROWSER%%', browser)
|
||||
.replace('%%TIME_AND_DATE%%', date.toLocaleString())
|
||||
}
|
||||
25
packages/auth/src/Domain/Email/user-signed-in.html.ts
Normal file
25
packages/auth/src/Domain/Email/user-signed-in.html.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export const html = `
|
||||
<div>
|
||||
<p>Hello,</p>
|
||||
<p>We've detected a new sign-in to your account %%EMAIL%%.</p>
|
||||
<p>
|
||||
<b>Device type</b>: %%DEVICE%%
|
||||
</p>
|
||||
<p>
|
||||
<b>Browser type</b>: %%BROWSER%%
|
||||
</p>
|
||||
<p>
|
||||
<strong>Time and date</strong>: <span>%%TIME_AND_DATE%%</span>
|
||||
</p>
|
||||
<p>
|
||||
If this was you, please disregard this email. If it wasn't you, we recommend signing into your account and
|
||||
changing your password immediately, then enabling 2FA.
|
||||
</p>
|
||||
<p>
|
||||
Thanks,
|
||||
<br />
|
||||
SN
|
||||
</p>
|
||||
<a href="https://app.standardnotes.com/?settings=account">Mute these emails</a>
|
||||
</div>
|
||||
`
|
||||
@@ -1,6 +1,6 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
import { EmailMessageIdentifier, JSONString, ProtocolVersion, RoleName, Uuid } from '@standardnotes/common'
|
||||
import { JSONString, ProtocolVersion, RoleName, Uuid } from '@standardnotes/common'
|
||||
import {
|
||||
AccountDeletionRequestedEvent,
|
||||
UserEmailChangedEvent,
|
||||
@@ -10,16 +10,16 @@ import {
|
||||
EmailBackupRequestedEvent,
|
||||
CloudBackupRequestedEvent,
|
||||
ListedAccountRequestedEvent,
|
||||
UserSignedInEvent,
|
||||
UserDisabledSessionUserAgentLoggingEvent,
|
||||
SharedSubscriptionInvitationCreatedEvent,
|
||||
SharedSubscriptionInvitationCanceledEvent,
|
||||
PredicateVerifiedEvent,
|
||||
DomainEventService,
|
||||
EmailMessageRequestedEvent,
|
||||
WebSocketMessageRequestedEvent,
|
||||
ExitDiscountApplyRequestedEvent,
|
||||
UserContentSizeRecalculationRequestedEvent,
|
||||
MuteEmailsSettingChangedEvent,
|
||||
EmailRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
@@ -32,6 +32,25 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||
|
||||
createMuteEmailsSettingChangedEvent(dto: {
|
||||
username: string
|
||||
mute: boolean
|
||||
emailSubscriptionRejectionLevel: string
|
||||
}): MuteEmailsSettingChangedEvent {
|
||||
return {
|
||||
type: 'MUTE_EMAILS_SETTING_CHANGED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.username,
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createUserContentSizeRecalculationRequestedEvent(userUuid: string): UserContentSizeRecalculationRequestedEvent {
|
||||
return {
|
||||
type: 'USER_CONTENT_SIZE_RECALCULATION_REQUESTED',
|
||||
@@ -82,13 +101,15 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
}
|
||||
}
|
||||
|
||||
createEmailMessageRequestedEvent(dto: {
|
||||
createEmailRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
messageIdentifier: EmailMessageIdentifier
|
||||
context: Record<string, unknown>
|
||||
}): EmailMessageRequestedEvent {
|
||||
messageIdentifier: string
|
||||
level: string
|
||||
body: string
|
||||
subject: string
|
||||
}): EmailRequestedEvent {
|
||||
return {
|
||||
type: 'EMAIL_MESSAGE_REQUESTED',
|
||||
type: 'EMAIL_REQUESTED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
@@ -182,28 +203,6 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
}
|
||||
}
|
||||
|
||||
createUserSignedInEvent(dto: {
|
||||
userUuid: string
|
||||
userEmail: string
|
||||
device: string
|
||||
browser: string
|
||||
signInAlertEnabled: boolean
|
||||
muteSignInEmailsSettingUuid: Uuid
|
||||
}): UserSignedInEvent {
|
||||
return {
|
||||
type: 'USER_SIGNED_IN',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createListedAccountRequestedEvent(userUuid: string, userEmail: string): ListedAccountRequestedEvent {
|
||||
return {
|
||||
type: 'LISTED_ACCOUNT_REQUESTED',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Uuid, RoleName, EmailMessageIdentifier, ProtocolVersion, JSONString } from '@standardnotes/common'
|
||||
import { Uuid, RoleName, ProtocolVersion, JSONString } from '@standardnotes/common'
|
||||
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
||||
import {
|
||||
AccountDeletionRequestedEvent,
|
||||
@@ -9,34 +9,28 @@ import {
|
||||
OfflineSubscriptionTokenCreatedEvent,
|
||||
EmailBackupRequestedEvent,
|
||||
ListedAccountRequestedEvent,
|
||||
UserSignedInEvent,
|
||||
UserDisabledSessionUserAgentLoggingEvent,
|
||||
SharedSubscriptionInvitationCreatedEvent,
|
||||
SharedSubscriptionInvitationCanceledEvent,
|
||||
PredicateVerifiedEvent,
|
||||
EmailMessageRequestedEvent,
|
||||
WebSocketMessageRequestedEvent,
|
||||
ExitDiscountApplyRequestedEvent,
|
||||
UserContentSizeRecalculationRequestedEvent,
|
||||
MuteEmailsSettingChangedEvent,
|
||||
EmailRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createUserContentSizeRecalculationRequestedEvent(userUuid: string): UserContentSizeRecalculationRequestedEvent
|
||||
createWebSocketMessageRequestedEvent(dto: { userUuid: Uuid; message: JSONString }): WebSocketMessageRequestedEvent
|
||||
createEmailMessageRequestedEvent(dto: {
|
||||
createEmailRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
messageIdentifier: EmailMessageIdentifier
|
||||
context: Record<string, unknown>
|
||||
}): EmailMessageRequestedEvent
|
||||
createUserSignedInEvent(dto: {
|
||||
userUuid: string
|
||||
userEmail: string
|
||||
device: string
|
||||
browser: string
|
||||
signInAlertEnabled: boolean
|
||||
muteSignInEmailsSettingUuid: Uuid
|
||||
}): UserSignedInEvent
|
||||
messageIdentifier: string
|
||||
level: string
|
||||
body: string
|
||||
subject: string
|
||||
}): EmailRequestedEvent
|
||||
createListedAccountRequestedEvent(userUuid: string, userEmail: string): ListedAccountRequestedEvent
|
||||
createUserRegisteredEvent(dto: {
|
||||
userUuid: string
|
||||
@@ -91,4 +85,9 @@ export interface DomainEventFactoryInterface {
|
||||
userEmail: string
|
||||
discountCode: string
|
||||
}): ExitDiscountApplyRequestedEvent
|
||||
createMuteEmailsSettingChangedEvent(dto: {
|
||||
username: string
|
||||
mute: boolean
|
||||
emailSubscriptionRejectionLevel: string
|
||||
}): MuteEmailsSettingChangedEvent
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ import {
|
||||
CloudBackupRequestedEvent,
|
||||
DomainEventPublisherInterface,
|
||||
EmailBackupRequestedEvent,
|
||||
MuteEmailsSettingChangedEvent,
|
||||
UserDisabledSessionUserAgentLoggingEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import {
|
||||
EmailBackupFrequency,
|
||||
LogSessionUserAgentOption,
|
||||
MuteMarketingEmailsOption,
|
||||
OneDriveBackupFrequency,
|
||||
SettingName,
|
||||
} from '@standardnotes/settings'
|
||||
@@ -57,6 +59,9 @@ describe('SettingInterpreter', () => {
|
||||
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()
|
||||
@@ -201,6 +206,23 @@ describe('SettingInterpreter', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should trigger mute subscription emails rejection if mute setting changed', async () => {
|
||||
const setting = {
|
||||
name: SettingName.MuteMarketingEmails,
|
||||
value: MuteMarketingEmailsOption.Muted,
|
||||
} as jest.Mocked<Setting>
|
||||
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createInterpreter().interpretSettingUpdated(setting, user, MuteMarketingEmailsOption.Muted)
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
expect(domainEventFactory.createMuteEmailsSettingChangedEvent).toHaveBeenCalledWith({
|
||||
emailSubscriptionRejectionLevel: 'MARKETING',
|
||||
mute: true,
|
||||
username: 'test@test.te',
|
||||
})
|
||||
})
|
||||
|
||||
it('should trigger cloud backup if backup frequency setting is updated and a backup token setting is present', async () => {
|
||||
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce({
|
||||
name: SettingName.OneDriveBackupToken,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { EmailLevel } from '@standardnotes/domain-core'
|
||||
import {
|
||||
DropboxBackupFrequency,
|
||||
EmailBackupFrequency,
|
||||
@@ -39,6 +40,13 @@ export class SettingInterpreter implements SettingInterpreterInterface {
|
||||
OneDriveBackupFrequency.Disabled,
|
||||
]
|
||||
|
||||
private readonly emailSettingToSubscriptionRejectionLevelMap: Map<SettingName, string> = new Map([
|
||||
[SettingName.MuteFailedBackupsEmails, EmailLevel.LEVELS.FailedEmailBackup],
|
||||
[SettingName.MuteFailedCloudBackupsEmails, EmailLevel.LEVELS.FailedCloudBackup],
|
||||
[SettingName.MuteMarketingEmails, EmailLevel.LEVELS.Marketing],
|
||||
[SettingName.MuteSignInEmails, EmailLevel.LEVELS.SignIn],
|
||||
])
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@@ -48,6 +56,10 @@ export class SettingInterpreter implements SettingInterpreterInterface {
|
||||
) {}
|
||||
|
||||
async interpretSettingUpdated(updatedSetting: Setting, user: User, unencryptedValue: string | null): Promise<void> {
|
||||
if (this.isChangingMuteEmailsSetting(updatedSetting)) {
|
||||
await this.triggerEmailSubscriptionChange(user, updatedSetting.name as SettingName, unencryptedValue)
|
||||
}
|
||||
|
||||
if (this.isEnablingEmailBackupSetting(updatedSetting)) {
|
||||
await this.triggerEmailBackup(user.uuid)
|
||||
}
|
||||
@@ -78,6 +90,15 @@ export class SettingInterpreter implements SettingInterpreterInterface {
|
||||
)
|
||||
}
|
||||
|
||||
private isChangingMuteEmailsSetting(setting: Setting): boolean {
|
||||
return [
|
||||
SettingName.MuteFailedBackupsEmails,
|
||||
SettingName.MuteFailedCloudBackupsEmails,
|
||||
SettingName.MuteMarketingEmails,
|
||||
SettingName.MuteSignInEmails,
|
||||
].includes(setting.name as SettingName)
|
||||
}
|
||||
|
||||
private isEnablingEmailBackupSetting(setting: Setting): boolean {
|
||||
return setting.name === SettingName.EmailBackupFrequency && setting.value !== EmailBackupFrequency.Disabled
|
||||
}
|
||||
@@ -96,6 +117,20 @@ export class SettingInterpreter implements SettingInterpreterInterface {
|
||||
return SettingName.LogSessionUserAgent === setting.name && LogSessionUserAgentOption.Disabled === setting.value
|
||||
}
|
||||
|
||||
private async triggerEmailSubscriptionChange(
|
||||
user: User,
|
||||
settingName: SettingName,
|
||||
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({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { DomainEventPublisherInterface, UserSignedInEvent } from '@standardnotes/domain-events'
|
||||
import { DomainEventPublisherInterface, EmailRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { AuthResponseFactoryInterface } from '../Auth/AuthResponseFactoryInterface'
|
||||
@@ -10,10 +10,6 @@ import { SessionServiceInterface } from '../Session/SessionServiceInterface'
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { SignIn } from './SignIn'
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { Setting } from '../Setting/Setting'
|
||||
import { MuteSignInEmailsOption } from '@standardnotes/settings'
|
||||
import { PKCERepositoryInterface } from '../User/PKCERepositoryInterface'
|
||||
import { CrypterInterface } from '../Encryption/CrypterInterface'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
@@ -26,10 +22,7 @@ describe('SignIn', () => {
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let sessionService: SessionServiceInterface
|
||||
let roleService: RoleServiceInterface
|
||||
let logger: Logger
|
||||
let settingService: SettingServiceInterface
|
||||
let setting: Setting
|
||||
let pkceRepository: PKCERepositoryInterface
|
||||
let crypter: CrypterInterface
|
||||
|
||||
@@ -40,8 +33,6 @@ describe('SignIn', () => {
|
||||
domainEventPublisher,
|
||||
domainEventFactory,
|
||||
sessionService,
|
||||
roleService,
|
||||
settingService,
|
||||
pkceRepository,
|
||||
crypter,
|
||||
logger,
|
||||
@@ -68,27 +59,12 @@ describe('SignIn', () => {
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createUserSignedInEvent = jest.fn().mockReturnValue({} as jest.Mocked<UserSignedInEvent>)
|
||||
domainEventFactory.createEmailRequestedEvent = jest.fn().mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
|
||||
|
||||
sessionService = {} as jest.Mocked<SessionServiceInterface>
|
||||
sessionService.getOperatingSystemInfoFromUserAgent = jest.fn().mockReturnValue('iOS 1')
|
||||
sessionService.getBrowserInfoFromUserAgent = jest.fn().mockReturnValue('Firefox 1')
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.userHasPermission = jest.fn().mockReturnValue(true)
|
||||
|
||||
setting = {
|
||||
uuid: '3-4-5',
|
||||
value: MuteSignInEmailsOption.NotMuted,
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
settingService.createOrReplace = jest.fn().mockReturnValue({
|
||||
status: 'created',
|
||||
setting,
|
||||
})
|
||||
|
||||
pkceRepository = {} as jest.Mocked<PKCERepositoryInterface>
|
||||
pkceRepository.removeCodeChallenge = jest.fn().mockReturnValue(true)
|
||||
|
||||
@@ -118,18 +94,33 @@ describe('SignIn', () => {
|
||||
authResponse: { foo: 'bar' },
|
||||
})
|
||||
|
||||
expect(domainEventFactory.createUserSignedInEvent).toHaveBeenCalledWith({
|
||||
browser: 'Firefox 1',
|
||||
device: 'iOS 1',
|
||||
userEmail: 'test@test.com',
|
||||
userUuid: '1-2-3',
|
||||
signInAlertEnabled: true,
|
||||
muteSignInEmailsSettingUuid: '3-4-5',
|
||||
})
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not sign in a user without code verifier', async () => {
|
||||
it('should not sign in 004 user without code verifier', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
email: 'test@test.te',
|
||||
password: 'qweqwe123123',
|
||||
userAgent: 'Google Chrome',
|
||||
apiVersion: '20190520',
|
||||
ephemeralSession: false,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorCode: 410,
|
||||
errorMessage: 'Please update your client application.',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not sign in 005 user without code verifier', async () => {
|
||||
user = {
|
||||
uuid: '1-2-3',
|
||||
email: 'test@test.com',
|
||||
version: '005',
|
||||
} as jest.Mocked<User>
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
email: 'test@test.te',
|
||||
@@ -160,92 +151,10 @@ describe('SignIn', () => {
|
||||
authResponse: { foo: 'bar' },
|
||||
})
|
||||
|
||||
expect(domainEventFactory.createUserSignedInEvent).toHaveBeenCalledWith({
|
||||
browser: 'Firefox 1',
|
||||
device: 'iOS 1',
|
||||
userEmail: 'test@test.com',
|
||||
userUuid: '1-2-3',
|
||||
signInAlertEnabled: true,
|
||||
muteSignInEmailsSettingUuid: '3-4-5',
|
||||
})
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should sign in a user and disable sign in alert if setting is configured', async () => {
|
||||
setting = {
|
||||
uuid: '3-4-5',
|
||||
value: MuteSignInEmailsOption.Muted,
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
email: 'test@test.te',
|
||||
password: 'qweqwe123123',
|
||||
userAgent: 'Google Chrome',
|
||||
apiVersion: '20190520',
|
||||
ephemeralSession: false,
|
||||
codeVerifier: 'test',
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
authResponse: { foo: 'bar' },
|
||||
})
|
||||
|
||||
expect(domainEventFactory.createUserSignedInEvent).toHaveBeenCalledWith({
|
||||
browser: 'Firefox 1',
|
||||
device: 'iOS 1',
|
||||
userEmail: 'test@test.com',
|
||||
userUuid: '1-2-3',
|
||||
signInAlertEnabled: false,
|
||||
muteSignInEmailsSettingUuid: '3-4-5',
|
||||
})
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should sign in a user and create mute sign in email setting if it does not exist', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
email: 'test@test.te',
|
||||
password: 'qweqwe123123',
|
||||
userAgent: 'Google Chrome',
|
||||
apiVersion: '20190520',
|
||||
ephemeralSession: false,
|
||||
codeVerifier: 'test',
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
authResponse: { foo: 'bar' },
|
||||
})
|
||||
|
||||
expect(domainEventFactory.createUserSignedInEvent).toHaveBeenCalledWith({
|
||||
browser: 'Firefox 1',
|
||||
device: 'iOS 1',
|
||||
userEmail: 'test@test.com',
|
||||
userUuid: '1-2-3',
|
||||
signInAlertEnabled: true,
|
||||
muteSignInEmailsSettingUuid: '3-4-5',
|
||||
})
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
expect(settingService.createOrReplace).toHaveBeenCalledWith({
|
||||
props: {
|
||||
name: 'MUTE_SIGN_IN_EMAILS',
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: 0,
|
||||
unencryptedValue: 'not_muted',
|
||||
},
|
||||
user: {
|
||||
email: 'test@test.com',
|
||||
encryptedPassword: '$2a$11$K3g6XoTau8VmLJcai1bB0eD9/YvBSBRtBhMprJOaVZ0U3SgasZH3a',
|
||||
uuid: '1-2-3',
|
||||
version: '004',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should sign in a user even if publishing a sign in event fails', async () => {
|
||||
domainEventPublisher.publish = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import * as bcrypt from 'bcryptjs'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
import { MuteSignInEmailsOption, SettingName } from '@standardnotes/settings'
|
||||
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AuthResponseFactoryResolverInterface } from '../Auth/AuthResponseFactoryResolverInterface'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
|
||||
import { Setting } from '../Setting/Setting'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { SignInDTO } from './SignInDTO'
|
||||
@@ -21,8 +15,10 @@ import { UseCaseInterface } from './UseCaseInterface'
|
||||
import { PKCERepositoryInterface } from '../User/PKCERepositoryInterface'
|
||||
import { CrypterInterface } from '../Encryption/CrypterInterface'
|
||||
import { SignInDTOV2Challenged } from './SignInDTOV2Challenged'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { leftVersionGreaterThanOrEqualToRight, ProtocolVersion } from '@standardnotes/common'
|
||||
import { HttpStatusCode } from '@standardnotes/api'
|
||||
import { EmailLevel } from '@standardnotes/domain-core'
|
||||
import { getBody, getSubject } from '../Email/UserSignedIn'
|
||||
|
||||
@injectable()
|
||||
export class SignIn implements UseCaseInterface {
|
||||
@@ -33,8 +29,6 @@ export class SignIn implements UseCaseInterface {
|
||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.SessionService) private sessionService: SessionServiceInterface,
|
||||
@inject(TYPES.RoleService) private roleService: RoleServiceInterface,
|
||||
@inject(TYPES.SettingService) private settingService: SettingServiceInterface,
|
||||
@inject(TYPES.PKCERepository) private pkceRepository: PKCERepositoryInterface,
|
||||
@inject(TYPES.Crypter) private crypter: CrypterInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
@@ -65,7 +59,12 @@ export class SignIn implements UseCaseInterface {
|
||||
}
|
||||
}
|
||||
|
||||
if (user.version === ProtocolVersion.V004 && !performingCodeChallengedSignIn) {
|
||||
const userVersionIs004OrGreater = leftVersionGreaterThanOrEqualToRight(
|
||||
user.version as ProtocolVersion,
|
||||
ProtocolVersion.V004,
|
||||
)
|
||||
|
||||
if (userVersionIs004OrGreater && !performingCodeChallengedSignIn) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: 'Please update your client application.',
|
||||
@@ -109,18 +108,18 @@ export class SignIn implements UseCaseInterface {
|
||||
|
||||
private async sendSignInEmailNotification(user: User, userAgent: string): Promise<void> {
|
||||
try {
|
||||
const muteSignInEmailsSetting = await this.findOrCreateMuteSignInEmailsSetting(user)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createUserSignedInEvent({
|
||||
userUuid: user.uuid,
|
||||
this.domainEventFactory.createEmailRequestedEvent({
|
||||
userEmail: user.email,
|
||||
device: this.sessionService.getOperatingSystemInfoFromUserAgent(userAgent),
|
||||
browser: this.sessionService.getBrowserInfoFromUserAgent(userAgent),
|
||||
signInAlertEnabled:
|
||||
(await this.roleService.userHasPermission(user.uuid, PermissionName.SignInAlerts)) &&
|
||||
muteSignInEmailsSetting.value === MuteSignInEmailsOption.NotMuted,
|
||||
muteSignInEmailsSettingUuid: muteSignInEmailsSetting.uuid,
|
||||
level: EmailLevel.LEVELS.SignIn,
|
||||
body: getBody(
|
||||
user.email,
|
||||
this.sessionService.getOperatingSystemInfoFromUserAgent(userAgent),
|
||||
this.sessionService.getBrowserInfoFromUserAgent(userAgent),
|
||||
new Date(),
|
||||
),
|
||||
messageIdentifier: 'SIGN_IN',
|
||||
subject: getSubject(user.email),
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
@@ -128,29 +127,6 @@ export class SignIn implements UseCaseInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private async findOrCreateMuteSignInEmailsSetting(user: User): Promise<Setting> {
|
||||
const existingMuteSignInEmailsSetting = await this.settingService.findSettingWithDecryptedValue({
|
||||
userUuid: user.uuid,
|
||||
settingName: SettingName.MuteSignInEmails,
|
||||
})
|
||||
|
||||
if (existingMuteSignInEmailsSetting !== null) {
|
||||
return existingMuteSignInEmailsSetting
|
||||
}
|
||||
|
||||
const createSettingResult = await this.settingService.createOrReplace({
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.MuteSignInEmails,
|
||||
sensitive: false,
|
||||
unencryptedValue: MuteSignInEmailsOption.NotMuted,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
},
|
||||
})
|
||||
|
||||
return createSettingResult.setting
|
||||
}
|
||||
|
||||
private isCodeChallengedVersion(dto: SignInDTO): dto is SignInDTOV2Challenged {
|
||||
return (dto as SignInDTOV2Challenged).codeVerifier !== undefined
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Repository, SelectQueryBuilder } from 'typeorm'
|
||||
import { OfflineSetting } from '../../Domain/Setting/OfflineSetting'
|
||||
import { OfflineSettingName } from '../../Domain/Setting/OfflineSettingName'
|
||||
|
||||
import { MySQLOfflineSettingRepository } from './MySQLOfflineSettingRepository'
|
||||
|
||||
describe('MySQLOfflineSettingRepository', () => {
|
||||
let queryBuilder: SelectQueryBuilder<OfflineSetting>
|
||||
let offlineSetting: OfflineSetting
|
||||
let ormRepository: Repository<OfflineSetting>
|
||||
|
||||
const createRepository = () => new MySQLOfflineSettingRepository(ormRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<OfflineSetting>>
|
||||
|
||||
offlineSetting = {} as jest.Mocked<OfflineSetting>
|
||||
|
||||
ormRepository = {} as jest.Mocked<Repository<OfflineSetting>>
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
|
||||
ormRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should save', async () => {
|
||||
await createRepository().save(offlineSetting)
|
||||
|
||||
expect(ormRepository.save).toHaveBeenCalledWith(offlineSetting)
|
||||
})
|
||||
|
||||
it('should find one setting by name and user email', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(offlineSetting)
|
||||
|
||||
const result = await createRepository().findOneByNameAndEmail(OfflineSettingName.FeaturesToken, 'test@test.com')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('offline_setting.name = :name AND offline_setting.email = :email', {
|
||||
name: 'FEATURES_TOKEN',
|
||||
email: 'test@test.com',
|
||||
})
|
||||
expect(result).toEqual(offlineSetting)
|
||||
})
|
||||
|
||||
it('should find one setting by name and value', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(offlineSetting)
|
||||
|
||||
const result = await createRepository().findOneByNameAndValue(OfflineSettingName.FeaturesToken, 'features-token')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('offline_setting.name = :name AND offline_setting.value = :value', {
|
||||
name: 'FEATURES_TOKEN',
|
||||
value: 'features-token',
|
||||
})
|
||||
expect(result).toEqual(offlineSetting)
|
||||
})
|
||||
})
|
||||
@@ -1,189 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
|
||||
import { Repository, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm'
|
||||
|
||||
import { MySQLOfflineUserSubscriptionRepository } from './MySQLOfflineUserSubscriptionRepository'
|
||||
import { OfflineUserSubscription } from '../../Domain/Subscription/OfflineUserSubscription'
|
||||
|
||||
describe('MySQLOfflineUserSubscriptionRepository', () => {
|
||||
let selectQueryBuilder: SelectQueryBuilder<OfflineUserSubscription>
|
||||
let updateQueryBuilder: UpdateQueryBuilder<OfflineUserSubscription>
|
||||
let offlineSubscription: OfflineUserSubscription
|
||||
let ormRepository: Repository<OfflineUserSubscription>
|
||||
|
||||
const createRepository = () => new MySQLOfflineUserSubscriptionRepository(ormRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
selectQueryBuilder = {} as jest.Mocked<SelectQueryBuilder<OfflineUserSubscription>>
|
||||
updateQueryBuilder = {} as jest.Mocked<UpdateQueryBuilder<OfflineUserSubscription>>
|
||||
|
||||
offlineSubscription = {
|
||||
planName: SubscriptionName.ProPlan,
|
||||
cancelled: false,
|
||||
email: 'test@test.com',
|
||||
} as jest.Mocked<OfflineUserSubscription>
|
||||
|
||||
ormRepository = {} as jest.Mocked<Repository<OfflineUserSubscription>>
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
ormRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should save', async () => {
|
||||
await createRepository().save(offlineSubscription)
|
||||
|
||||
expect(ormRepository.save).toHaveBeenCalledWith(offlineSubscription)
|
||||
})
|
||||
|
||||
it('should find one longest lasting uncanceled subscription by user email if there are canceled ones', async () => {
|
||||
const canceledSubscription = {
|
||||
planName: SubscriptionName.ProPlan,
|
||||
cancelled: true,
|
||||
email: 'test@test.com',
|
||||
} as jest.Mocked<OfflineUserSubscription>
|
||||
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getMany = jest.fn().mockReturnValue([canceledSubscription, offlineSubscription])
|
||||
|
||||
const result = await createRepository().findOneByEmail('test@test.com')
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('email = :email', {
|
||||
email: 'test@test.com',
|
||||
})
|
||||
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
|
||||
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
|
||||
expect(result).toEqual(offlineSubscription)
|
||||
})
|
||||
|
||||
it('should find one, longest lasting subscription by user email if there are no canceled ones', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getMany = jest.fn().mockReturnValue([offlineSubscription])
|
||||
|
||||
const result = await createRepository().findOneByEmail('test@test.com')
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('email = :email', {
|
||||
email: 'test@test.com',
|
||||
})
|
||||
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
|
||||
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
|
||||
expect(result).toEqual(offlineSubscription)
|
||||
})
|
||||
|
||||
it('should find one, longest lasting subscription by user email if there are no ucanceled ones', async () => {
|
||||
offlineSubscription.cancelled = true
|
||||
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getMany = jest.fn().mockReturnValue([offlineSubscription])
|
||||
|
||||
const result = await createRepository().findOneByEmail('test@test.com')
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('email = :email', {
|
||||
email: 'test@test.com',
|
||||
})
|
||||
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
|
||||
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
|
||||
expect(result).toEqual(offlineSubscription)
|
||||
})
|
||||
|
||||
it('should find none if there are no subscriptions for the user', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getMany = jest.fn().mockReturnValue([])
|
||||
|
||||
const result = await createRepository().findOneByEmail('test@test.com')
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('email = :email', {
|
||||
email: 'test@test.com',
|
||||
})
|
||||
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
|
||||
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should find multiple by user email active after', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getMany = jest.fn().mockReturnValue([offlineSubscription])
|
||||
|
||||
const result = await createRepository().findByEmail('test@test.com', 123)
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('email = :email AND ends_at > :endsAt', {
|
||||
email: 'test@test.com',
|
||||
endsAt: 123,
|
||||
})
|
||||
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
|
||||
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
|
||||
expect(result).toEqual([offlineSubscription])
|
||||
})
|
||||
|
||||
it('should update cancelled by subscription id', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
|
||||
|
||||
updateQueryBuilder.update = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.set = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.execute = jest.fn()
|
||||
|
||||
await createRepository().updateCancelled(1, true, 1000)
|
||||
|
||||
expect(updateQueryBuilder.update).toHaveBeenCalled()
|
||||
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
|
||||
updatedAt: expect.any(Number),
|
||||
cancelled: true,
|
||||
})
|
||||
expect(updateQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
|
||||
subscriptionId: 1,
|
||||
})
|
||||
expect(updateQueryBuilder.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update ends at by subscription id', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
|
||||
|
||||
updateQueryBuilder.update = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.set = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.execute = jest.fn()
|
||||
|
||||
await createRepository().updateEndsAt(1, 1000, 1000)
|
||||
|
||||
expect(updateQueryBuilder.update).toHaveBeenCalled()
|
||||
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
|
||||
updatedAt: expect.any(Number),
|
||||
endsAt: 1000,
|
||||
})
|
||||
expect(updateQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
|
||||
subscriptionId: 1,
|
||||
})
|
||||
expect(updateQueryBuilder.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should find one offline user subscription by user subscription id', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getOne = jest.fn().mockReturnValue(offlineSubscription)
|
||||
|
||||
const result = await createRepository().findOneBySubscriptionId(123)
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
|
||||
subscriptionId: 123,
|
||||
})
|
||||
expect(selectQueryBuilder.getOne).toHaveBeenCalled()
|
||||
expect(result).toEqual(offlineSubscription)
|
||||
})
|
||||
})
|
||||
@@ -1,78 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Repository, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm'
|
||||
|
||||
import { RevokedSession } from '../../Domain/Session/RevokedSession'
|
||||
|
||||
import { MySQLRevokedSessionRepository } from './MySQLRevokedSessionRepository'
|
||||
|
||||
describe('MySQLRevokedSessionRepository', () => {
|
||||
let ormRepository: Repository<RevokedSession>
|
||||
let queryBuilder: SelectQueryBuilder<RevokedSession>
|
||||
let updateQueryBuilder: UpdateQueryBuilder<RevokedSession>
|
||||
let session: RevokedSession
|
||||
|
||||
const createRepository = () => new MySQLRevokedSessionRepository(ormRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<RevokedSession>>
|
||||
updateQueryBuilder = {} as jest.Mocked<UpdateQueryBuilder<RevokedSession>>
|
||||
|
||||
session = {} as jest.Mocked<RevokedSession>
|
||||
|
||||
ormRepository = {} as jest.Mocked<Repository<RevokedSession>>
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
|
||||
ormRepository.save = jest.fn()
|
||||
ormRepository.remove = jest.fn()
|
||||
})
|
||||
|
||||
it('should save', async () => {
|
||||
await createRepository().save(session)
|
||||
|
||||
expect(ormRepository.save).toHaveBeenCalledWith(session)
|
||||
})
|
||||
|
||||
it('should remove', async () => {
|
||||
await createRepository().remove(session)
|
||||
|
||||
expect(ormRepository.remove).toHaveBeenCalledWith(session)
|
||||
})
|
||||
|
||||
it('should clear user agent data on all user sessions', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
|
||||
|
||||
updateQueryBuilder.update = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.set = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.execute = jest.fn()
|
||||
|
||||
await createRepository().clearUserAgentByUserUuid('1-2-3')
|
||||
|
||||
expect(updateQueryBuilder.update).toHaveBeenCalled()
|
||||
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
|
||||
userAgent: null,
|
||||
})
|
||||
expect(updateQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :userUuid', { userUuid: '1-2-3' })
|
||||
expect(updateQueryBuilder.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should find one session by id', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(session)
|
||||
|
||||
const result = await createRepository().findOneByUuid('123')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('revoked_session.uuid = :uuid', { uuid: '123' })
|
||||
expect(result).toEqual(session)
|
||||
})
|
||||
|
||||
it('should find all revoked sessions by user id', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getMany = jest.fn().mockReturnValue([session])
|
||||
|
||||
const result = await createRepository().findAllByUserUuid('123')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('revoked_session.user_uuid = :user_uuid', { user_uuid: '123' })
|
||||
expect(result).toEqual([session])
|
||||
})
|
||||
})
|
||||
@@ -1,49 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Repository, SelectQueryBuilder } from 'typeorm'
|
||||
import { Role } from '../../Domain/Role/Role'
|
||||
|
||||
import { MySQLRoleRepository } from './MySQLRoleRepository'
|
||||
|
||||
describe('MySQLRoleRepository', () => {
|
||||
let ormRepository: Repository<Role>
|
||||
let queryBuilder: SelectQueryBuilder<Role>
|
||||
let role: Role
|
||||
|
||||
const createRepository = () => new MySQLRoleRepository(ormRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<Role>>
|
||||
queryBuilder.cache = jest.fn().mockReturnThis()
|
||||
|
||||
role = {} as jest.Mocked<Role>
|
||||
|
||||
ormRepository = {} as jest.Mocked<Repository<Role>>
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
|
||||
})
|
||||
|
||||
it('should find latest version of a role by name', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.take = jest.fn().mockReturnThis()
|
||||
queryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
queryBuilder.getMany = jest.fn().mockReturnValue([role])
|
||||
|
||||
const result = await createRepository().findOneByName('test')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('role.name = :name', { name: 'test' })
|
||||
expect(queryBuilder.take).toHaveBeenCalledWith(1)
|
||||
expect(queryBuilder.orderBy).toHaveBeenCalledWith('version', 'DESC')
|
||||
expect(result).toEqual(role)
|
||||
})
|
||||
|
||||
it('should return null if not found the latest version of a role by name', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.take = jest.fn().mockReturnThis()
|
||||
queryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
queryBuilder.getMany = jest.fn().mockReturnValue([])
|
||||
|
||||
const result = await createRepository().findOneByName('test')
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,174 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import * as dayjs from 'dayjs'
|
||||
|
||||
import { Repository, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm'
|
||||
import { Session } from '../../Domain/Session/Session'
|
||||
|
||||
import { MySQLSessionRepository } from './MySQLSessionRepository'
|
||||
|
||||
describe('MySQLSessionRepository', () => {
|
||||
let ormRepository: Repository<Session>
|
||||
let queryBuilder: SelectQueryBuilder<Session>
|
||||
let updateQueryBuilder: UpdateQueryBuilder<Session>
|
||||
let session: Session
|
||||
|
||||
const createRepository = () => new MySQLSessionRepository(ormRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<Session>>
|
||||
updateQueryBuilder = {} as jest.Mocked<UpdateQueryBuilder<Session>>
|
||||
|
||||
session = {} as jest.Mocked<Session>
|
||||
|
||||
ormRepository = {} as jest.Mocked<Repository<Session>>
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
|
||||
ormRepository.save = jest.fn()
|
||||
ormRepository.remove = jest.fn()
|
||||
})
|
||||
|
||||
it('should save', async () => {
|
||||
await createRepository().save(session)
|
||||
|
||||
expect(ormRepository.save).toHaveBeenCalledWith(session)
|
||||
})
|
||||
|
||||
it('should remove', async () => {
|
||||
await createRepository().remove(session)
|
||||
|
||||
expect(ormRepository.remove).toHaveBeenCalledWith(session)
|
||||
})
|
||||
|
||||
it('should clear user agent data on all user sessions', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
|
||||
|
||||
updateQueryBuilder.update = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.set = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.execute = jest.fn()
|
||||
|
||||
await createRepository().clearUserAgentByUserUuid('1-2-3')
|
||||
|
||||
expect(updateQueryBuilder.update).toHaveBeenCalled()
|
||||
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
|
||||
userAgent: null,
|
||||
})
|
||||
expect(updateQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :userUuid', { userUuid: '1-2-3' })
|
||||
expect(updateQueryBuilder.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update hashed tokens on a session', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
|
||||
|
||||
updateQueryBuilder.update = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.set = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.execute = jest.fn()
|
||||
|
||||
await createRepository().updateHashedTokens('123', '234', '345')
|
||||
|
||||
expect(updateQueryBuilder.update).toHaveBeenCalled()
|
||||
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
|
||||
hashedAccessToken: '234',
|
||||
hashedRefreshToken: '345',
|
||||
})
|
||||
expect(updateQueryBuilder.where).toHaveBeenCalledWith('uuid = :uuid', { uuid: '123' })
|
||||
expect(updateQueryBuilder.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update token expiration dates on a session', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
|
||||
|
||||
updateQueryBuilder.update = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.set = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.execute = jest.fn()
|
||||
|
||||
await createRepository().updatedTokenExpirationDates(
|
||||
'123',
|
||||
dayjs.utc('2020-11-26 13:34').toDate(),
|
||||
dayjs.utc('2020-11-26 14:34').toDate(),
|
||||
)
|
||||
|
||||
expect(updateQueryBuilder.update).toHaveBeenCalled()
|
||||
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
|
||||
accessExpiration: dayjs.utc('2020-11-26T13:34:00.000Z').toDate(),
|
||||
refreshExpiration: dayjs.utc('2020-11-26T14:34:00.000Z').toDate(),
|
||||
})
|
||||
expect(updateQueryBuilder.where).toHaveBeenCalledWith('uuid = :uuid', { uuid: '123' })
|
||||
expect(updateQueryBuilder.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should find active sessions by user id', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getMany = jest.fn().mockReturnValue([session])
|
||||
|
||||
const result = await createRepository().findAllByRefreshExpirationAndUserUuid('123')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith(
|
||||
'session.refresh_expiration > :refresh_expiration AND session.user_uuid = :user_uuid',
|
||||
{ refresh_expiration: expect.any(Date), user_uuid: '123' },
|
||||
)
|
||||
expect(result).toEqual([session])
|
||||
})
|
||||
|
||||
it('should find all sessions by user id', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getMany = jest.fn().mockReturnValue([session])
|
||||
|
||||
const result = await createRepository().findAllByUserUuid('123')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('session.user_uuid = :user_uuid', { user_uuid: '123' })
|
||||
expect(result).toEqual([session])
|
||||
})
|
||||
|
||||
it('should find one session by id', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(session)
|
||||
|
||||
const result = await createRepository().findOneByUuid('123')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('session.uuid = :uuid', { uuid: '123' })
|
||||
expect(result).toEqual(session)
|
||||
})
|
||||
|
||||
it('should find one session by id and user id', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(session)
|
||||
|
||||
const result = await createRepository().findOneByUuidAndUserUuid('123', '234')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('session.uuid = :uuid AND session.user_uuid = :user_uuid', {
|
||||
uuid: '123',
|
||||
user_uuid: '234',
|
||||
})
|
||||
expect(result).toEqual(session)
|
||||
})
|
||||
|
||||
it('should delete all session for a user except the current one', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.delete = jest.fn().mockReturnThis()
|
||||
queryBuilder.execute = jest.fn()
|
||||
|
||||
await createRepository().deleteAllByUserUuid('123', '234')
|
||||
|
||||
expect(queryBuilder.delete).toHaveBeenCalled()
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid AND uuid != :current_session_uuid', {
|
||||
user_uuid: '123',
|
||||
current_session_uuid: '234',
|
||||
})
|
||||
expect(queryBuilder.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should delete one session by id', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.delete = jest.fn().mockReturnThis()
|
||||
queryBuilder.execute = jest.fn()
|
||||
|
||||
await createRepository().deleteOneByUuid('123')
|
||||
|
||||
expect(queryBuilder.delete).toHaveBeenCalled()
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('uuid = :uuid', { uuid: '123' })
|
||||
expect(queryBuilder.execute).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,140 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { ReadStream } from 'fs'
|
||||
import { Repository, SelectQueryBuilder } from 'typeorm'
|
||||
import { Setting } from '../../Domain/Setting/Setting'
|
||||
|
||||
import { MySQLSettingRepository } from './MySQLSettingRepository'
|
||||
import { EmailBackupFrequency, SettingName } from '@standardnotes/settings'
|
||||
|
||||
describe('MySQLSettingRepository', () => {
|
||||
let ormRepository: Repository<Setting>
|
||||
let queryBuilder: SelectQueryBuilder<Setting>
|
||||
let setting: Setting
|
||||
|
||||
const createRepository = () => new MySQLSettingRepository(ormRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<Setting>>
|
||||
|
||||
setting = {} as jest.Mocked<Setting>
|
||||
|
||||
ormRepository = {} as jest.Mocked<Repository<Setting>>
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
|
||||
ormRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should save', async () => {
|
||||
await createRepository().save(setting)
|
||||
|
||||
expect(ormRepository.save).toHaveBeenCalledWith(setting)
|
||||
})
|
||||
|
||||
it('should stream all settings by name and value', async () => {
|
||||
const stream = {} as jest.Mocked<ReadStream>
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
queryBuilder.stream = jest.fn().mockReturnValue(stream)
|
||||
|
||||
const result = await createRepository().streamAllByNameAndValue(
|
||||
SettingName.EmailBackupFrequency,
|
||||
EmailBackupFrequency.Daily,
|
||||
)
|
||||
|
||||
expect(result).toEqual(stream)
|
||||
})
|
||||
|
||||
it('should find one setting by uuid', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(setting)
|
||||
|
||||
const result = await createRepository().findOneByUuid('1-2-3')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('setting.uuid = :uuid', { uuid: '1-2-3' })
|
||||
expect(result).toEqual(setting)
|
||||
})
|
||||
|
||||
it('should find one setting by name and user uuid', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(setting)
|
||||
|
||||
const result = await createRepository().findOneByNameAndUserUuid('test', '1-2-3')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('setting.name = :name AND setting.user_uuid = :user_uuid', {
|
||||
name: 'test',
|
||||
user_uuid: '1-2-3',
|
||||
})
|
||||
expect(result).toEqual(setting)
|
||||
})
|
||||
|
||||
it('should find one setting by name and uuid', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(setting)
|
||||
|
||||
const result = await createRepository().findOneByUuidAndNames('1-2-3', ['test' as SettingName])
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('setting.uuid = :uuid AND setting.name IN (:...names)', {
|
||||
names: ['test'],
|
||||
uuid: '1-2-3',
|
||||
})
|
||||
expect(result).toEqual(setting)
|
||||
})
|
||||
|
||||
it('should find last setting by name and user uuid', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
queryBuilder.limit = jest.fn().mockReturnThis()
|
||||
queryBuilder.getMany = jest.fn().mockReturnValue([setting])
|
||||
|
||||
const result = await createRepository().findLastByNameAndUserUuid('test', '1-2-3')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('setting.name = :name AND setting.user_uuid = :user_uuid', {
|
||||
name: 'test',
|
||||
user_uuid: '1-2-3',
|
||||
})
|
||||
expect(queryBuilder.orderBy).toHaveBeenCalledWith('updated_at', 'DESC')
|
||||
expect(queryBuilder.limit).toHaveBeenCalledWith(1)
|
||||
expect(result).toEqual(setting)
|
||||
})
|
||||
|
||||
it('should return null if not found last setting by name and user uuid', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
queryBuilder.limit = jest.fn().mockReturnThis()
|
||||
queryBuilder.getMany = jest.fn().mockReturnValue([])
|
||||
|
||||
const result = await createRepository().findLastByNameAndUserUuid('test', '1-2-3')
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should find all by user uuid', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
|
||||
const settings = [setting]
|
||||
queryBuilder.getMany = jest.fn().mockReturnValue(settings)
|
||||
|
||||
const userUuid = '123'
|
||||
const result = await createRepository().findAllByUserUuid(userUuid)
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('setting.user_uuid = :user_uuid', { user_uuid: userUuid })
|
||||
expect(result).toEqual(settings)
|
||||
})
|
||||
|
||||
it('should delete setting if it does exist', async () => {
|
||||
const queryBuilder = {
|
||||
delete: () => queryBuilder,
|
||||
where: () => queryBuilder,
|
||||
execute: () => undefined,
|
||||
}
|
||||
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
|
||||
|
||||
const result = await createRepository().deleteByUserUuid({
|
||||
userUuid: 'userUuid',
|
||||
settingName: 'settingName',
|
||||
})
|
||||
|
||||
expect(result).toEqual(undefined)
|
||||
})
|
||||
})
|
||||
@@ -1,100 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Repository, SelectQueryBuilder } from 'typeorm'
|
||||
|
||||
import { MySQLSharedSubscriptionInvitationRepository } from './MySQLSharedSubscriptionInvitationRepository'
|
||||
import { SharedSubscriptionInvitation } from '../../Domain/SharedSubscription/SharedSubscriptionInvitation'
|
||||
import { InvitationStatus } from '../../Domain/SharedSubscription/InvitationStatus'
|
||||
|
||||
describe('MySQLSharedSubscriptionInvitationRepository', () => {
|
||||
let ormRepository: Repository<SharedSubscriptionInvitation>
|
||||
let queryBuilder: SelectQueryBuilder<SharedSubscriptionInvitation>
|
||||
let invitation: SharedSubscriptionInvitation
|
||||
|
||||
const createRepository = () => new MySQLSharedSubscriptionInvitationRepository(ormRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<SharedSubscriptionInvitation>>
|
||||
|
||||
invitation = {} as jest.Mocked<SharedSubscriptionInvitation>
|
||||
|
||||
ormRepository = {} as jest.Mocked<Repository<SharedSubscriptionInvitation>>
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
|
||||
ormRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should save', async () => {
|
||||
await createRepository().save(invitation)
|
||||
|
||||
expect(ormRepository.save).toHaveBeenCalledWith(invitation)
|
||||
})
|
||||
|
||||
it('should get invitations by inviter email', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getMany = jest.fn().mockReturnValue([])
|
||||
|
||||
const result = await createRepository().findByInviterEmail('test@test.te')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('invitation.inviter_identifier = :inviterEmail', {
|
||||
inviterEmail: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should count invitations by inviter email and statuses', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getCount = jest.fn().mockReturnValue(3)
|
||||
|
||||
const result = await createRepository().countByInviterEmailAndStatus('test@test.te', [InvitationStatus.Sent])
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith(
|
||||
'invitation.inviter_identifier = :inviterEmail AND invitation.status IN (:...statuses)',
|
||||
{ inviterEmail: 'test@test.te', statuses: ['sent'] },
|
||||
)
|
||||
|
||||
expect(result).toEqual(3)
|
||||
})
|
||||
|
||||
it('should find one invitation by name and uuid', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(invitation)
|
||||
|
||||
const result = await createRepository().findOneByUuidAndStatus('1-2-3', InvitationStatus.Sent)
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('invitation.uuid = :uuid AND invitation.status = :status', {
|
||||
uuid: '1-2-3',
|
||||
status: 'sent',
|
||||
})
|
||||
|
||||
expect(result).toEqual(invitation)
|
||||
})
|
||||
|
||||
it('should find one invitation by invitee and inviter email', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(invitation)
|
||||
|
||||
const result = await createRepository().findOneByInviteeAndInviterEmail('invitee@test.te', 'inviter@test.te')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith(
|
||||
'invitation.inviter_identifier = :inviterEmail AND invitation.invitee_identifier = :inviteeEmail',
|
||||
{
|
||||
inviterEmail: 'inviter@test.te',
|
||||
inviteeEmail: 'invitee@test.te',
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).toEqual(invitation)
|
||||
})
|
||||
|
||||
it('should find one invitation by uuid', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(invitation)
|
||||
|
||||
const result = await createRepository().findOneByUuid('1-2-3')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('invitation.uuid = :uuid', { uuid: '1-2-3' })
|
||||
|
||||
expect(result).toEqual(invitation)
|
||||
})
|
||||
})
|
||||
@@ -1,68 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Repository, SelectQueryBuilder } from 'typeorm'
|
||||
import { SubscriptionSetting } from '../../Domain/Setting/SubscriptionSetting'
|
||||
|
||||
import { MySQLSubscriptionSettingRepository } from './MySQLSubscriptionSettingRepository'
|
||||
|
||||
describe('MySQLSubscriptionSettingRepository', () => {
|
||||
let ormRepository: Repository<SubscriptionSetting>
|
||||
let queryBuilder: SelectQueryBuilder<SubscriptionSetting>
|
||||
let setting: SubscriptionSetting
|
||||
|
||||
const createRepository = () => new MySQLSubscriptionSettingRepository(ormRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<SubscriptionSetting>>
|
||||
|
||||
setting = {} as jest.Mocked<SubscriptionSetting>
|
||||
|
||||
ormRepository = {} as jest.Mocked<Repository<SubscriptionSetting>>
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
|
||||
ormRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should save', async () => {
|
||||
await createRepository().save(setting)
|
||||
|
||||
expect(ormRepository.save).toHaveBeenCalledWith(setting)
|
||||
})
|
||||
|
||||
it('should find one setting by uuid', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(setting)
|
||||
|
||||
const result = await createRepository().findOneByUuid('1-2-3')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('setting.uuid = :uuid', { uuid: '1-2-3' })
|
||||
expect(result).toEqual(setting)
|
||||
})
|
||||
|
||||
it('should find last setting by name and user uuid', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
queryBuilder.limit = jest.fn().mockReturnThis()
|
||||
queryBuilder.getMany = jest.fn().mockReturnValue([setting])
|
||||
|
||||
const result = await createRepository().findLastByNameAndUserSubscriptionUuid('test', '1-2-3')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith(
|
||||
'setting.name = :name AND setting.user_subscription_uuid = :userSubscriptionUuid',
|
||||
{ name: 'test', userSubscriptionUuid: '1-2-3' },
|
||||
)
|
||||
expect(queryBuilder.orderBy).toHaveBeenCalledWith('updated_at', 'DESC')
|
||||
expect(queryBuilder.limit).toHaveBeenCalledWith(1)
|
||||
expect(result).toEqual(setting)
|
||||
})
|
||||
|
||||
it('should return null if not found last setting by name and user uuid', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
queryBuilder.limit = jest.fn().mockReturnThis()
|
||||
queryBuilder.getMany = jest.fn().mockReturnValue([])
|
||||
|
||||
const result = await createRepository().findLastByNameAndUserSubscriptionUuid('test', '1-2-3')
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,69 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { ReadStream } from 'fs'
|
||||
|
||||
import { Repository, SelectQueryBuilder } from 'typeorm'
|
||||
import { User } from '../../Domain/User/User'
|
||||
|
||||
import { MySQLUserRepository } from './MySQLUserRepository'
|
||||
|
||||
describe('MySQLUserRepository', () => {
|
||||
let ormRepository: Repository<User>
|
||||
let queryBuilder: SelectQueryBuilder<User>
|
||||
let user: User
|
||||
|
||||
const createRepository = () => new MySQLUserRepository(ormRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<User>>
|
||||
queryBuilder.cache = jest.fn().mockReturnThis()
|
||||
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
ormRepository = {} as jest.Mocked<Repository<User>>
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
|
||||
ormRepository.save = jest.fn()
|
||||
ormRepository.remove = jest.fn()
|
||||
})
|
||||
|
||||
it('should save', async () => {
|
||||
await createRepository().save(user)
|
||||
|
||||
expect(ormRepository.save).toHaveBeenCalledWith(user)
|
||||
})
|
||||
|
||||
it('should remove', async () => {
|
||||
await createRepository().remove(user)
|
||||
|
||||
expect(ormRepository.remove).toHaveBeenCalledWith(user)
|
||||
})
|
||||
|
||||
it('should find one user by id', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(user)
|
||||
|
||||
const result = await createRepository().findOneByUuid('123')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('user.uuid = :uuid', { uuid: '123' })
|
||||
expect(result).toEqual(user)
|
||||
})
|
||||
|
||||
it('should stream all users', async () => {
|
||||
const stream = {} as jest.Mocked<ReadStream>
|
||||
queryBuilder.stream = jest.fn().mockReturnValue(stream)
|
||||
|
||||
const result = await createRepository().streamAll()
|
||||
|
||||
expect(result).toEqual(stream)
|
||||
})
|
||||
|
||||
it('should find one user by email', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(user)
|
||||
|
||||
const result = await createRepository().findOneByEmail('test@test.te')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('user.email = :email', { email: 'test@test.te' })
|
||||
expect(result).toEqual(user)
|
||||
})
|
||||
})
|
||||
@@ -22,7 +22,10 @@ export class MySQLUserRepository implements UserRepositoryInterface {
|
||||
}
|
||||
|
||||
async streamAll(): Promise<ReadStream> {
|
||||
return this.ormRepository.createQueryBuilder('user').stream()
|
||||
return this.ormRepository
|
||||
.createQueryBuilder('user')
|
||||
.where('created_at < :createdAt', { createdAt: new Date().toISOString() })
|
||||
.stream()
|
||||
}
|
||||
|
||||
async streamTeam(memberEmail?: string): Promise<ReadStream> {
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
|
||||
import { Repository, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm'
|
||||
import { UserSubscription } from '../../Domain/Subscription/UserSubscription'
|
||||
|
||||
import { MySQLUserSubscriptionRepository } from './MySQLUserSubscriptionRepository'
|
||||
import { UserSubscriptionType } from '../../Domain/Subscription/UserSubscriptionType'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
describe('MySQLUserSubscriptionRepository', () => {
|
||||
let ormRepository: Repository<UserSubscription>
|
||||
let selectQueryBuilder: SelectQueryBuilder<UserSubscription>
|
||||
let updateQueryBuilder: UpdateQueryBuilder<UserSubscription>
|
||||
let subscription: UserSubscription
|
||||
let timer: TimerInterface
|
||||
|
||||
const createRepository = () => new MySQLUserSubscriptionRepository(ormRepository, timer)
|
||||
|
||||
beforeEach(() => {
|
||||
selectQueryBuilder = {} as jest.Mocked<SelectQueryBuilder<UserSubscription>>
|
||||
updateQueryBuilder = {} as jest.Mocked<UpdateQueryBuilder<UserSubscription>>
|
||||
|
||||
subscription = {
|
||||
planName: SubscriptionName.ProPlan,
|
||||
cancelled: false,
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
ormRepository = {} as jest.Mocked<Repository<UserSubscription>>
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
ormRepository.save = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
|
||||
})
|
||||
|
||||
it('should save', async () => {
|
||||
await createRepository().save(subscription)
|
||||
|
||||
expect(ormRepository.save).toHaveBeenCalledWith(subscription)
|
||||
})
|
||||
|
||||
it('should find all subscriptions by user uuid', async () => {
|
||||
const canceledSubscription = {
|
||||
planName: SubscriptionName.ProPlan,
|
||||
cancelled: true,
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getMany = jest.fn().mockReturnValue([canceledSubscription, subscription])
|
||||
|
||||
const result = await createRepository().findByUserUuid('123')
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
|
||||
user_uuid: '123',
|
||||
})
|
||||
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
|
||||
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
|
||||
expect(result).toEqual([canceledSubscription, subscription])
|
||||
})
|
||||
|
||||
it('should count all active subscriptions', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.groupBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getCount = jest.fn().mockReturnValue(2)
|
||||
|
||||
const result = await createRepository().countActiveSubscriptions()
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('ends_at > :timestamp', {
|
||||
timestamp: 123,
|
||||
})
|
||||
expect(selectQueryBuilder.groupBy).toHaveBeenCalledWith('user_uuid')
|
||||
expect(selectQueryBuilder.getCount).toHaveBeenCalled()
|
||||
expect(result).toEqual(2)
|
||||
})
|
||||
|
||||
it('should find one longest lasting uncanceled subscription by user uuid if there are canceled ones', async () => {
|
||||
const canceledSubscription = {
|
||||
planName: SubscriptionName.ProPlan,
|
||||
cancelled: true,
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getMany = jest.fn().mockReturnValue([canceledSubscription, subscription])
|
||||
|
||||
const result = await createRepository().findOneByUserUuid('123')
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
|
||||
user_uuid: '123',
|
||||
})
|
||||
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
|
||||
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
|
||||
expect(result).toEqual(subscription)
|
||||
})
|
||||
|
||||
it('should find one, longest lasting subscription by user uuid if there are no canceled ones', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getMany = jest.fn().mockReturnValue([subscription])
|
||||
|
||||
const result = await createRepository().findOneByUserUuid('123')
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
|
||||
user_uuid: '123',
|
||||
})
|
||||
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
|
||||
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
|
||||
expect(result).toEqual(subscription)
|
||||
})
|
||||
|
||||
it('should count by user uuid', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getCount = jest.fn().mockReturnValue(2)
|
||||
|
||||
const result = await createRepository().countByUserUuid('123')
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
|
||||
user_uuid: '123',
|
||||
})
|
||||
expect(selectQueryBuilder.getCount).toHaveBeenCalled()
|
||||
expect(result).toEqual(2)
|
||||
})
|
||||
|
||||
it('should find one, longest lasting subscription by user uuid if there are no ucanceled ones', async () => {
|
||||
subscription.cancelled = true
|
||||
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getMany = jest.fn().mockReturnValue([subscription])
|
||||
|
||||
const result = await createRepository().findOneByUserUuid('123')
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
|
||||
user_uuid: '123',
|
||||
})
|
||||
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
|
||||
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
|
||||
expect(result).toEqual(subscription)
|
||||
})
|
||||
|
||||
it('should find none if there are no subscriptions for the user', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getMany = jest.fn().mockReturnValue([])
|
||||
|
||||
const result = await createRepository().findOneByUserUuid('123')
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
|
||||
user_uuid: '123',
|
||||
})
|
||||
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
|
||||
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should update ends at by subscription id', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
|
||||
|
||||
updateQueryBuilder.update = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.set = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.execute = jest.fn()
|
||||
|
||||
await createRepository().updateEndsAt(1, 1000, 1000)
|
||||
|
||||
expect(updateQueryBuilder.update).toHaveBeenCalled()
|
||||
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
|
||||
updatedAt: 1000,
|
||||
renewedAt: 1000,
|
||||
endsAt: 1000,
|
||||
})
|
||||
expect(updateQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
|
||||
subscriptionId: 1,
|
||||
})
|
||||
expect(updateQueryBuilder.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update cancelled by subscription id', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
|
||||
|
||||
updateQueryBuilder.update = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.set = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.execute = jest.fn()
|
||||
|
||||
await createRepository().updateCancelled(1, true, 1000)
|
||||
|
||||
expect(updateQueryBuilder.update).toHaveBeenCalled()
|
||||
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
|
||||
updatedAt: expect.any(Number),
|
||||
cancelled: true,
|
||||
})
|
||||
expect(updateQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
|
||||
subscriptionId: 1,
|
||||
})
|
||||
expect(updateQueryBuilder.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should find subscriptions by id', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getMany = jest.fn().mockReturnValue([subscription])
|
||||
|
||||
const result = await createRepository().findBySubscriptionId(123)
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
|
||||
subscriptionId: 123,
|
||||
})
|
||||
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('created_at', 'DESC')
|
||||
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
|
||||
expect(result).toEqual([subscription])
|
||||
})
|
||||
|
||||
it('should find subscriptions by id and type', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getMany = jest.fn().mockReturnValue([subscription])
|
||||
|
||||
const result = await createRepository().findBySubscriptionIdAndType(123, UserSubscriptionType.Regular)
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith(
|
||||
'subscription_id = :subscriptionId AND subscription_type = :type',
|
||||
{
|
||||
subscriptionId: 123,
|
||||
type: 'regular',
|
||||
},
|
||||
)
|
||||
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('created_at', 'DESC')
|
||||
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
|
||||
expect(result).toEqual([subscription])
|
||||
})
|
||||
|
||||
it('should find one subscription by id and user uuid', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getOne = jest.fn().mockReturnValue(subscription)
|
||||
|
||||
const result = await createRepository().findOneByUserUuidAndSubscriptionId('1-2-3', 5)
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith(
|
||||
'user_uuid = :userUuid AND subscription_id = :subscriptionId',
|
||||
{
|
||||
subscriptionId: 5,
|
||||
userUuid: '1-2-3',
|
||||
},
|
||||
)
|
||||
expect(selectQueryBuilder.getOne).toHaveBeenCalled()
|
||||
expect(result).toEqual(subscription)
|
||||
})
|
||||
|
||||
it('should find one subscription by uuid', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getOne = jest.fn().mockReturnValue(subscription)
|
||||
|
||||
const result = await createRepository().findOneByUuid('1-2-3')
|
||||
|
||||
expect(selectQueryBuilder.where).toHaveBeenCalledWith('uuid = :uuid', {
|
||||
uuid: '1-2-3',
|
||||
})
|
||||
expect(selectQueryBuilder.getOne).toHaveBeenCalled()
|
||||
expect(result).toEqual(subscription)
|
||||
})
|
||||
})
|
||||
@@ -1,92 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import * as IORedis from 'ioredis'
|
||||
import { LockRepository } from './LockRepository'
|
||||
|
||||
describe('LockRepository', () => {
|
||||
let redisClient: IORedis.Redis
|
||||
const maxLoginAttempts = 3
|
||||
const failedLoginLockout = 120
|
||||
|
||||
const createRepository = () => new LockRepository(redisClient, maxLoginAttempts, failedLoginLockout)
|
||||
|
||||
beforeEach(() => {
|
||||
redisClient = {} as jest.Mocked<IORedis.Redis>
|
||||
redisClient.expire = jest.fn()
|
||||
redisClient.del = jest.fn()
|
||||
redisClient.get = jest.fn()
|
||||
redisClient.setex = jest.fn()
|
||||
})
|
||||
|
||||
it('should lock a successfully used OTP for the lockout period', async () => {
|
||||
await createRepository().lockSuccessfullOTP('test@test.te', '123456')
|
||||
|
||||
expect(redisClient.setex).toHaveBeenCalledWith('otp-lock:test@test.te', 60, '123456')
|
||||
})
|
||||
|
||||
it('should indicate if an OTP was already used in the lockout period', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue('123456')
|
||||
|
||||
expect(await createRepository().isOTPLocked('test@test.te', '123456')).toEqual(true)
|
||||
})
|
||||
|
||||
it('should indicate if an OTP was not already used in the lockout period', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue('654321')
|
||||
|
||||
expect(await createRepository().isOTPLocked('test@test.te', '123456')).toEqual(false)
|
||||
})
|
||||
|
||||
it('should lock a user for the lockout period', async () => {
|
||||
await createRepository().lockUser('123')
|
||||
|
||||
expect(redisClient.expire).toHaveBeenCalledWith('lock:123', 120)
|
||||
})
|
||||
|
||||
it('should tell a user is locked if his counter is above threshold', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue('4')
|
||||
|
||||
expect(await createRepository().isUserLocked('123')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should tell a user is locked if his counter is at the threshold', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue('3')
|
||||
|
||||
expect(await createRepository().isUserLocked('123')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should tell a user is not locked if his counter is below threshold', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue('2')
|
||||
|
||||
expect(await createRepository().isUserLocked('123')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should tell a user is not locked if he has no counter', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(await createRepository().isUserLocked('123')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should tell what the user lock counter is', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue('3')
|
||||
|
||||
expect(await createRepository().getLockCounter('123')).toStrictEqual(3)
|
||||
})
|
||||
|
||||
it('should tell that the user lock counter is 0 when there is no counter', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(await createRepository().getLockCounter('123')).toStrictEqual(0)
|
||||
})
|
||||
|
||||
it('should reset a lock counter', async () => {
|
||||
await createRepository().resetLockCounter('123')
|
||||
|
||||
expect(redisClient.del).toHaveBeenCalledWith('lock:123')
|
||||
})
|
||||
|
||||
it('should update a lock counter', async () => {
|
||||
await createRepository().updateLockCounter('123', 3)
|
||||
|
||||
expect(redisClient.setex).toHaveBeenCalledWith('lock:123', 120, 3)
|
||||
})
|
||||
})
|
||||
@@ -1,152 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import * as IORedis from 'ioredis'
|
||||
|
||||
import { RedisEphemeralSessionRepository } from './RedisEphemeralSessionRepository'
|
||||
import { EphemeralSession } from '../../Domain/Session/EphemeralSession'
|
||||
|
||||
describe('RedisEphemeralSessionRepository', () => {
|
||||
let redisClient: IORedis.Redis
|
||||
let pipeline: IORedis.Pipeline
|
||||
|
||||
const createRepository = () => new RedisEphemeralSessionRepository(redisClient, 3600)
|
||||
|
||||
beforeEach(() => {
|
||||
redisClient = {} as jest.Mocked<IORedis.Redis>
|
||||
|
||||
redisClient.get = jest.fn()
|
||||
redisClient.smembers = jest.fn()
|
||||
|
||||
pipeline = {} as jest.Mocked<IORedis.Pipeline>
|
||||
pipeline.setex = jest.fn()
|
||||
pipeline.expire = jest.fn()
|
||||
pipeline.sadd = jest.fn()
|
||||
pipeline.del = jest.fn()
|
||||
pipeline.srem = jest.fn()
|
||||
pipeline.exec = jest.fn()
|
||||
|
||||
redisClient.pipeline = jest.fn().mockReturnValue(pipeline)
|
||||
})
|
||||
|
||||
it('should delete an ephemeral', async () => {
|
||||
await createRepository().deleteOne('1-2-3', '2-3-4')
|
||||
|
||||
expect(pipeline.del).toHaveBeenCalledWith('session:1-2-3:2-3-4')
|
||||
expect(pipeline.del).toHaveBeenCalledWith('session:1-2-3')
|
||||
expect(pipeline.srem).toHaveBeenCalledWith('user-sessions:2-3-4', '1-2-3')
|
||||
})
|
||||
|
||||
it('should save an ephemeral session', async () => {
|
||||
const ephemeralSession = new EphemeralSession()
|
||||
ephemeralSession.uuid = '1-2-3'
|
||||
ephemeralSession.userUuid = '2-3-4'
|
||||
ephemeralSession.userAgent = 'Mozilla Firefox'
|
||||
ephemeralSession.createdAt = new Date(1)
|
||||
ephemeralSession.updatedAt = new Date(2)
|
||||
|
||||
await createRepository().save(ephemeralSession)
|
||||
|
||||
expect(pipeline.setex).toHaveBeenCalledWith(
|
||||
'session:1-2-3:2-3-4',
|
||||
3600,
|
||||
'{"uuid":"1-2-3","userUuid":"2-3-4","userAgent":"Mozilla Firefox","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z"}',
|
||||
)
|
||||
expect(pipeline.sadd).toHaveBeenCalledWith('user-sessions:2-3-4', '1-2-3')
|
||||
expect(pipeline.expire).toHaveBeenCalledWith('user-sessions:2-3-4', 3600)
|
||||
})
|
||||
|
||||
it('should find all ephemeral sessions by user uuid', async () => {
|
||||
redisClient.smembers = jest.fn().mockReturnValue(['1-2-3', '2-3-4', '3-4-5'])
|
||||
|
||||
redisClient.get = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(
|
||||
'{"uuid":"1-2-3","userUuid":"2-3-4","userAgent":"Mozilla Firefox","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z"}',
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
'{"uuid":"2-3-4","userUuid":"2-3-4","userAgent":"Google Chrome","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z"}',
|
||||
)
|
||||
.mockReturnValueOnce(null)
|
||||
|
||||
const ephemeralSessions = await createRepository().findAllByUserUuid('2-3-4')
|
||||
|
||||
expect(ephemeralSessions.length).toEqual(2)
|
||||
expect(ephemeralSessions[1].userAgent).toEqual('Google Chrome')
|
||||
})
|
||||
|
||||
it('should find an ephemeral session by uuid', async () => {
|
||||
redisClient.get = jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
'{"uuid":"1-2-3","userUuid":"2-3-4","userAgent":"Mozilla Firefox","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z"}',
|
||||
)
|
||||
|
||||
const ephemeralSession = <EphemeralSession>await createRepository().findOneByUuid('1-2-3')
|
||||
|
||||
expect(ephemeralSession).not.toBeUndefined()
|
||||
expect(ephemeralSession.userAgent).toEqual('Mozilla Firefox')
|
||||
})
|
||||
|
||||
it('should find an ephemeral session by uuid and user uuid', async () => {
|
||||
redisClient.get = jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
'{"uuid":"1-2-3","userUuid":"2-3-4","userAgent":"Mozilla Firefox","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z"}',
|
||||
)
|
||||
|
||||
const ephemeralSession = <EphemeralSession>await createRepository().findOneByUuidAndUserUuid('1-2-3', '2-3-4')
|
||||
|
||||
expect(ephemeralSession).not.toBeUndefined()
|
||||
expect(ephemeralSession.userAgent).toEqual('Mozilla Firefox')
|
||||
})
|
||||
|
||||
it('should return undefined if session is not found', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue(null)
|
||||
|
||||
const ephemeralSession = <EphemeralSession>await createRepository().findOneByUuid('1-2-3')
|
||||
|
||||
expect(ephemeralSession).toBeNull()
|
||||
})
|
||||
|
||||
it('should return undefined if ephemeral session is not found', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue(null)
|
||||
|
||||
const ephemeralSession = <EphemeralSession>await createRepository().findOneByUuidAndUserUuid('1-2-3', '2-3-4')
|
||||
|
||||
expect(ephemeralSession).toBeNull()
|
||||
})
|
||||
|
||||
it('should update tokens and expirations dates', async () => {
|
||||
redisClient.get = jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
'{"uuid":"1-2-3","userUuid":"2-3-4","userAgent":"Mozilla Firefox","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z"}',
|
||||
)
|
||||
|
||||
await createRepository().updateTokensAndExpirationDates(
|
||||
'1-2-3',
|
||||
'dummy_access_token',
|
||||
'dummy_refresh_token',
|
||||
new Date(3),
|
||||
new Date(4),
|
||||
)
|
||||
|
||||
expect(pipeline.setex).toHaveBeenCalledWith(
|
||||
'session:1-2-3:2-3-4',
|
||||
3600,
|
||||
'{"uuid":"1-2-3","userUuid":"2-3-4","userAgent":"Mozilla Firefox","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z","hashedAccessToken":"dummy_access_token","hashedRefreshToken":"dummy_refresh_token","accessExpiration":"1970-01-01T00:00:00.003Z","refreshExpiration":"1970-01-01T00:00:00.004Z"}',
|
||||
)
|
||||
})
|
||||
|
||||
it('should not update tokens and expirations dates if the ephemeral session does not exist', async () => {
|
||||
await createRepository().updateTokensAndExpirationDates(
|
||||
'1-2-3',
|
||||
'dummy_access_token',
|
||||
'dummy_refresh_token',
|
||||
new Date(3),
|
||||
new Date(4),
|
||||
)
|
||||
|
||||
expect(pipeline.setex).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,59 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import * as IORedis from 'ioredis'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { RedisOfflineSubscriptionTokenRepository } from './RedisOfflineSubscriptionTokenRepository'
|
||||
import { OfflineSubscriptionToken } from '../../Domain/Auth/OfflineSubscriptionToken'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
describe('RedisOfflineSubscriptionTokenRepository', () => {
|
||||
let redisClient: IORedis.Redis
|
||||
let timer: TimerInterface
|
||||
let logger: Logger
|
||||
|
||||
const createRepository = () => new RedisOfflineSubscriptionTokenRepository(redisClient, timer, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
redisClient = {} as jest.Mocked<IORedis.Redis>
|
||||
redisClient.set = jest.fn()
|
||||
redisClient.get = jest.fn()
|
||||
redisClient.expireat = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertMicrosecondsToSeconds = jest.fn().mockReturnValue(1)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
})
|
||||
|
||||
it('should get a user uuid in exchange for an dashboard token', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue('test@test.com')
|
||||
|
||||
expect(await createRepository().getUserEmailByToken('random-string')).toEqual('test@test.com')
|
||||
|
||||
expect(redisClient.get).toHaveBeenCalledWith('offline-subscription-token:random-string')
|
||||
})
|
||||
|
||||
it('should return undefined if a user uuid is not exchanged for an dashboard token', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(await createRepository().getUserEmailByToken('random-string')).toBeUndefined()
|
||||
|
||||
expect(redisClient.get).toHaveBeenCalledWith('offline-subscription-token:random-string')
|
||||
})
|
||||
|
||||
it('should save an dashboard token', async () => {
|
||||
const offlineSubscriptionToken: OfflineSubscriptionToken = {
|
||||
userEmail: 'test@test.com',
|
||||
token: 'random-string',
|
||||
expiresAt: 123,
|
||||
}
|
||||
|
||||
await createRepository().save(offlineSubscriptionToken)
|
||||
|
||||
expect(redisClient.set).toHaveBeenCalledWith('offline-subscription-token:random-string', 'test@test.com')
|
||||
|
||||
expect(redisClient.expireat).toHaveBeenCalledWith('offline-subscription-token:random-string', 1)
|
||||
})
|
||||
})
|
||||
@@ -1,34 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import * as IORedis from 'ioredis'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { RedisPKCERepository } from './RedisPKCERepository'
|
||||
|
||||
describe('RedisPKCERepository', () => {
|
||||
let redisClient: IORedis.Redis
|
||||
let logger: Logger
|
||||
|
||||
const createRepository = () => new RedisPKCERepository(redisClient, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
redisClient = {} as jest.Mocked<IORedis.Redis>
|
||||
redisClient.setex = jest.fn()
|
||||
redisClient.del = jest.fn().mockReturnValue(1)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
})
|
||||
|
||||
it('should store a code challenge', async () => {
|
||||
await createRepository().storeCodeChallenge('test')
|
||||
|
||||
expect(redisClient.setex).toHaveBeenCalledWith('pkce:test', 3600, 'test')
|
||||
})
|
||||
|
||||
it('should remove a code challenge and notify of success', async () => {
|
||||
expect(await createRepository().removeCodeChallenge('test')).toBeTruthy()
|
||||
|
||||
expect(redisClient.del).toHaveBeenCalledWith('pkce:test')
|
||||
})
|
||||
})
|
||||
@@ -1,70 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import * as IORedis from 'ioredis'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { RedisSubscriptionTokenRepository } from './RedisSubscriptionTokenRepository'
|
||||
import { SubscriptionToken } from '../../Domain/Subscription/SubscriptionToken'
|
||||
|
||||
describe('RedisSubscriptionTokenRepository', () => {
|
||||
let redisClient: IORedis.Redis
|
||||
let timer: TimerInterface
|
||||
|
||||
const createRepository = () => new RedisSubscriptionTokenRepository(redisClient, timer)
|
||||
|
||||
beforeEach(() => {
|
||||
redisClient = {} as jest.Mocked<IORedis.Redis>
|
||||
redisClient.set = jest.fn().mockReturnValue('OK')
|
||||
redisClient.get = jest.fn()
|
||||
redisClient.expireat = jest.fn().mockReturnValue(1)
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertMicrosecondsToSeconds = jest.fn().mockReturnValue(1)
|
||||
})
|
||||
|
||||
it('should get a user uuid in exchange for an subscription token', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue('1-2-3')
|
||||
|
||||
expect(await createRepository().getUserUuidByToken('random-string')).toEqual('1-2-3')
|
||||
|
||||
expect(redisClient.get).toHaveBeenCalledWith('subscription-token:random-string')
|
||||
})
|
||||
|
||||
it('should return undefined if a user uuid is not exchanged for an subscription token', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(await createRepository().getUserUuidByToken('random-string')).toBeUndefined()
|
||||
|
||||
expect(redisClient.get).toHaveBeenCalledWith('subscription-token:random-string')
|
||||
})
|
||||
|
||||
it('should save an subscription token', async () => {
|
||||
const subscriptionToken: SubscriptionToken = {
|
||||
userUuid: '1-2-3',
|
||||
token: 'random-string',
|
||||
expiresAt: 123,
|
||||
}
|
||||
|
||||
expect(await createRepository().save(subscriptionToken)).toBeTruthy()
|
||||
|
||||
expect(redisClient.set).toHaveBeenCalledWith('subscription-token:random-string', '1-2-3')
|
||||
|
||||
expect(redisClient.expireat).toHaveBeenCalledWith('subscription-token:random-string', 1)
|
||||
})
|
||||
|
||||
it('should indicate subscription token was not saved', async () => {
|
||||
redisClient.set = jest.fn().mockReturnValue(null)
|
||||
|
||||
const subscriptionToken: SubscriptionToken = {
|
||||
userUuid: '1-2-3',
|
||||
token: 'random-string',
|
||||
expiresAt: 123,
|
||||
}
|
||||
|
||||
expect(await createRepository().save(subscriptionToken)).toBeFalsy()
|
||||
|
||||
expect(redisClient.set).toHaveBeenCalledWith('subscription-token:random-string', '1-2-3')
|
||||
|
||||
expect(redisClient.expireat).toHaveBeenCalledWith('subscription-token:random-string', 1)
|
||||
})
|
||||
})
|
||||
@@ -1,53 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import {
|
||||
DomainEventPublisherInterface,
|
||||
UserRolesChangedEvent,
|
||||
WebSocketMessageRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
|
||||
import { User } from '../../Domain/User/User'
|
||||
import { WebSocketsClientService } from './WebSocketsClientService'
|
||||
import { DomainEventFactoryInterface } from '../../Domain/Event/DomainEventFactoryInterface'
|
||||
|
||||
describe('WebSocketsClientService', () => {
|
||||
let user: User
|
||||
let event: UserRolesChangedEvent
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createService = () => new WebSocketsClientService(domainEventFactory, domainEventPublisher)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([
|
||||
{
|
||||
name: RoleName.ProUser,
|
||||
},
|
||||
]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
event = {} as jest.Mocked<UserRolesChangedEvent>
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createUserRolesChangedEvent = jest.fn().mockReturnValue(event)
|
||||
domainEventFactory.createWebSocketMessageRequestedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<WebSocketMessageRequestedEvent>)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
})
|
||||
|
||||
it('should request a message about a user role changed', async () => {
|
||||
await createService().sendUserRolesChangedEvent(user)
|
||||
|
||||
expect(domainEventFactory.createUserRolesChangedEvent).toHaveBeenCalledWith('123', 'test@test.com', [
|
||||
RoleName.ProUser,
|
||||
])
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,47 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Permission } from '../Domain/Permission/Permission'
|
||||
|
||||
import { PermissionProjector } from './PermissionProjector'
|
||||
|
||||
describe('PermissionProjector', () => {
|
||||
let permission: Permission
|
||||
|
||||
const createProjector = () => new PermissionProjector()
|
||||
|
||||
beforeEach(() => {
|
||||
permission = new Permission()
|
||||
permission.uuid = '123'
|
||||
permission.name = 'permission1'
|
||||
permission.createdAt = new Date(1)
|
||||
permission.updatedAt = new Date(2)
|
||||
})
|
||||
|
||||
it('should create a simple projection', () => {
|
||||
const projection = createProjector().projectSimple(permission)
|
||||
expect(projection).toMatchObject({
|
||||
uuid: '123',
|
||||
name: 'permission1',
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw error on custom projection', () => {
|
||||
let error = null
|
||||
try {
|
||||
createProjector().projectCustom('test', permission)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should throw error on not implemetned full projection', () => {
|
||||
let error = null
|
||||
try {
|
||||
createProjector().projectFull(permission)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,47 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Role } from '../Domain/Role/Role'
|
||||
|
||||
import { RoleProjector } from './RoleProjector'
|
||||
|
||||
describe('RoleProjector', () => {
|
||||
let role: Role
|
||||
|
||||
const createProjector = () => new RoleProjector()
|
||||
|
||||
beforeEach(() => {
|
||||
role = new Role()
|
||||
role.uuid = '123'
|
||||
role.name = 'role1'
|
||||
role.createdAt = new Date(1)
|
||||
role.updatedAt = new Date(2)
|
||||
})
|
||||
|
||||
it('should create a simple projection', () => {
|
||||
const projection = createProjector().projectSimple(role)
|
||||
expect(projection).toMatchObject({
|
||||
uuid: '123',
|
||||
name: 'role1',
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw error on custom projection', () => {
|
||||
let error = null
|
||||
try {
|
||||
createProjector().projectCustom('test', role)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should throw error on not implemetned full projection', () => {
|
||||
let error = null
|
||||
try {
|
||||
createProjector().projectFull(role)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,109 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
|
||||
import { SessionProjector } from './SessionProjector'
|
||||
import { Session } from '../Domain/Session/Session'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
describe('SessionProjector', () => {
|
||||
let session: Session
|
||||
let currentSession: Session
|
||||
let sessionService: SessionServiceInterface
|
||||
let timer: TimerInterface
|
||||
|
||||
const createProjector = () => new SessionProjector(sessionService, timer)
|
||||
|
||||
beforeEach(() => {
|
||||
session = new Session()
|
||||
session.uuid = '123'
|
||||
session.hashedAccessToken = 'hashed access token'
|
||||
session.userUuid = '234'
|
||||
session.apiVersion = '004'
|
||||
session.createdAt = new Date(1)
|
||||
session.updatedAt = new Date(1)
|
||||
session.accessExpiration = new Date(1)
|
||||
session.refreshExpiration = new Date(1)
|
||||
session.readonlyAccess = false
|
||||
|
||||
currentSession = new Session()
|
||||
currentSession.uuid = '234'
|
||||
|
||||
sessionService = {} as jest.Mocked<SessionServiceInterface>
|
||||
sessionService.getDeviceInfo = jest.fn().mockReturnValue('Some Device Info')
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertDateToISOString = jest.fn().mockReturnValue('2020-11-26T13:34:00.000Z')
|
||||
})
|
||||
|
||||
it('should create a simple projection of a session', () => {
|
||||
const projection = createProjector().projectSimple(session)
|
||||
expect(projection).toMatchObject({
|
||||
uuid: '123',
|
||||
api_version: '004',
|
||||
created_at: '2020-11-26T13:34:00.000Z',
|
||||
updated_at: '2020-11-26T13:34:00.000Z',
|
||||
device_info: 'Some Device Info',
|
||||
readonly_access: false,
|
||||
access_expiration: '2020-11-26T13:34:00.000Z',
|
||||
refresh_expiration: '2020-11-26T13:34:00.000Z',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a custom projection of a session', () => {
|
||||
const projection = createProjector().projectCustom(
|
||||
SessionProjector.CURRENT_SESSION_PROJECTION.toString(),
|
||||
session,
|
||||
currentSession,
|
||||
)
|
||||
|
||||
expect(projection).toMatchObject({
|
||||
uuid: '123',
|
||||
api_version: '004',
|
||||
created_at: '2020-11-26T13:34:00.000Z',
|
||||
updated_at: '2020-11-26T13:34:00.000Z',
|
||||
device_info: 'Some Device Info',
|
||||
current: false,
|
||||
readonly_access: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a custom projection of a current session', () => {
|
||||
currentSession.uuid = '123'
|
||||
|
||||
const projection = createProjector().projectCustom(
|
||||
SessionProjector.CURRENT_SESSION_PROJECTION.toString(),
|
||||
session,
|
||||
currentSession,
|
||||
)
|
||||
|
||||
expect(projection).toMatchObject({
|
||||
uuid: '123',
|
||||
api_version: '004',
|
||||
created_at: '2020-11-26T13:34:00.000Z',
|
||||
updated_at: '2020-11-26T13:34:00.000Z',
|
||||
device_info: 'Some Device Info',
|
||||
current: true,
|
||||
readonly_access: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw error on unknown custom projection', () => {
|
||||
let error = null
|
||||
try {
|
||||
createProjector().projectCustom('test', session, currentSession)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect((error as Error).message).toEqual('Not supported projection type: test')
|
||||
})
|
||||
|
||||
it('should throw error on not implemetned full projection', () => {
|
||||
let error = null
|
||||
try {
|
||||
createProjector().projectFull(session)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect((error as Error).message).toEqual('not implemented')
|
||||
})
|
||||
})
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Setting } from '../Domain/Setting/Setting'
|
||||
|
||||
import { SettingProjector } from './SettingProjector'
|
||||
|
||||
describe('SettingProjector', () => {
|
||||
let setting: Setting
|
||||
|
||||
const createProjector = () => new SettingProjector()
|
||||
|
||||
beforeEach(() => {
|
||||
setting = {
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
serverEncryptionVersion: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
} as jest.Mocked<Setting>
|
||||
})
|
||||
|
||||
it('should create a simple projection of a setting', async () => {
|
||||
const projection = await createProjector().projectSimple(setting)
|
||||
expect(projection).toStrictEqual({
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
})
|
||||
})
|
||||
it('should create a simple projection of list of settings', async () => {
|
||||
const projection = await createProjector().projectManySimple([setting])
|
||||
expect(projection).toStrictEqual([
|
||||
{
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting'
|
||||
|
||||
import { SubscriptionSettingProjector } from './SubscriptionSettingProjector'
|
||||
|
||||
describe('SubscriptionSettingProjector', () => {
|
||||
let setting: SubscriptionSetting
|
||||
|
||||
const createProjector = () => new SubscriptionSettingProjector()
|
||||
|
||||
beforeEach(() => {
|
||||
setting = {
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
serverEncryptionVersion: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
} as jest.Mocked<SubscriptionSetting>
|
||||
})
|
||||
|
||||
it('should create a simple projection of a setting', async () => {
|
||||
const projection = await createProjector().projectSimple(setting)
|
||||
expect(projection).toStrictEqual({
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
})
|
||||
})
|
||||
it('should create a simple projection of list of settings', async () => {
|
||||
const projection = await createProjector().projectManySimple([setting])
|
||||
expect(projection).toStrictEqual([
|
||||
{
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,45 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { UserProjector } from './UserProjector'
|
||||
import { User } from '../Domain/User/User'
|
||||
|
||||
describe('UserProjector', () => {
|
||||
let user: User
|
||||
|
||||
const createProjector = () => new UserProjector()
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User()
|
||||
user.uuid = '123'
|
||||
user.email = 'test@test.te'
|
||||
user.encryptedPassword = '123qwe345'
|
||||
})
|
||||
|
||||
it('should create a simple projection of a user', () => {
|
||||
const projection = createProjector().projectSimple(user)
|
||||
expect(projection).toMatchObject({
|
||||
uuid: '123',
|
||||
email: 'test@test.te',
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw error on custom projection', () => {
|
||||
let error = null
|
||||
try {
|
||||
createProjector().projectCustom('test', user)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should throw error on not implemetned full projection', () => {
|
||||
let error = null
|
||||
try {
|
||||
createProjector().projectFull(user)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.9.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.8.0...@standardnotes/domain-core@1.9.0) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-core:** rename email subscription rejection level to email level ([c87561f](https://github.com/standardnotes/server/commit/c87561fca782883b84f58b4f0b9f85ecc279ca50))
|
||||
|
||||
# [1.8.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.7.0...@standardnotes/domain-core@1.8.0) (2022-12-05)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-core:** add email subscription rejection levels ([02f3c85](https://github.com/standardnotes/server/commit/02f3c85796ade7cb69edbdda2367c0d91ac1bdf0))
|
||||
|
||||
# [1.7.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.6.0...@standardnotes/domain-core@1.7.0) (2022-12-05)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-core",
|
||||
"version": "1.7.0",
|
||||
"version": "1.9.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
18
packages/domain-core/src/Domain/Email/EmailLevel.spec.ts
Normal file
18
packages/domain-core/src/Domain/Email/EmailLevel.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { EmailLevel } from './EmailLevel'
|
||||
|
||||
describe('EmailLevel', () => {
|
||||
it('should create a value object', () => {
|
||||
const valueOrError = EmailLevel.create(EmailLevel.LEVELS.SignIn)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
expect(valueOrError.getValue().value).toEqual('SIGN_IN')
|
||||
})
|
||||
|
||||
it('should not create an invalid value object', () => {
|
||||
for (const value of ['', undefined, null, 0, 'FOOBAR']) {
|
||||
const valueOrError = EmailLevel.create(value as string)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeTruthy()
|
||||
}
|
||||
})
|
||||
})
|
||||
31
packages/domain-core/src/Domain/Email/EmailLevel.ts
Normal file
31
packages/domain-core/src/Domain/Email/EmailLevel.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Result } from '../Core/Result'
|
||||
import { ValueObject } from '../Core/ValueObject'
|
||||
|
||||
import { EmailLevelProps } from './EmailLevelProps'
|
||||
|
||||
export class EmailLevel extends ValueObject<EmailLevelProps> {
|
||||
static readonly LEVELS = {
|
||||
System: 'SYSTEM',
|
||||
SignIn: 'SIGN_IN',
|
||||
Marketing: 'MARKETING',
|
||||
FailedCloudBackup: 'FAILED_CLOUD_BACKUP',
|
||||
FailedEmailBackup: 'FAILED_EMAIL_BACKUP',
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return this.props.value
|
||||
}
|
||||
|
||||
private constructor(props: EmailLevelProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
static create(name: string): Result<EmailLevel> {
|
||||
const isValidName = Object.values(this.LEVELS).includes(name)
|
||||
if (!isValidName) {
|
||||
return Result.fail<EmailLevel>(`Invalid subscription rejection level: ${name}`)
|
||||
} else {
|
||||
return Result.ok<EmailLevel>(new EmailLevel({ value: name }))
|
||||
}
|
||||
}
|
||||
}
|
||||
3
packages/domain-core/src/Domain/Email/EmailLevelProps.ts
Normal file
3
packages/domain-core/src/Domain/Email/EmailLevelProps.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface EmailLevelProps {
|
||||
value: string
|
||||
}
|
||||
@@ -20,6 +20,9 @@ export * from './Core/Validator'
|
||||
export * from './Core/ValueObject'
|
||||
export * from './Core/ValueObjectProps'
|
||||
|
||||
export * from './Email/EmailLevel'
|
||||
export * from './Email/EmailLevelProps'
|
||||
|
||||
export * from './Mapping/MapperInterface'
|
||||
|
||||
export * from './Subscription/SubscriptionPlanName'
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.9.43](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.42...@standardnotes/domain-events-infra@1.9.43) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.42](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.41...@standardnotes/domain-events-infra@1.9.42) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.41](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.40...@standardnotes/domain-events-infra@1.9.41) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.40](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.39...@standardnotes/domain-events-infra@1.9.40) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.39](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.38...@standardnotes/domain-events-infra@1.9.39) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.38](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.37...@standardnotes/domain-events-infra@1.9.38) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.37](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.36...@standardnotes/domain-events-infra@1.9.37) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.36](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.35...@standardnotes/domain-events-infra@1.9.36) (2022-11-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.9.36",
|
||||
"version": "1.9.43",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,48 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.98.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.97.0...@standardnotes/domain-events@2.98.0) (2022-12-08)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-events:** remove unused events and add attachments option for sending emails ([435cd2f](https://github.com/standardnotes/server/commit/435cd2f66a1332a294001e87eed3ece1b8b991ae))
|
||||
|
||||
# [2.97.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.96.0...@standardnotes/domain-events@2.97.0) (2022-12-08)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-events:** remove unused account reset requested event ([3a12f5c](https://github.com/standardnotes/server/commit/3a12f5c1c40ab6cb236b963bad2a987bacef55e4))
|
||||
|
||||
# [2.96.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.95.0...@standardnotes/domain-events@2.96.0) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** replace user signed in events with email requested ([e48cca6](https://github.com/standardnotes/server/commit/e48cca6b45b02876f2d82b726c1d2f124d90b587))
|
||||
|
||||
# [2.95.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.94.1...@standardnotes/domain-events@2.95.0) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-events:** add email requested event ([fa75aa4](https://github.com/standardnotes/server/commit/fa75aa40f036dc3b9b4ed1364bffdbba6dec4da4))
|
||||
|
||||
## [2.94.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.94.0...@standardnotes/domain-events@2.94.1) (2022-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **domain-events:** remove not used event ([cb9499b](https://github.com/standardnotes/server/commit/cb9499b87f89ade166905fd6639ee330386c50b1))
|
||||
|
||||
# [2.94.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.93.0...@standardnotes/domain-events@2.94.0) (2022-12-06)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-events:** add mute emails setting changed event ([b7fb1d9](https://github.com/standardnotes/server/commit/b7fb1d9c08f66b0366f9af9cea8241f4c5ea9a18))
|
||||
|
||||
# [2.93.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.92.0...@standardnotes/domain-events@2.93.0) (2022-12-05)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-events:** add email subscription sync requested event ([fddf9fc](https://github.com/standardnotes/server/commit/fddf9fccbd92fa4279e97bcd2420ec9e270fedbb))
|
||||
|
||||
# [2.92.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.91.0...@standardnotes/domain-events@2.92.0) (2022-11-30)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.92.0",
|
||||
"version": "2.98.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { AccountClaimRequestedEventPayload } from './AccountClaimRequestedEventPayload'
|
||||
|
||||
export interface AccountClaimRequestedEvent extends DomainEventInterface {
|
||||
type: 'ACCOUNT_CLAIM_REQUESTED'
|
||||
payload: AccountClaimRequestedEventPayload
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface AccountClaimRequestedEventPayload {
|
||||
email: string
|
||||
token: string
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
|
||||
import { AccountResetRequestedEventPayload } from './AccountResetRequestedEventPayload'
|
||||
|
||||
export interface AccountResetRequestedEvent extends DomainEventInterface {
|
||||
type: 'ACCOUNT_RESET_REQUESTED'
|
||||
payload: AccountResetRequestedEventPayload
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface AccountResetRequestedEventPayload {
|
||||
resetRequestToken: string
|
||||
userEmail: string
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { ActivationCodeRequestedEventPayload } from './ActivationCodeRequestedEventPayload'
|
||||
|
||||
export interface ActivationCodeRequestedEvent extends DomainEventInterface {
|
||||
type: 'ACTIVATION_CODE_REQUESTED'
|
||||
payload: ActivationCodeRequestedEventPayload
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface ActivationCodeRequestedEventPayload {
|
||||
userEmail: string
|
||||
offlineFeaturesToken: string
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
|
||||
import { DiscountAppliedEventPayload } from './DiscountAppliedEventPayload'
|
||||
|
||||
export interface DiscountAppliedEvent extends DomainEventInterface {
|
||||
type: 'DISCOUNT_APPLIED'
|
||||
payload: DiscountAppliedEventPayload
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface DiscountAppliedEventPayload {
|
||||
userEmail: string
|
||||
discountRate: number
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { EmailMessageRequestedEventPayload } from './EmailMessageRequestedEventPayload'
|
||||
|
||||
export interface EmailMessageRequestedEvent extends DomainEventInterface {
|
||||
type: 'EMAIL_MESSAGE_REQUESTED'
|
||||
payload: EmailMessageRequestedEventPayload
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface EmailMessageRequestedEventPayload {
|
||||
userEmail: string
|
||||
messageIdentifier: string
|
||||
context: Record<string, unknown>
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { EmailRequestedEventPayload } from './EmailRequestedEventPayload'
|
||||
|
||||
export interface EmailRequestedEvent extends DomainEventInterface {
|
||||
type: 'EMAIL_REQUESTED'
|
||||
payload: EmailRequestedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface EmailRequestedEventPayload {
|
||||
userEmail: string
|
||||
messageIdentifier: string
|
||||
level: string
|
||||
subject: string
|
||||
body: string
|
||||
attachments?: Array<{
|
||||
filePath: string
|
||||
fileName: string
|
||||
attachmentFileName: string
|
||||
attachmentContentType: string
|
||||
}>
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
|
||||
import { InvoiceGeneratedEventPayload } from './InvoiceGeneratedEventPayload'
|
||||
|
||||
export interface InvoiceGeneratedEvent extends DomainEventInterface {
|
||||
type: 'INVOICE_GENERATED'
|
||||
payload: InvoiceGeneratedEventPayload
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export interface InvoiceGeneratedEventPayload {
|
||||
userEmail: string
|
||||
invoiceNumber: string
|
||||
paymentDateFormatted: string
|
||||
s3BucketName: string
|
||||
s3InvoiceObjectKey: string
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
|
||||
import { MuteEmailsSettingChangedEventPayload } from './MuteEmailsSettingChangedEventPayload'
|
||||
|
||||
export interface MuteEmailsSettingChangedEvent extends DomainEventInterface {
|
||||
type: 'MUTE_EMAILS_SETTING_CHANGED'
|
||||
payload: MuteEmailsSettingChangedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface MuteEmailsSettingChangedEventPayload {
|
||||
username: string
|
||||
mute: boolean
|
||||
emailSubscriptionRejectionLevel: string
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { UserSignedInEventPayload } from './UserSignedInEventPayload'
|
||||
|
||||
export interface UserSignedInEvent extends DomainEventInterface {
|
||||
type: 'USER_SIGNED_IN'
|
||||
payload: UserSignedInEventPayload
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
|
||||
export interface UserSignedInEventPayload {
|
||||
userUuid: string
|
||||
userEmail: string
|
||||
signInAlertEnabled: boolean
|
||||
muteSignInEmailsSettingUuid: Uuid
|
||||
device: string
|
||||
browser?: string
|
||||
}
|
||||
@@ -1,17 +1,9 @@
|
||||
export * from './Event/AccountClaimRequestedEvent'
|
||||
export * from './Event/AccountClaimRequestedEventPayload'
|
||||
export * from './Event/AccountDeletionRequestedEvent'
|
||||
export * from './Event/AccountDeletionRequestedEventPayload'
|
||||
export * from './Event/AccountResetRequestedEvent'
|
||||
export * from './Event/AccountResetRequestedEventPayload'
|
||||
export * from './Event/ActivationCodeRequestedEvent'
|
||||
export * from './Event/ActivationCodeRequestedEventPayload'
|
||||
export * from './Event/CloudBackupRequestedEvent'
|
||||
export * from './Event/CloudBackupRequestedEventPayload'
|
||||
export * from './Event/DailyAnalyticsReportGeneratedEvent'
|
||||
export * from './Event/DailyAnalyticsReportGeneratedEventPayload'
|
||||
export * from './Event/DiscountAppliedEvent'
|
||||
export * from './Event/DiscountAppliedEventPayload'
|
||||
export * from './Event/DiscountApplyRequestedEvent'
|
||||
export * from './Event/DiscountApplyRequestedEventPayload'
|
||||
export * from './Event/DiscountWithdrawRequestedEvent'
|
||||
@@ -28,8 +20,8 @@ export * from './Event/EmailBackupAttachmentCreatedEvent'
|
||||
export * from './Event/EmailBackupAttachmentCreatedEventPayload'
|
||||
export * from './Event/EmailBackupRequestedEvent'
|
||||
export * from './Event/EmailBackupRequestedEventPayload'
|
||||
export * from './Event/EmailMessageRequestedEvent'
|
||||
export * from './Event/EmailMessageRequestedEventPayload'
|
||||
export * from './Event/EmailRequestedEvent'
|
||||
export * from './Event/EmailRequestedEventPayload'
|
||||
export * from './Event/ExitDiscountAppliedEvent'
|
||||
export * from './Event/ExitDiscountAppliedEventPayload'
|
||||
export * from './Event/ExitDiscountApplyRequestedEvent'
|
||||
@@ -44,8 +36,6 @@ export * from './Event/FileUploadedEvent'
|
||||
export * from './Event/FileUploadedEventPayload'
|
||||
export * from './Event/GoogleDriveBackupFailedEvent'
|
||||
export * from './Event/GoogleDriveBackupFailedEventPayload'
|
||||
export * from './Event/InvoiceGeneratedEvent'
|
||||
export * from './Event/InvoiceGeneratedEventPayload'
|
||||
export * from './Event/ItemDumpedEvent'
|
||||
export * from './Event/ItemDumpedEventPayload'
|
||||
export * from './Event/ItemRevisionCreationRequestedEvent'
|
||||
@@ -58,6 +48,8 @@ export * from './Event/ListedAccountDeletedEvent'
|
||||
export * from './Event/ListedAccountDeletedEventPayload'
|
||||
export * from './Event/ListedAccountRequestedEvent'
|
||||
export * from './Event/ListedAccountRequestedEventPayload'
|
||||
export * from './Event/MuteEmailsSettingChangedEvent'
|
||||
export * from './Event/MuteEmailsSettingChangedEventPayload'
|
||||
export * from './Event/OfflineSubscriptionTokenCreatedEvent'
|
||||
export * from './Event/OfflineSubscriptionTokenCreatedEventPayload'
|
||||
export * from './Event/OneDriveBackupFailedEvent'
|
||||
@@ -116,8 +108,6 @@ export * from './Event/UserRegisteredEvent'
|
||||
export * from './Event/UserRegisteredEventPayload'
|
||||
export * from './Event/UserRolesChangedEvent'
|
||||
export * from './Event/UserRolesChangedEventPayload'
|
||||
export * from './Event/UserSignedInEvent'
|
||||
export * from './Event/UserSignedInEventPayload'
|
||||
export * from './Event/WebSocketMessageRequestedEvent'
|
||||
export * from './Event/WebSocketMessageRequestedEventPayload'
|
||||
export * from './Event/WorkspaceInviteAcceptedEvent'
|
||||
|
||||
@@ -3,6 +3,41 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.6.40](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.39...@standardnotes/event-store@1.6.40) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.6.39](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.38...@standardnotes/event-store@1.6.39) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.6.38](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.37...@standardnotes/event-store@1.6.38) (2022-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **event-store:** add email requested subscription ([eff0945](https://github.com/standardnotes/server/commit/eff09454c3a28b0124b74c2850fed19313b9e2b2))
|
||||
* **event-store:** reduce handlers ([473feba](https://github.com/standardnotes/server/commit/473feba6a8f008c9d73238be82e1d197082464c0))
|
||||
|
||||
## [1.6.37](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.36...@standardnotes/event-store@1.6.37) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.6.36](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.35...@standardnotes/event-store@1.6.36) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.6.35](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.34...@standardnotes/event-store@1.6.35) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.6.34](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.33...@standardnotes/event-store@1.6.34) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.6.33](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.32...@standardnotes/event-store@1.6.33) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.6.32](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.31...@standardnotes/event-store@1.6.32) (2022-11-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.6.32",
|
||||
"version": "1.6.40",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -78,7 +78,7 @@ export class ContainerConfigLoader {
|
||||
['LISTED_ACCOUNT_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
['LISTED_ACCOUNT_CREATED', container.get(TYPES.EventHandler)],
|
||||
['LISTED_ACCOUNT_DELETED', container.get(TYPES.EventHandler)],
|
||||
['USER_SIGNED_IN', container.get(TYPES.EventHandler)],
|
||||
['EMAIL_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
['SHARED_SUBSCRIPTION_INVITATION_CREATED', container.get(TYPES.EventHandler)],
|
||||
['EMAIL_BACKUP_ATTACHMENT_CREATED', container.get(TYPES.EventHandler)],
|
||||
['EMAIL_BACKUP_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.8.39](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.38...@standardnotes/files-server@1.8.39) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.8.38](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.37...@standardnotes/files-server@1.8.38) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.8.37](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.36...@standardnotes/files-server@1.8.37) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.8.36](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.35...@standardnotes/files-server@1.8.36) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.8.35](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.34...@standardnotes/files-server@1.8.35) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.8.34](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.33...@standardnotes/files-server@1.8.34) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.8.33](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.32...@standardnotes/files-server@1.8.33) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.8.32](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.31...@standardnotes/files-server@1.8.32) (2022-11-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.8.32",
|
||||
"version": "1.8.39",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,42 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.9.12](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.11...@standardnotes/revisions-server@1.9.12) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.9.11](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.10...@standardnotes/revisions-server@1.9.11) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.9.10](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.9...@standardnotes/revisions-server@1.9.10) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.9.9](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.8...@standardnotes/revisions-server@1.9.9) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.9.8](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.7...@standardnotes/revisions-server@1.9.8) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.9.7](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.6...@standardnotes/revisions-server@1.9.7) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.9.6](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.5...@standardnotes/revisions-server@1.9.6) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.9.5](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.4...@standardnotes/revisions-server@1.9.5) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.9.4](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.3...@standardnotes/revisions-server@1.9.4) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.9.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.2...@standardnotes/revisions-server@1.9.3) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.9.3",
|
||||
"version": "1.9.12",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.14.4](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.3...@standardnotes/scheduler-server@1.14.4) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.14.3](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.2...@standardnotes/scheduler-server@1.14.3) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.14.2](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.1...@standardnotes/scheduler-server@1.14.2) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.14.1](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.0...@standardnotes/scheduler-server@1.14.1) (2022-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **scheduler:** importing email contents ([c52bb93](https://github.com/standardnotes/server/commit/c52bb93d794447f04d3ea173f0aac9f26e4eba20))
|
||||
|
||||
# [1.14.0](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.37...@standardnotes/scheduler-server@1.14.0) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **scheduler:** add scheduled emails contents ([6e0855f](https://github.com/standardnotes/server/commit/6e0855f9b32c230c9ad5594fb6af6dd460300fc1))
|
||||
|
||||
## [1.13.37](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.36...@standardnotes/scheduler-server@1.13.37) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.13.36](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.35...@standardnotes/scheduler-server@1.13.36) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.13.35](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.34...@standardnotes/scheduler-server@1.13.35) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.13.34](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.33...@standardnotes/scheduler-server@1.13.34) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.13.33](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.32...@standardnotes/scheduler-server@1.13.33) (2022-11-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -7,4 +7,5 @@ module.exports = {
|
||||
transform: {
|
||||
...tsjPreset.transform,
|
||||
},
|
||||
coveragePathIgnorePatterns: ['/Bootstrap/', '/Infra/', '/Domain/Email/', '/Domain/Event/'],
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.13.33",
|
||||
"version": "1.14.4",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
@@ -27,6 +27,7 @@
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.19.0",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
"@standardnotes/predicates": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { html } from './encourage-email-backups.html'
|
||||
|
||||
export function getSubject(): string {
|
||||
return 'Enable email backups for your account'
|
||||
}
|
||||
|
||||
export function getBody(): string {
|
||||
return html
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { html } from './encourage-subscription-purchasing.html'
|
||||
|
||||
export function getSubject(): string {
|
||||
return 'Checking in after one month with Standard Notes'
|
||||
}
|
||||
|
||||
export function getBody(registrationDate: string): string {
|
||||
const body = html
|
||||
|
||||
return body.replace('%%REGISTRATION_DATE%%', registrationDate)
|
||||
}
|
||||
9
packages/scheduler/src/Domain/Email/ExitInterview.ts
Normal file
9
packages/scheduler/src/Domain/Email/ExitInterview.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { html } from './exit-interview.html'
|
||||
|
||||
export function getSubject(): string {
|
||||
return 'Can we ask why you canceled?'
|
||||
}
|
||||
|
||||
export function getBody(): string {
|
||||
return html
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export const html = `<div>
|
||||
<p>
|
||||
Did you know you can enable daily email backups for your account? This <strong>free</strong> feature sends an
|
||||
email to your inbox with an encrypted backup file including all your notes and tags.
|
||||
</p>
|
||||
<p>
|
||||
Email backups are an important feature that help protect you against worst-case scenarios. Your backups can be
|
||||
used to restore your account to a previous state, or to import old versions of notes into your present
|
||||
account.
|
||||
</p>
|
||||
<p>
|
||||
To enable free email backups, use the Standard Notes web or desktop app, and open Preferences > Backups > Email Backups.
|
||||
</p>
|
||||
|
||||
<a href="https://standardnotes.com/help/28/how-do-i-enable-daily-email-backups">
|
||||
Learn more about daily email backups →
|
||||
</a>
|
||||
</div>`
|
||||
@@ -0,0 +1,84 @@
|
||||
export const html = `<div>
|
||||
<p>Hi there,</p>
|
||||
<p>
|
||||
We hope you've been finding great use out of Standard Notes. We built Standard Notes to be a secure place for
|
||||
your most sensitive notes and files.
|
||||
</p>
|
||||
<p>
|
||||
As a reminder,
|
||||
<strong>
|
||||
<em>you signed up for the Standard Notes free plan on %%REGISTRATION_DATE%%</em>
|
||||
</strong>
|
||||
Your free account comes with standard features like end-to-end encryption, multiple-device sync, and
|
||||
two-factor authentication.
|
||||
</p>
|
||||
<p>
|
||||
If you're ready to advance your usage of Standard Notes, we recommend upgrading to one of our more powerful
|
||||
plans.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Productivity</strong> <strong>($59/year)</strong> powers up your editing experience with unique
|
||||
and special-built note-types for markdown, rich text, spreadsheets, todo, and more.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Professional</strong> <strong>($99/year)</strong> gives you all the power of Productivity plus
|
||||
100GB of end-to-end encrypted file storage for your private photos, videos, and documents, plus family
|
||||
subscription sharing with up to 5 people.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Professional comes with a 90-day money back guarantee, so if you're not completely satisfied, we're happy to
|
||||
refund your full purchase amount.
|
||||
</p>
|
||||
<p>
|
||||
<strong>
|
||||
<a href="https://standardnotes.com/plans">Upgrade your plan →</a>
|
||||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
<strong>
|
||||
<a href="https://standardnotes.com/features">Learn more about the features →</a>
|
||||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Questions & Answers</strong>
|
||||
</p>
|
||||
<p>
|
||||
<em>How does Standard Notes compare with conventional note-taking apps?</em>
|
||||
</p>
|
||||
<p>
|
||||
Data you store with Standard Notes is encrypted with end-to-end encryption using a key only you know. Because
|
||||
of this, we can't read your notes, and neither can anyone else.
|
||||
</p>
|
||||
<p>
|
||||
<em>What kind of notes should I store in Standard Notes?</em>
|
||||
</p>
|
||||
<p>
|
||||
This question can be reframed as: "What shouldn't I store in non-private services?" This would include
|
||||
sensitive/sensual data related to your health and wellness, secrets and keys, notes and documents with
|
||||
personally identifiable information that, if leaked, would lead to the theft of your identity, and business,
|
||||
financial, or legal information which cover non-public or confidential information.
|
||||
</p>
|
||||
<p>
|
||||
<em>Where can I access my notes?</em>
|
||||
</p>
|
||||
<p>
|
||||
Providing you with easy access to your notes and files on all your devices is a key focus for us. We provide
|
||||
secure and well-designed applications for your web browser, desktop (macOS, Windows, Linux,) and mobile
|
||||
(Android and iOS).
|
||||
</p>
|
||||
<p>
|
||||
<em>I have more questions.</em>
|
||||
</p>
|
||||
<p>
|
||||
We love questions. Head over to our Help page to see if your question is answered there. If not, reply
|
||||
directly to this email or send an email to <a href="help@standardnotes.com">help@standardnotes.com</a> and
|
||||
we'd be happy to help.
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
29
packages/scheduler/src/Domain/Email/exit-interview.html.ts
Normal file
29
packages/scheduler/src/Domain/Email/exit-interview.html.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export const html = `<div>
|
||||
<p>
|
||||
We're truly sad to see you leave. Our mission is simple: build the best, most private, and most secure
|
||||
note-taking app available. It's clear we've fallen short of your expectations somewhere along the way.
|
||||
</p>
|
||||
<p>
|
||||
We just want you to know—if price was the reason you canceled, we're not willing to lose you. That's no issue
|
||||
for us and we're happy to work out something that fits better with your budget. If price is your primary
|
||||
concern, please click the link below, and we'll get in touch with some options.
|
||||
</p>
|
||||
<a href="https://app.standardnotes.com/?user-request=exit-discount">Apply For A Limited Discount Offer →</a>
|
||||
<p>
|
||||
If you canceled for another reason, such as a missing feature, or a feature that wasn't behaving or working as
|
||||
you expected, please let us know! We build this product for you, and feedback from customers like yourself who
|
||||
are willing to pay for a product is most crucial for us as we continue to evolve and iterate on Standard
|
||||
Notes.
|
||||
</p>
|
||||
<p>If you have a minute, please fill out this brief exit interview: </p>
|
||||
<a href="https://standardnotes.typeform.com/to/dX5lzPtm">Short Exit Interview →</a>
|
||||
<p>
|
||||
Our team reads every single response, and your feedback will be shared with the relevant department within our
|
||||
team.
|
||||
</p>
|
||||
<p>
|
||||
If you have any other thoughts or questions, please feel free to reply directly to this email, and a member of
|
||||
our support team will be in touch with you.
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
@@ -1,223 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { EmailMessageIdentifier } from '@standardnotes/common'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { DomainEventFactory } from './DomainEventFactory'
|
||||
import { PredicateAuthority, PredicateName } from '@standardnotes/predicates'
|
||||
import { Job } from '../Job/Job'
|
||||
import { Predicate } from '../Predicate/Predicate'
|
||||
|
||||
describe('DomainEventFactory', () => {
|
||||
let timer: TimerInterface
|
||||
|
||||
const createFactory = () => new DomainEventFactory(timer)
|
||||
|
||||
beforeEach(() => {
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
|
||||
})
|
||||
|
||||
it('should create a DISCOUNT_APPLY_REQUESTED event', () => {
|
||||
expect(
|
||||
createFactory().createDiscountApplyRequestedEvent({
|
||||
userEmail: 'test@test.te',
|
||||
discountCode: 'off-10',
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: expect.any(Date),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: 'test@test.te',
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: 'scheduler',
|
||||
},
|
||||
payload: {
|
||||
userEmail: 'test@test.te',
|
||||
discountCode: 'off-10',
|
||||
},
|
||||
type: 'DISCOUNT_APPLY_REQUESTED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a DISCOUNT_WITHDRAW_REQUESTED event', () => {
|
||||
expect(
|
||||
createFactory().createDiscountWithdrawRequestedEvent({
|
||||
userEmail: 'test@test.te',
|
||||
discountCode: 'off-10',
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: expect.any(Date),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: 'test@test.te',
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: 'scheduler',
|
||||
},
|
||||
payload: {
|
||||
userEmail: 'test@test.te',
|
||||
discountCode: 'off-10',
|
||||
},
|
||||
type: 'DISCOUNT_WITHDRAW_REQUESTED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a EXIT_DISCOUNT_WITHDRAW_REQUESTED event', () => {
|
||||
expect(
|
||||
createFactory().createExitDiscountWithdrawRequestedEvent({
|
||||
userEmail: 'test@test.te',
|
||||
discountCode: 'exit-20',
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: expect.any(Date),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: 'test@test.te',
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: 'scheduler',
|
||||
},
|
||||
payload: {
|
||||
userEmail: 'test@test.te',
|
||||
discountCode: 'exit-20',
|
||||
},
|
||||
type: 'EXIT_DISCOUNT_WITHDRAW_REQUESTED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a EMAIL_MESSAGE_REQUESTED event', () => {
|
||||
expect(
|
||||
createFactory().createEmailMessageRequestedEvent({
|
||||
userEmail: 'test@test.te',
|
||||
messageIdentifier: EmailMessageIdentifier.ENCOURAGE_EMAIL_BACKUPS,
|
||||
context: {
|
||||
foo: 'bar',
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: expect.any(Date),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: 'test@test.te',
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: 'scheduler',
|
||||
},
|
||||
payload: {
|
||||
messageIdentifier: 'ENCOURAGE_EMAIL_BACKUPS',
|
||||
userEmail: 'test@test.te',
|
||||
context: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
type: 'EMAIL_MESSAGE_REQUESTED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a PREDICATE_VERIFICATION_REQUESTED event dedicated for auth', () => {
|
||||
expect(
|
||||
createFactory().createPredicateVerificationRequestedEvent(
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
userIdentifier: '2-3-4',
|
||||
userIdentifierType: 'uuid',
|
||||
} as jest.Mocked<Job>,
|
||||
{
|
||||
authority: PredicateAuthority.Auth,
|
||||
name: PredicateName.EmailBackupsEnabled,
|
||||
status: 'pending',
|
||||
} as jest.Mocked<Predicate>,
|
||||
),
|
||||
).toEqual({
|
||||
createdAt: expect.any(Date),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '2-3-4',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: 'scheduler',
|
||||
target: 'auth',
|
||||
},
|
||||
payload: {
|
||||
predicate: {
|
||||
authority: 'auth',
|
||||
jobUuid: '1-2-3',
|
||||
name: 'email-backups-enabled',
|
||||
},
|
||||
},
|
||||
type: 'PREDICATE_VERIFICATION_REQUESTED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a PREDICATE_VERIFICATION_REQUESTED event dedicated for syncing server', () => {
|
||||
expect(
|
||||
createFactory().createPredicateVerificationRequestedEvent(
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
userIdentifier: '2-3-4',
|
||||
userIdentifierType: 'uuid',
|
||||
} as jest.Mocked<Job>,
|
||||
{
|
||||
authority: PredicateAuthority.SyncingServer,
|
||||
name: PredicateName.EmailBackupsEnabled,
|
||||
status: 'pending',
|
||||
} as jest.Mocked<Predicate>,
|
||||
),
|
||||
).toEqual({
|
||||
createdAt: expect.any(Date),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '2-3-4',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: 'scheduler',
|
||||
target: 'syncing-server',
|
||||
},
|
||||
payload: {
|
||||
predicate: {
|
||||
authority: 'syncing-server',
|
||||
jobUuid: '1-2-3',
|
||||
name: 'email-backups-enabled',
|
||||
},
|
||||
},
|
||||
type: 'PREDICATE_VERIFICATION_REQUESTED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a PREDICATE_VERIFICATION_REQUESTED event dedicated for unknown target', () => {
|
||||
expect(
|
||||
createFactory().createPredicateVerificationRequestedEvent(
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
userIdentifier: '2-3-4',
|
||||
userIdentifierType: 'uuid',
|
||||
} as jest.Mocked<Job>,
|
||||
{
|
||||
authority: 'foobar' as PredicateAuthority,
|
||||
name: PredicateName.EmailBackupsEnabled,
|
||||
status: 'pending',
|
||||
} as jest.Mocked<Predicate>,
|
||||
),
|
||||
).toEqual({
|
||||
createdAt: expect.any(Date),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '2-3-4',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: 'scheduler',
|
||||
},
|
||||
payload: {
|
||||
predicate: {
|
||||
authority: 'foobar',
|
||||
jobUuid: '1-2-3',
|
||||
name: 'email-backups-enabled',
|
||||
},
|
||||
},
|
||||
type: 'PREDICATE_VERIFICATION_REQUESTED',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,8 @@
|
||||
import { EmailMessageIdentifier } from '@standardnotes/common'
|
||||
import {
|
||||
DiscountApplyRequestedEvent,
|
||||
DiscountWithdrawRequestedEvent,
|
||||
DomainEventService,
|
||||
EmailMessageRequestedEvent,
|
||||
EmailRequestedEvent,
|
||||
ExitDiscountWithdrawRequestedEvent,
|
||||
PredicateVerificationRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
@@ -70,13 +69,15 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
}
|
||||
}
|
||||
|
||||
createEmailMessageRequestedEvent(dto: {
|
||||
createEmailRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
messageIdentifier: EmailMessageIdentifier
|
||||
context: Record<string, unknown>
|
||||
}): EmailMessageRequestedEvent {
|
||||
messageIdentifier: string
|
||||
level: string
|
||||
body: string
|
||||
subject: string
|
||||
}): EmailRequestedEvent {
|
||||
return {
|
||||
type: 'EMAIL_MESSAGE_REQUESTED',
|
||||
type: 'EMAIL_REQUESTED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { EmailMessageIdentifier } from '@standardnotes/common'
|
||||
import {
|
||||
DiscountApplyRequestedEvent,
|
||||
DiscountWithdrawRequestedEvent,
|
||||
EmailMessageRequestedEvent,
|
||||
EmailRequestedEvent,
|
||||
ExitDiscountWithdrawRequestedEvent,
|
||||
PredicateVerificationRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
@@ -12,11 +11,13 @@ import { Predicate } from '../Predicate/Predicate'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createPredicateVerificationRequestedEvent(job: Job, predicate: Predicate): PredicateVerificationRequestedEvent
|
||||
createEmailMessageRequestedEvent(dto: {
|
||||
createEmailRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
messageIdentifier: EmailMessageIdentifier
|
||||
context: Record<string, unknown>
|
||||
}): EmailMessageRequestedEvent
|
||||
messageIdentifier: string
|
||||
level: string
|
||||
body: string
|
||||
subject: string
|
||||
}): EmailRequestedEvent
|
||||
createDiscountApplyRequestedEvent(dto: { userEmail: string; discountCode: string }): DiscountApplyRequestedEvent
|
||||
createDiscountWithdrawRequestedEvent(dto: { userEmail: string; discountCode: string }): DiscountWithdrawRequestedEvent
|
||||
createExitDiscountWithdrawRequestedEvent(dto: {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ExitDiscountWithdrawRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { PredicateName } from '@standardnotes/predicates'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import 'reflect-metadata'
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
@@ -26,13 +27,17 @@ describe('JobDoneInterpreter', () => {
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let job: Job
|
||||
let logger: Logger
|
||||
let timer: TimerInterface
|
||||
|
||||
const createInterpreter = () =>
|
||||
new JobDoneInterpreter(jobRepository, predicateRepository, domainEventFactory, domainEventPublisher, logger)
|
||||
new JobDoneInterpreter(jobRepository, predicateRepository, domainEventFactory, domainEventPublisher, timer, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
job = {} as jest.Mocked<Job>
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertMicrosecondsToDate = jest.fn().mockReturnValue(new Date())
|
||||
|
||||
jobRepository = {} as jest.Mocked<JobRepositoryInterface>
|
||||
jobRepository.findOneByUuid = jest.fn().mockReturnValue(job)
|
||||
|
||||
@@ -40,7 +45,7 @@ describe('JobDoneInterpreter', () => {
|
||||
predicateRepository.findByJobUuid = jest.fn().mockReturnValue([])
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createEmailMessageRequestedEvent = jest
|
||||
domainEventFactory.createEmailRequestedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<EmailMessageRequestedEvent>)
|
||||
domainEventFactory.createDiscountApplyRequestedEvent = jest
|
||||
@@ -89,11 +94,7 @@ describe('JobDoneInterpreter', () => {
|
||||
|
||||
await createInterpreter().interpret('1-2-3')
|
||||
|
||||
expect(domainEventFactory.createEmailMessageRequestedEvent).toHaveBeenCalledWith({
|
||||
context: {},
|
||||
messageIdentifier: 'ENCOURAGE_EMAIL_BACKUPS',
|
||||
userEmail: 'test@test.te',
|
||||
})
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -111,7 +112,7 @@ describe('JobDoneInterpreter', () => {
|
||||
|
||||
await createInterpreter().interpret('1-2-3')
|
||||
|
||||
expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -124,7 +125,7 @@ describe('JobDoneInterpreter', () => {
|
||||
|
||||
await createInterpreter().interpret('1-2-3')
|
||||
|
||||
expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -143,11 +144,7 @@ describe('JobDoneInterpreter', () => {
|
||||
|
||||
await createInterpreter().interpret('1-2-3')
|
||||
|
||||
expect(domainEventFactory.createEmailMessageRequestedEvent).toHaveBeenCalledWith({
|
||||
context: { userRegisteredAt: 123 },
|
||||
messageIdentifier: 'ENCOURAGE_SUBSCRIPTION_PURCHASING',
|
||||
userEmail: 'test@test.te',
|
||||
})
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -160,7 +157,7 @@ describe('JobDoneInterpreter', () => {
|
||||
|
||||
await createInterpreter().interpret('1-2-3')
|
||||
|
||||
expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -173,11 +170,7 @@ describe('JobDoneInterpreter', () => {
|
||||
|
||||
await createInterpreter().interpret('1-2-3')
|
||||
|
||||
expect(domainEventFactory.createEmailMessageRequestedEvent).toHaveBeenCalledWith({
|
||||
context: {},
|
||||
messageIdentifier: 'EXIT_INTERVIEW',
|
||||
userEmail: 'test@test.te',
|
||||
})
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -190,7 +183,7 @@ describe('JobDoneInterpreter', () => {
|
||||
|
||||
await createInterpreter().interpret('1-2-3')
|
||||
|
||||
expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -295,7 +288,7 @@ describe('JobDoneInterpreter', () => {
|
||||
|
||||
await createInterpreter().interpret('1-2-3')
|
||||
|
||||
expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { EmailMessageIdentifier } from '@standardnotes/common'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { PredicateName } from '@standardnotes/predicates'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
import { EmailLevel } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
@@ -13,6 +14,15 @@ import { Job } from './Job'
|
||||
import { JobDoneInterpreterInterface } from './JobDoneInterpreterInterface'
|
||||
import { JobName } from './JobName'
|
||||
import { JobRepositoryInterface } from './JobRepositoryInterface'
|
||||
import { getSubject as getExitInterviewSubject, getBody as getExitInterviewBody } from '../Email/ExitInterview'
|
||||
import {
|
||||
getSubject as getEncourageEmailBackupsSubject,
|
||||
getBody as getEncourageEmailBackupsBody,
|
||||
} from '../Email/EncourageEmailBackups'
|
||||
import {
|
||||
getSubject as getEncourageSubscriptionPurchasingSubject,
|
||||
getBody as getEncourageSubscriptionPurchasingBody,
|
||||
} from '../Email/EncourageSubscriptionPurchasing'
|
||||
|
||||
@injectable()
|
||||
export class JobDoneInterpreter implements JobDoneInterpreterInterface {
|
||||
@@ -21,6 +31,7 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
|
||||
@inject(TYPES.PredicateRepository) private predicateRepository: PredicateRepositoryInterface,
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -81,10 +92,12 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
|
||||
this.logger.debug(`[${job.uuid}]${job.name}: requesting email backup encouragement email.`)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createEmailMessageRequestedEvent({
|
||||
this.domainEventFactory.createEmailRequestedEvent({
|
||||
userEmail: job.userIdentifier,
|
||||
messageIdentifier: EmailMessageIdentifier.ENCOURAGE_EMAIL_BACKUPS,
|
||||
context: {},
|
||||
messageIdentifier: 'ENCOURAGE_EMAIL_BACKUPS',
|
||||
subject: getEncourageEmailBackupsSubject(),
|
||||
body: getEncourageEmailBackupsBody(),
|
||||
level: EmailLevel.LEVELS.System,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -93,12 +106,14 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
|
||||
this.logger.debug(`[${job.uuid}]${job.name}: requesting subscription purchase encouragement email.`)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createEmailMessageRequestedEvent({
|
||||
this.domainEventFactory.createEmailRequestedEvent({
|
||||
userEmail: job.userIdentifier,
|
||||
messageIdentifier: EmailMessageIdentifier.ENCOURAGE_SUBSCRIPTION_PURCHASING,
|
||||
context: {
|
||||
userRegisteredAt: job.createdAt,
|
||||
},
|
||||
messageIdentifier: 'ENCOURAGE_SUBSCRIPTION_PURCHASING',
|
||||
subject: getEncourageSubscriptionPurchasingSubject(),
|
||||
body: getEncourageSubscriptionPurchasingBody(
|
||||
this.timer.convertMicrosecondsToDate(job.createdAt).toLocaleString(),
|
||||
),
|
||||
level: EmailLevel.LEVELS.System,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -107,10 +122,12 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
|
||||
this.logger.debug(`[${job.uuid}]${job.name}: requesting exit interview email.`)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createEmailMessageRequestedEvent({
|
||||
this.domainEventFactory.createEmailRequestedEvent({
|
||||
userEmail: job.userIdentifier,
|
||||
messageIdentifier: EmailMessageIdentifier.EXIT_INTERVIEW,
|
||||
context: {},
|
||||
messageIdentifier: 'EXIT_INTERVIEW',
|
||||
subject: getExitInterviewSubject(),
|
||||
body: getExitInterviewBody(),
|
||||
level: EmailLevel.LEVELS.System,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,42 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.12](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.11...@standardnotes/syncing-server@1.20.12) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.20.11](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.10...@standardnotes/syncing-server@1.20.11) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.20.10](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.9...@standardnotes/syncing-server@1.20.10) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.20.9](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.8...@standardnotes/syncing-server@1.20.9) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.20.8](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.7...@standardnotes/syncing-server@1.20.8) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.20.7](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.6...@standardnotes/syncing-server@1.20.7) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.20.6](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.5...@standardnotes/syncing-server@1.20.6) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.20.5](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.4...@standardnotes/syncing-server@1.20.5) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.20.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.3...@standardnotes/syncing-server@1.20.4) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.20.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.2...@standardnotes/syncing-server@1.20.3) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.20.3",
|
||||
"version": "1.20.12",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.40](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.39...@standardnotes/websockets-server@1.4.40) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.4.39](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.38...@standardnotes/websockets-server@1.4.39) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.4.38](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.37...@standardnotes/websockets-server@1.4.38) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.4.37](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.36...@standardnotes/websockets-server@1.4.37) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.4.36](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.35...@standardnotes/websockets-server@1.4.36) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.4.35](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.34...@standardnotes/websockets-server@1.4.35) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.4.34](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.33...@standardnotes/websockets-server@1.4.34) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.4.33](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.32...@standardnotes/websockets-server@1.4.33) (2022-11-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/websockets-server",
|
||||
"version": "1.4.33",
|
||||
"version": "1.4.40",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.17.39](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.38...@standardnotes/workspace-server@1.17.39) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
## [1.17.38](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.37...@standardnotes/workspace-server@1.17.38) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
## [1.17.37](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.36...@standardnotes/workspace-server@1.17.37) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
## [1.17.36](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.35...@standardnotes/workspace-server@1.17.36) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
## [1.17.35](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.34...@standardnotes/workspace-server@1.17.35) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
## [1.17.34](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.33...@standardnotes/workspace-server@1.17.34) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
## [1.17.33](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.32...@standardnotes/workspace-server@1.17.33) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
## [1.17.32](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.31...@standardnotes/workspace-server@1.17.32) (2022-11-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/workspace-server",
|
||||
"version": "1.17.32",
|
||||
"version": "1.17.39",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -1888,6 +1888,7 @@ __metadata:
|
||||
"@sentry/node": "npm:^7.19.0"
|
||||
"@standardnotes/api": "npm:^1.19.0"
|
||||
"@standardnotes/common": "workspace:*"
|
||||
"@standardnotes/domain-core": "workspace:^"
|
||||
"@standardnotes/domain-events": "workspace:*"
|
||||
"@standardnotes/domain-events-infra": "workspace:*"
|
||||
"@standardnotes/features": "npm:^1.52.1"
|
||||
@@ -2283,6 +2284,7 @@ __metadata:
|
||||
"@newrelic/winston-enricher": "npm:^4.0.0"
|
||||
"@sentry/node": "npm:^7.19.0"
|
||||
"@standardnotes/common": "workspace:*"
|
||||
"@standardnotes/domain-core": "workspace:^"
|
||||
"@standardnotes/domain-events": "workspace:*"
|
||||
"@standardnotes/domain-events-infra": "workspace:*"
|
||||
"@standardnotes/predicates": "workspace:*"
|
||||
|
||||
Reference in New Issue
Block a user