feat(auth): add session traces

This commit is contained in:
Karol Sójko 2022-12-19 12:25:08 +01:00
parent f504a8288c
commit 8bcb552783
No known key found for this signature in database
GPG key ID: A50543BF560BDEB0
35 changed files with 793 additions and 234 deletions

62
.pnp.cjs generated
View file

@ -126,7 +126,7 @@ const RAW_RUNTIME_STATE =
["@lerna-lite/cli", "npm:1.6.0"],\
["@lerna-lite/list", "npm:1.6.0"],\
["@lerna-lite/run", "npm:1.6.0"],\
["@sentry/node", "npm:7.19.0"],\
["@sentry/node", "npm:7.27.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/node", "npm:18.11.9"],\
@ -2438,16 +2438,6 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@sentry/core", [\
["npm:7.19.0", {\
"packageLocation": "./.yarn/cache/@sentry-core-npm-7.19.0-151e6173ac-cabd7852ff.zip/node_modules/@sentry/core/",\
"packageDependencies": [\
["@sentry/core", "npm:7.19.0"],\
["@sentry/types", "npm:7.19.0"],\
["@sentry/utils", "npm:7.19.0"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.27.0", {\
"packageLocation": "./.yarn/cache/@sentry-core-npm-7.27.0-72a2ae90aa-1144287db2.zip/node_modules/@sentry/core/",\
"packageDependencies": [\
@ -2473,20 +2463,6 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@sentry/node", [\
["npm:7.19.0", {\
"packageLocation": "./.yarn/cache/@sentry-node-npm-7.19.0-fd3d8dbde1-3a69647d2e.zip/node_modules/@sentry/node/",\
"packageDependencies": [\
["@sentry/node", "npm:7.19.0"],\
["@sentry/core", "npm:7.19.0"],\
["@sentry/types", "npm:7.19.0"],\
["@sentry/utils", "npm:7.19.0"],\
["cookie", "npm:0.4.2"],\
["https-proxy-agent", "npm:5.0.1"],\
["lru_map", "npm:0.3.3"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.27.0", {\
"packageLocation": "./.yarn/cache/@sentry-node-npm-7.27.0-f1028265b5-b85cd47555.zip/node_modules/@sentry/node/",\
"packageDependencies": [\
@ -2533,13 +2509,6 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@sentry/types", [\
["npm:7.19.0", {\
"packageLocation": "./.yarn/cache/@sentry-types-npm-7.19.0-d6ed1960f2-541e1ef49a.zip/node_modules/@sentry/types/",\
"packageDependencies": [\
["@sentry/types", "npm:7.19.0"]\
],\
"linkType": "HARD"\
}],\
["npm:7.27.0", {\
"packageLocation": "./.yarn/cache/@sentry-types-npm-7.27.0-67702fc3e1-20ace8aa51.zip/node_modules/@sentry/types/",\
"packageDependencies": [\
@ -2549,15 +2518,6 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@sentry/utils", [\
["npm:7.19.0", {\
"packageLocation": "./.yarn/cache/@sentry-utils-npm-7.19.0-79844d4d90-50e4f391fe.zip/node_modules/@sentry/utils/",\
"packageDependencies": [\
["@sentry/utils", "npm:7.19.0"],\
["@sentry/types", "npm:7.19.0"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.27.0", {\
"packageLocation": "./.yarn/cache/@sentry-utils-npm-7.27.0-1935a93244-760c02397d.zip/node_modules/@sentry/utils/",\
"packageDependencies": [\
@ -2621,7 +2581,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/analytics", "workspace:packages/analytics"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.19.0"],\
["@sentry/node", "npm:7.27.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@ -2673,7 +2633,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/api-gateway", "workspace:packages/api-gateway"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.19.0"],\
["@sentry/node", "npm:7.27.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
@ -2729,7 +2689,9 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/auth-server", "workspace:packages/auth"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.19.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/profiling-node", "npm:0.0.12"],\
["@sentry/tracing", "npm:7.27.0"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
@ -2953,7 +2915,7 @@ const RAW_RUNTIME_STATE =
"packageLocation": "./packages/files/",\
"packageDependencies": [\
["@standardnotes/files-server", "workspace:packages/files"],\
["@sentry/node", "npm:7.19.0"],\
["@sentry/node", "npm:7.27.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/config", "npm:2.4.3"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@ -3088,7 +3050,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.19.0"],\
["@sentry/node", "npm:7.27.0"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
@ -3133,7 +3095,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/scheduler-server", "workspace:packages/scheduler"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.19.0"],\
["@sentry/node", "npm:7.27.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@ -3194,7 +3156,7 @@ const RAW_RUNTIME_STATE =
["@lerna-lite/cli", "npm:1.6.0"],\
["@lerna-lite/list", "npm:1.6.0"],\
["@lerna-lite/run", "npm:1.6.0"],\
["@sentry/node", "npm:7.19.0"],\
["@sentry/node", "npm:7.27.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/node", "npm:18.11.9"],\
@ -3362,7 +3324,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/websockets-server", "workspace:packages/websockets"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.19.0"],\
["@sentry/node", "npm:7.27.0"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@ -3402,7 +3364,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/workspace-server", "workspace:packages/workspace"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.19.0"],\
["@sentry/node", "npm:7.27.0"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\

View file

@ -61,7 +61,7 @@
},
"packageManager": "yarn@4.0.0-rc.25",
"dependencies": {
"@sentry/node": "^7.19.0",
"@sentry/node": "^7.27.0",
"newrelic": "^9.6.0"
}
}

View file

@ -38,7 +38,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.19.0",
"@sentry/node": "^7.27.0",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*",

View file

@ -21,7 +21,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.19.0",
"@sentry/node": "^7.27.0",
"@standardnotes/common": "workspace:^",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",

View file

@ -13,8 +13,8 @@ ENCRYPTION_SERVER_KEY=change-me-!
PORT=3000
DB_HOST=127.0.0.1
DB_REPLICA_HOST=127.0.0.1
DB_HOST=localhost
DB_REPLICA_HOST=localhost
DB_PORT=3306
DB_USERNAME=auth
DB_PASSWORD=changeme123

View file

@ -3,6 +3,8 @@ import 'reflect-metadata'
import 'newrelic'
import * as Sentry from '@sentry/node'
import * as Tracing from '@sentry/tracing'
import { ProfilingIntegration } from '@sentry/profiling-node'
import '../src/Controller/HealthCheckController'
import '../src/Controller/SessionController'
@ -24,7 +26,7 @@ import '../src/Infra/InversifyExpressUtils/InversifyExpressUserRequestsControlle
import '../src/Infra/InversifyExpressUtils/InversifyExpressWebSocketsController'
import * as cors from 'cors'
import { urlencoded, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'
import { urlencoded, json, Request, Response, NextFunction, ErrorRequestHandler } from 'express'
import * as winston from 'winston'
import * as dayjs from 'dayjs'
import * as utc from 'dayjs/plugin/utc'
@ -53,13 +55,28 @@ void container.load().then((container) => {
app.use(cors())
if (env.get('SENTRY_DSN', true)) {
const tracesSampleRate = env.get('SENTRY_TRACE_SAMPLE_RATE', true)
? +env.get('SENTRY_TRACE_SAMPLE_RATE', true)
: 0
const profilesSampleRate = env.get('SENTRY_PROFILES_SAMPLE_RATE', true)
? +env.get('SENTRY_PROFILES_SAMPLE_RATE', true)
: 0
Sentry.init({
dsn: env.get('SENTRY_DSN'),
integrations: [new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true })],
tracesSampleRate: 0,
integrations: [
new Sentry.Integrations.Http({ tracing: tracesSampleRate !== 0 }),
new ProfilingIntegration(),
new Tracing.Integrations.Express({
app,
}),
],
tracesSampleRate,
profilesSampleRate,
})
app.use(Sentry.Handlers.requestHandler() as RequestHandler)
app.use(Sentry.Handlers.requestHandler())
app.use(Sentry.Handlers.tracingHandler())
}
})

View file

@ -7,6 +7,6 @@ module.exports = {
transform: {
...tsjPreset.transform,
},
coveragePathIgnorePatterns: ['/Bootstrap/', '/InversifyExpressUtils/', '/Infra/', '/Projection/', '/Domain/Email/'],
coveragePathIgnorePatterns: ['/Bootstrap/', '/Infra/', '/Projection/', '/Domain/Email/', '/Mapping/'],
setupFilesAfterEnv: ['./test-setup.ts'],
}

View file

@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addSessionTraces1671448907955 implements MigrationInterface {
name = 'addSessionTraces1671448907955'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `session_traces` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `username` varchar(255) NOT NULL, `subscription_plan_name` varchar(64) NULL, `created_at` datetime NOT NULL, `creation_date` date NOT NULL, `expires_at` datetime NOT NULL, INDEX `subscription_plan_name` (`subscription_plan_name`), INDEX `creation_date` (`creation_date`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `creation_date` ON `session_traces`')
await queryRunner.query('DROP INDEX `subscription_plan_name` ON `session_traces`')
await queryRunner.query('DROP TABLE `session_traces`')
}
}

View file

@ -31,7 +31,9 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.19.0",
"@sentry/node": "^7.27.0",
"@sentry/profiling-node": "^0.0.12",
"@sentry/tracing": "^7.27.0",
"@standardnotes/api": "^1.19.0",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",

View file

@ -194,6 +194,13 @@ import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceTok
import { ProcessUserRequest } from '../Domain/UseCase/ProcessUserRequest/ProcessUserRequest'
import { UserRequestsController } from '../Controller/UserRequestsController'
import { EmailSubscriptionUnsubscribedEventHandler } from '../Domain/Handler/EmailSubscriptionUnsubscribedEventHandler'
import { SessionTraceRepositoryInterface } from '../Domain/Session/SessionTraceRepositoryInterface'
import { MySQLSessionTraceRepository } from '../Infra/MySQL/MySQLSessionTraceRepository'
import { MapperInterface } from '@standardnotes/domain-core'
import { SessionTracePersistenceMapper } from '../Mapping/SessionTracePersistenceMapper'
import { SessionTrace } from '../Domain/Session/SessionTrace'
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
import { TraceSession } from '../Domain/UseCase/TraceSession/TraceSession'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@ -231,6 +238,8 @@ export class ContainerConfigLoader {
})
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
if (env.get('SNS_TOPIC_ARN', true)) {
const snsConfig: AWS.SNS.Types.ClientConfiguration = {
apiVersion: 'latest',
@ -265,11 +274,47 @@ export class ContainerConfigLoader {
container.bind<AWS.SQS>(TYPES.SQS).toConstantValue(new AWS.SQS(sqsConfig))
}
// Mapping
container
.bind<MapperInterface<SessionTrace, TypeORMSessionTrace>>(TYPES.SessionTracePersistenceMapper)
.toConstantValue(new SessionTracePersistenceMapper())
// Controller
container.bind<AuthController>(TYPES.AuthController).to(AuthController)
container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
container.bind<UserRequestsController>(TYPES.UserRequestsController).to(UserRequestsController)
// ORM
container
.bind<Repository<OfflineSetting>>(TYPES.ORMOfflineSettingRepository)
.toConstantValue(AppDataSource.getRepository(OfflineSetting))
container
.bind<Repository<OfflineUserSubscription>>(TYPES.ORMOfflineUserSubscriptionRepository)
.toConstantValue(AppDataSource.getRepository(OfflineUserSubscription))
container
.bind<Repository<RevokedSession>>(TYPES.ORMRevokedSessionRepository)
.toConstantValue(AppDataSource.getRepository(RevokedSession))
container.bind<Repository<Role>>(TYPES.ORMRoleRepository).toConstantValue(AppDataSource.getRepository(Role))
container
.bind<Repository<Session>>(TYPES.ORMSessionRepository)
.toConstantValue(AppDataSource.getRepository(Session))
container
.bind<Repository<Setting>>(TYPES.ORMSettingRepository)
.toConstantValue(AppDataSource.getRepository(Setting))
container
.bind<Repository<SharedSubscriptionInvitation>>(TYPES.ORMSharedSubscriptionInvitationRepository)
.toConstantValue(AppDataSource.getRepository(SharedSubscriptionInvitation))
container
.bind<Repository<SubscriptionSetting>>(TYPES.ORMSubscriptionSettingRepository)
.toConstantValue(AppDataSource.getRepository(SubscriptionSetting))
container.bind<Repository<User>>(TYPES.ORMUserRepository).toConstantValue(AppDataSource.getRepository(User))
container
.bind<Repository<UserSubscription>>(TYPES.ORMUserSubscriptionRepository)
.toConstantValue(AppDataSource.getRepository(UserSubscription))
container
.bind<Repository<TypeORMSessionTrace>>(TYPES.ORMSessionTraceRepository)
.toConstantValue(AppDataSource.getRepository(TypeORMSessionTrace))
// Repositories
container.bind<SessionRepositoryInterface>(TYPES.SessionRepository).to(MySQLSessionRepository)
container.bind<RevokedSessionRepositoryInterface>(TYPES.RevokedSessionRepository).to(MySQLRevokedSessionRepository)
@ -300,34 +345,14 @@ export class ContainerConfigLoader {
.bind<SharedSubscriptionInvitationRepositoryInterface>(TYPES.SharedSubscriptionInvitationRepository)
.to(MySQLSharedSubscriptionInvitationRepository)
container.bind<PKCERepositoryInterface>(TYPES.PKCERepository).to(RedisPKCERepository)
// ORM
container
.bind<Repository<OfflineSetting>>(TYPES.ORMOfflineSettingRepository)
.toConstantValue(AppDataSource.getRepository(OfflineSetting))
container
.bind<Repository<OfflineUserSubscription>>(TYPES.ORMOfflineUserSubscriptionRepository)
.toConstantValue(AppDataSource.getRepository(OfflineUserSubscription))
container
.bind<Repository<RevokedSession>>(TYPES.ORMRevokedSessionRepository)
.toConstantValue(AppDataSource.getRepository(RevokedSession))
container.bind<Repository<Role>>(TYPES.ORMRoleRepository).toConstantValue(AppDataSource.getRepository(Role))
container
.bind<Repository<Session>>(TYPES.ORMSessionRepository)
.toConstantValue(AppDataSource.getRepository(Session))
container
.bind<Repository<Setting>>(TYPES.ORMSettingRepository)
.toConstantValue(AppDataSource.getRepository(Setting))
container
.bind<Repository<SharedSubscriptionInvitation>>(TYPES.ORMSharedSubscriptionInvitationRepository)
.toConstantValue(AppDataSource.getRepository(SharedSubscriptionInvitation))
container
.bind<Repository<SubscriptionSetting>>(TYPES.ORMSubscriptionSettingRepository)
.toConstantValue(AppDataSource.getRepository(SubscriptionSetting))
container.bind<Repository<User>>(TYPES.ORMUserRepository).toConstantValue(AppDataSource.getRepository(User))
container
.bind<Repository<UserSubscription>>(TYPES.ORMUserSubscriptionRepository)
.toConstantValue(AppDataSource.getRepository(UserSubscription))
.bind<SessionTraceRepositoryInterface>(TYPES.SessionTraceRepository)
.toConstantValue(
new MySQLSessionTraceRepository(
container.get(TYPES.ORMSessionTraceRepository),
container.get(TYPES.SessionTracePersistenceMapper),
),
)
// Middleware
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware)
@ -383,8 +408,97 @@ export class ContainerConfigLoader {
container.bind(TYPES.SYNCING_SERVER_URL).toConstantValue(env.get('SYNCING_SERVER_URL'))
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
container
.bind(TYPES.SESSION_TRACE_DAYS_TTL)
.toConstantValue(env.get('SESSION_TRACE_DAYS_TTL', true) ? +env.get('SESSION_TRACE_DAYS_TTL', true) : 90)
// Services
container.bind<UAParser>(TYPES.DeviceDetector).toConstantValue(new UAParser())
container.bind<SessionService>(TYPES.SessionService).to(SessionService)
container.bind<AuthResponseFactory20161215>(TYPES.AuthResponseFactory20161215).to(AuthResponseFactory20161215)
container.bind<AuthResponseFactory20190520>(TYPES.AuthResponseFactory20190520).to(AuthResponseFactory20190520)
container.bind<AuthResponseFactory20200115>(TYPES.AuthResponseFactory20200115).to(AuthResponseFactory20200115)
container.bind<AuthResponseFactoryResolver>(TYPES.AuthResponseFactoryResolver).to(AuthResponseFactoryResolver)
container.bind<KeyParamsFactory>(TYPES.KeyParamsFactory).to(KeyParamsFactory)
container
.bind<TokenDecoderInterface<SessionTokenData>>(TYPES.SessionTokenDecoder)
.toConstantValue(new TokenDecoder<SessionTokenData>(container.get(TYPES.JWT_SECRET)))
container
.bind<TokenDecoderInterface<SessionTokenData>>(TYPES.FallbackSessionTokenDecoder)
.toConstantValue(new TokenDecoder<SessionTokenData>(container.get(TYPES.LEGACY_JWT_SECRET)))
container
.bind<TokenDecoderInterface<CrossServiceTokenData>>(TYPES.CrossServiceTokenDecoder)
.toConstantValue(new TokenDecoder<CrossServiceTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
container
.bind<TokenDecoderInterface<OfflineUserTokenData>>(TYPES.OfflineUserTokenDecoder)
.toConstantValue(new TokenDecoder<OfflineUserTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
container
.bind<TokenDecoderInterface<WebSocketConnectionTokenData>>(TYPES.WebSocketConnectionTokenDecoder)
.toConstantValue(
new TokenDecoder<WebSocketConnectionTokenData>(container.get(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)),
)
container
.bind<TokenEncoderInterface<OfflineUserTokenData>>(TYPES.OfflineUserTokenEncoder)
.toConstantValue(new TokenEncoder<OfflineUserTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
container
.bind<TokenEncoderInterface<SessionTokenData>>(TYPES.SessionTokenEncoder)
.toConstantValue(new TokenEncoder<SessionTokenData>(container.get(TYPES.JWT_SECRET)))
container
.bind<TokenEncoderInterface<CrossServiceTokenData>>(TYPES.CrossServiceTokenEncoder)
.toConstantValue(new TokenEncoder<CrossServiceTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
container
.bind<TokenEncoderInterface<ValetTokenData>>(TYPES.ValetTokenEncoder)
.toConstantValue(new TokenEncoder<ValetTokenData>(container.get(TYPES.VALET_TOKEN_SECRET)))
container.bind<AuthenticationMethodResolver>(TYPES.AuthenticationMethodResolver).to(AuthenticationMethodResolver)
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
container.bind<AxiosInstance>(TYPES.HTTPClient).toConstantValue(axios.create())
container.bind<CrypterInterface>(TYPES.Crypter).to(CrypterNode)
container.bind<SettingServiceInterface>(TYPES.SettingService).to(SettingService)
container.bind<SubscriptionSettingServiceInterface>(TYPES.SubscriptionSettingService).to(SubscriptionSettingService)
container.bind<OfflineSettingServiceInterface>(TYPES.OfflineSettingService).to(OfflineSettingService)
container.bind<CryptoNode>(TYPES.CryptoNode).toConstantValue(new CryptoNode())
container.bind<ContentDecoderInterface>(TYPES.ContenDecoder).toConstantValue(new ContentDecoder())
container.bind<ClientServiceInterface>(TYPES.WebSocketsClientService).to(WebSocketsClientService)
container.bind<RoleServiceInterface>(TYPES.RoleService).to(RoleService)
container.bind<RoleToSubscriptionMapInterface>(TYPES.RoleToSubscriptionMap).to(RoleToSubscriptionMap)
container.bind<SettingsAssociationServiceInterface>(TYPES.SettingsAssociationService).to(SettingsAssociationService)
container
.bind<SubscriptionSettingsAssociationServiceInterface>(TYPES.SubscriptionSettingsAssociationService)
.to(SubscriptionSettingsAssociationService)
container.bind<FeatureServiceInterface>(TYPES.FeatureService).to(FeatureService)
container.bind<SettingInterpreterInterface>(TYPES.SettingInterpreter).to(SettingInterpreter)
container.bind<SettingDecrypterInterface>(TYPES.SettingDecrypter).to(SettingDecrypter)
container
.bind<SelectorInterface<ProtocolVersion>>(TYPES.ProtocolVersionSelector)
.toConstantValue(new DeterministicSelector<ProtocolVersion>())
container
.bind<SelectorInterface<boolean>>(TYPES.BooleanSelector)
.toConstantValue(new DeterministicSelector<boolean>())
container.bind<UserSubscriptionServiceInterface>(TYPES.UserSubscriptionService).to(UserSubscriptionService)
container.bind<ValidatorInterface<Uuid>>(TYPES.UuidValidator).toConstantValue(new UuidValidator())
if (env.get('SNS_TOPIC_ARN', true)) {
container
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
} else {
container
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
)
}
// use cases
container
.bind<TraceSession>(TYPES.TraceSession)
.toConstantValue(
new TraceSession(
container.get(TYPES.SessionTraceRepository),
container.get(TYPES.Timer),
container.get(TYPES.SESSION_TRACE_DAYS_TTL),
),
)
container.bind<AuthenticateUser>(TYPES.AuthenticateUser).to(AuthenticateUser)
container.bind<AuthenticateRequest>(TYPES.AuthenticateRequest).to(AuthenticateRequest)
container.bind<RefreshSessionToken>(TYPES.RefreshSessionToken).to(RefreshSessionToken)
@ -483,84 +597,6 @@ export class ContainerConfigLoader {
.bind<PredicateVerificationRequestedEventHandler>(TYPES.PredicateVerificationRequestedEventHandler)
.to(PredicateVerificationRequestedEventHandler)
// Services
container.bind<UAParser>(TYPES.DeviceDetector).toConstantValue(new UAParser())
container.bind<SessionService>(TYPES.SessionService).to(SessionService)
container.bind<AuthResponseFactory20161215>(TYPES.AuthResponseFactory20161215).to(AuthResponseFactory20161215)
container.bind<AuthResponseFactory20190520>(TYPES.AuthResponseFactory20190520).to(AuthResponseFactory20190520)
container.bind<AuthResponseFactory20200115>(TYPES.AuthResponseFactory20200115).to(AuthResponseFactory20200115)
container.bind<AuthResponseFactoryResolver>(TYPES.AuthResponseFactoryResolver).to(AuthResponseFactoryResolver)
container.bind<KeyParamsFactory>(TYPES.KeyParamsFactory).to(KeyParamsFactory)
container
.bind<TokenDecoderInterface<SessionTokenData>>(TYPES.SessionTokenDecoder)
.toConstantValue(new TokenDecoder<SessionTokenData>(container.get(TYPES.JWT_SECRET)))
container
.bind<TokenDecoderInterface<SessionTokenData>>(TYPES.FallbackSessionTokenDecoder)
.toConstantValue(new TokenDecoder<SessionTokenData>(container.get(TYPES.LEGACY_JWT_SECRET)))
container
.bind<TokenDecoderInterface<CrossServiceTokenData>>(TYPES.CrossServiceTokenDecoder)
.toConstantValue(new TokenDecoder<CrossServiceTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
container
.bind<TokenDecoderInterface<OfflineUserTokenData>>(TYPES.OfflineUserTokenDecoder)
.toConstantValue(new TokenDecoder<OfflineUserTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
container
.bind<TokenDecoderInterface<WebSocketConnectionTokenData>>(TYPES.WebSocketConnectionTokenDecoder)
.toConstantValue(
new TokenDecoder<WebSocketConnectionTokenData>(container.get(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)),
)
container
.bind<TokenEncoderInterface<OfflineUserTokenData>>(TYPES.OfflineUserTokenEncoder)
.toConstantValue(new TokenEncoder<OfflineUserTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
container
.bind<TokenEncoderInterface<SessionTokenData>>(TYPES.SessionTokenEncoder)
.toConstantValue(new TokenEncoder<SessionTokenData>(container.get(TYPES.JWT_SECRET)))
container
.bind<TokenEncoderInterface<CrossServiceTokenData>>(TYPES.CrossServiceTokenEncoder)
.toConstantValue(new TokenEncoder<CrossServiceTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
container
.bind<TokenEncoderInterface<ValetTokenData>>(TYPES.ValetTokenEncoder)
.toConstantValue(new TokenEncoder<ValetTokenData>(container.get(TYPES.VALET_TOKEN_SECRET)))
container.bind<AuthenticationMethodResolver>(TYPES.AuthenticationMethodResolver).to(AuthenticationMethodResolver)
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
container.bind<AxiosInstance>(TYPES.HTTPClient).toConstantValue(axios.create())
container.bind<CrypterInterface>(TYPES.Crypter).to(CrypterNode)
container.bind<SettingServiceInterface>(TYPES.SettingService).to(SettingService)
container.bind<SubscriptionSettingServiceInterface>(TYPES.SubscriptionSettingService).to(SubscriptionSettingService)
container.bind<OfflineSettingServiceInterface>(TYPES.OfflineSettingService).to(OfflineSettingService)
container.bind<CryptoNode>(TYPES.CryptoNode).toConstantValue(new CryptoNode())
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
container.bind<ContentDecoderInterface>(TYPES.ContenDecoder).toConstantValue(new ContentDecoder())
container.bind<ClientServiceInterface>(TYPES.WebSocketsClientService).to(WebSocketsClientService)
container.bind<RoleServiceInterface>(TYPES.RoleService).to(RoleService)
container.bind<RoleToSubscriptionMapInterface>(TYPES.RoleToSubscriptionMap).to(RoleToSubscriptionMap)
container.bind<SettingsAssociationServiceInterface>(TYPES.SettingsAssociationService).to(SettingsAssociationService)
container
.bind<SubscriptionSettingsAssociationServiceInterface>(TYPES.SubscriptionSettingsAssociationService)
.to(SubscriptionSettingsAssociationService)
container.bind<FeatureServiceInterface>(TYPES.FeatureService).to(FeatureService)
container.bind<SettingInterpreterInterface>(TYPES.SettingInterpreter).to(SettingInterpreter)
container.bind<SettingDecrypterInterface>(TYPES.SettingDecrypter).to(SettingDecrypter)
container
.bind<SelectorInterface<ProtocolVersion>>(TYPES.ProtocolVersionSelector)
.toConstantValue(new DeterministicSelector<ProtocolVersion>())
container
.bind<SelectorInterface<boolean>>(TYPES.BooleanSelector)
.toConstantValue(new DeterministicSelector<boolean>())
container.bind<UserSubscriptionServiceInterface>(TYPES.UserSubscriptionService).to(UserSubscriptionService)
container.bind<ValidatorInterface<Uuid>>(TYPES.UuidValidator).toConstantValue(new UuidValidator())
if (env.get('SNS_TOPIC_ARN', true)) {
container
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
} else {
container
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
)
}
container
.bind<EmailSubscriptionUnsubscribedEventHandler>(TYPES.EmailSubscriptionUnsubscribedEventHandler)
.toConstantValue(

View file

@ -10,6 +10,7 @@ import { SharedSubscriptionInvitation } from '../Domain/SharedSubscription/Share
import { OfflineUserSubscription } from '../Domain/Subscription/OfflineUserSubscription'
import { UserSubscription } from '../Domain/Subscription/UserSubscription'
import { User } from '../Domain/User/User'
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
import { Env } from './Env'
const env: Env = new Env()
@ -56,6 +57,7 @@ export const AppDataSource = new DataSource({
OfflineSetting,
SharedSubscriptionInvitation,
SubscriptionSetting,
TypeORMSessionTrace,
],
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
migrationsRun: true,

View file

@ -3,6 +3,8 @@ const TYPES = {
Redis: Symbol.for('Redis'),
SNS: Symbol.for('SNS'),
SQS: Symbol.for('SQS'),
// Mapping
SessionTracePersistenceMapper: Symbol.for('SessionTracePersistenceMapper'),
// Controller
AuthController: Symbol.for('AuthController'),
SubscriptionInvitesController: Symbol.for('SubscriptionInvitesController'),
@ -23,6 +25,7 @@ const TYPES = {
OfflineSubscriptionTokenRepository: Symbol.for('OfflineSubscriptionTokenRepository'),
SharedSubscriptionInvitationRepository: Symbol.for('SharedSubscriptionInvitationRepository'),
PKCERepository: Symbol.for('PKCERepository'),
SessionTraceRepository: Symbol.for('SessionTraceRepository'),
// ORM
ORMOfflineSettingRepository: Symbol.for('ORMOfflineSettingRepository'),
ORMOfflineUserSubscriptionRepository: Symbol.for('ORMOfflineUserSubscriptionRepository'),
@ -34,6 +37,7 @@ const TYPES = {
ORMSubscriptionSettingRepository: Symbol.for('ORMSubscriptionSettingRepository'),
ORMUserRepository: Symbol.for('ORMUserRepository'),
ORMUserSubscriptionRepository: Symbol.for('ORMUserSubscriptionRepository'),
ORMSessionTraceRepository: Symbol.for('ORMSessionTraceRepository'),
// Middleware
AuthMiddleware: Symbol.for('AuthMiddleware'),
ApiGatewayAuthMiddleware: Symbol.for('ApiGatewayAuthMiddleware'),
@ -81,6 +85,7 @@ const TYPES = {
SYNCING_SERVER_URL: Symbol.for('SYNCING_SERVER_URL'),
VERSION: Symbol.for('VERSION'),
PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'),
SESSION_TRACE_DAYS_TTL: Symbol.for('SESSION_TRACE_DAYS_TTL'),
// use cases
AuthenticateUser: Symbol.for('AuthenticateUser'),
AuthenticateRequest: Symbol.for('AuthenticateRequest'),
@ -119,6 +124,7 @@ const TYPES = {
VerifyPredicate: Symbol.for('VerifyPredicate'),
CreateCrossServiceToken: Symbol.for('CreateCrossServiceToken'),
ProcessUserRequest: Symbol.for('ProcessUserRequest'),
TraceSession: Symbol.for('TraceSession'),
// Handlers
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),

View file

@ -0,0 +1,17 @@
import { SubscriptionPlanName, Username, Uuid } from '@standardnotes/domain-core'
import { SessionTrace } from './SessionTrace'
describe('SessionTrace', () => {
it('should create an entity', () => {
const entityOrError = SessionTrace.create({
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
username: Username.create('foobar').getValue(),
subscriptionPlanName: SubscriptionPlanName.create(SubscriptionPlanName.NAMES.PlusPlan).getValue(),
createdAt: new Date(),
expiresAt: new Date(),
})
expect(entityOrError.isFailed()).toBeFalsy()
expect(entityOrError.getValue().id).not.toBeNull()
})
})

View file

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

View file

@ -0,0 +1,9 @@
import { SubscriptionPlanName, Username, Uuid } from '@standardnotes/domain-core'
export interface SessionTraceProps {
userUuid: Uuid
username: Username
createdAt: Date
expiresAt: Date
subscriptionPlanName: SubscriptionPlanName | null
}

View file

@ -0,0 +1,8 @@
import { Uuid } from '@standardnotes/domain-core'
import { SessionTrace } from './SessionTrace'
export interface SessionTraceRepositoryInterface {
save(sessionTrace: SessionTrace): Promise<void>
findOneByUserUuidAndDate(userUuid: Uuid, date: Date): Promise<SessionTrace | null>
}

View file

@ -8,6 +8,10 @@ import { Role } from '../../Role/Role'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
import { RoleToSubscriptionMapInterface } from '../../Role/RoleToSubscriptionMapInterface'
import { TraceSession } from '../TraceSession/TraceSession'
import { Logger } from 'winston'
import { Result, RoleName, SubscriptionPlanName } from '@standardnotes/domain-core'
describe('CreateCrossServiceToken', () => {
let userProjector: ProjectorInterface<User>
@ -15,6 +19,9 @@ describe('CreateCrossServiceToken', () => {
let roleProjector: ProjectorInterface<Role>
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
let userRepository: UserRepositoryInterface
let roleToSubscriptionMap: RoleToSubscriptionMapInterface
let traceSession: TraceSession
let logger: Logger
const jwtTTL = 60
let session: Session
@ -22,16 +29,29 @@ describe('CreateCrossServiceToken', () => {
let role: Role
const createUseCase = () =>
new CreateCrossServiceToken(userProjector, sessionProjector, roleProjector, tokenEncoder, userRepository, jwtTTL)
new CreateCrossServiceToken(
userProjector,
sessionProjector,
roleProjector,
tokenEncoder,
userRepository,
jwtTTL,
roleToSubscriptionMap,
traceSession,
logger,
)
beforeEach(() => {
session = {} as jest.Mocked<Session>
user = {} as jest.Mocked<User>
user = {
uuid: '1-2-3',
email: 'test@test.te',
} as jest.Mocked<User>
user.roles = Promise.resolve([role])
userProjector = {} as jest.Mocked<ProjectorInterface<User>>
userProjector.projectSimple = jest.fn().mockReturnValue({ bar: 'baz' })
userProjector.projectSimple = jest.fn().mockReturnValue({ uuid: '1-2-3', email: 'test@test.te' })
roleProjector = {} as jest.Mocked<ProjectorInterface<Role>>
roleProjector.projectSimple = jest.fn().mockReturnValue({ name: 'role1', uuid: '1-3-4' })
@ -45,6 +65,18 @@ describe('CreateCrossServiceToken', () => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
roleToSubscriptionMap = {} as jest.Mocked<RoleToSubscriptionMapInterface>
roleToSubscriptionMap.filterNonSubscriptionRoles = jest.fn().mockReturnValue([RoleName.NAMES.PlusUser])
roleToSubscriptionMap.getSubscriptionNameForRoleName = jest
.fn()
.mockReturnValue(SubscriptionPlanName.NAMES.PlusPlan)
traceSession = {} as jest.Mocked<TraceSession>
traceSession.execute = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
})
it('should create a cross service token for user', async () => {
@ -53,6 +85,11 @@ describe('CreateCrossServiceToken', () => {
session,
})
expect(traceSession.execute).toHaveBeenCalledWith({
userUuid: '1-2-3',
username: 'test@test.te',
subscriptionPlanName: 'PLUS_PLAN',
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
@ -65,7 +102,8 @@ describe('CreateCrossServiceToken', () => {
test: 'test',
},
user: {
bar: 'baz',
email: 'test@test.te',
uuid: '1-2-3',
},
},
60,
@ -86,7 +124,8 @@ describe('CreateCrossServiceToken', () => {
},
],
user: {
bar: 'baz',
email: 'test@test.te',
uuid: '1-2-3',
},
},
60,
@ -107,7 +146,8 @@ describe('CreateCrossServiceToken', () => {
},
],
user: {
bar: 'baz',
email: 'test@test.te',
uuid: '1-2-3',
},
},
60,
@ -128,4 +168,126 @@ describe('CreateCrossServiceToken', () => {
expect(caughtError).not.toBeNull()
})
it('should trace session without a subscription role', async () => {
roleToSubscriptionMap.filterNonSubscriptionRoles = jest.fn().mockReturnValue([])
await createUseCase().execute({
user,
session,
})
expect(traceSession.execute).toHaveBeenCalledWith({
userUuid: '1-2-3',
username: 'test@test.te',
subscriptionPlanName: null,
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
{
name: 'role1',
uuid: '1-3-4',
},
],
session: {
test: 'test',
},
user: {
email: 'test@test.te',
uuid: '1-2-3',
},
},
60,
)
})
it('should trace session without a subscription', async () => {
roleToSubscriptionMap.getSubscriptionNameForRoleName = jest.fn().mockReturnValue(undefined)
await createUseCase().execute({
user,
session,
})
expect(traceSession.execute).toHaveBeenCalledWith({
userUuid: '1-2-3',
username: 'test@test.te',
subscriptionPlanName: null,
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
{
name: 'role1',
uuid: '1-3-4',
},
],
session: {
test: 'test',
},
user: {
email: 'test@test.te',
uuid: '1-2-3',
},
},
60,
)
})
it('should create token if tracing session throws an error', async () => {
traceSession.execute = jest.fn().mockRejectedValue(new Error('test'))
await createUseCase().execute({
user,
session,
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
{
name: 'role1',
uuid: '1-3-4',
},
],
session: {
test: 'test',
},
user: {
email: 'test@test.te',
uuid: '1-2-3',
},
},
60,
)
})
it('should create token if tracing session fails', async () => {
traceSession.execute = jest.fn().mockReturnValue(Result.fail('Ooops'))
await createUseCase().execute({
user,
session,
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
{
name: 'role1',
uuid: '1-3-4',
},
],
session: {
test: 'test',
},
user: {
email: 'test@test.te',
uuid: '1-2-3',
},
},
60,
)
})
})

View file

@ -1,14 +1,18 @@
import { RoleName } from '@standardnotes/common'
import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
import { Role } from '../../Role/Role'
import { RoleToSubscriptionMapInterface } from '../../Role/RoleToSubscriptionMapInterface'
import { Session } from '../../Session/Session'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { TraceSession } from '../TraceSession/TraceSession'
import { UseCaseInterface } from '../UseCaseInterface'
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
import { CreateCrossServiceTokenResponse } from './CreateCrossServiceTokenResponse'
@ -21,6 +25,9 @@ export class CreateCrossServiceToken implements UseCaseInterface {
@inject(TYPES.CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.AUTH_JWT_TTL) private jwtTTL: number,
@inject(TYPES.RoleToSubscriptionMap) private roleToSubscriptionMap: RoleToSubscriptionMapInterface,
@inject(TYPES.TraceSession) private traceSession: TraceSession,
@inject(TYPES.Logger) private logger: Logger,
) {}
async execute(dto: CreateCrossServiceTokenDTO): Promise<CreateCrossServiceTokenResponse> {
@ -44,6 +51,19 @@ export class CreateCrossServiceToken implements UseCaseInterface {
authTokenData.session = this.projectSession(dto.session)
}
try {
const traceSessionResult = await this.traceSession.execute({
userUuid: user.uuid,
username: user.email,
subscriptionPlanName: this.getSubscriptionNameFromRoles(roles),
})
if (traceSessionResult.isFailed()) {
this.logger.error(traceSessionResult.getError())
}
} catch (error) {
this.logger.error(`Could not trace session while creating cross service token: ${(error as Error).message}`)
}
return {
token: this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL),
}
@ -80,4 +100,17 @@ export class CreateCrossServiceToken implements UseCaseInterface {
private projectRoles(roles: Array<Role>): Array<{ uuid: string; name: RoleName }> {
return roles.map((role) => <{ uuid: string; name: RoleName }>this.roleProjector.projectSimple(role))
}
private getSubscriptionNameFromRoles(roles: Array<Role>): string | null {
const nonSubscriptionRoles = this.roleToSubscriptionMap.filterNonSubscriptionRoles(roles)
if (nonSubscriptionRoles.length === 0) {
return null
}
const subscriptionName = this.roleToSubscriptionMap.getSubscriptionNameForRoleName(
nonSubscriptionRoles[0].name as RoleName,
)
return subscriptionName === undefined ? null : subscriptionName
}
}

View file

@ -0,0 +1,97 @@
import { Result } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { SessionTrace } from '../../Session/SessionTrace'
import { SessionTraceRepositoryInterface } from '../../Session/SessionTraceRepositoryInterface'
import { TraceSession } from './TraceSession'
describe('TraceSession', () => {
let sessionTraceRepository: SessionTraceRepositoryInterface
let timer: TimerInterface
const sessionTraceDaysTTL = 90
const createUseCase = () => new TraceSession(sessionTraceRepository, timer, sessionTraceDaysTTL)
beforeEach(() => {
sessionTraceRepository = {} as jest.Mocked<SessionTraceRepositoryInterface>
sessionTraceRepository.findOneByUserUuidAndDate = jest.fn().mockReturnValue(null)
sessionTraceRepository.save = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getUTCDateNDaysAhead = jest.fn().mockReturnValue(new Date())
})
it('should save a session trace', async () => {
const result = await createUseCase().execute({
userUuid: '0702b137-4f5c-438a-915e-8f8b46572ce5',
username: 'username',
subscriptionPlanName: 'PRO_PLAN',
})
expect(result.isFailed()).toBe(false)
expect(result.getValue().props.userUuid.value).toEqual('0702b137-4f5c-438a-915e-8f8b46572ce5')
expect(sessionTraceRepository.save).toHaveBeenCalledTimes(1)
})
it('should not save a session trace if one already exists for the same user and date', async () => {
sessionTraceRepository.findOneByUserUuidAndDate = jest.fn().mockReturnValue({} as jest.Mocked<SessionTrace>)
const result = await createUseCase().execute({
userUuid: '0702b137-4f5c-438a-915e-8f8b46572ce5',
username: 'username',
subscriptionPlanName: null,
})
expect(result.isFailed()).toBe(false)
expect(sessionTraceRepository.save).not.toHaveBeenCalled()
})
it('should return an error if userUuid is invalid', async () => {
const result = await createUseCase().execute({
userUuid: 'invalid',
username: 'username',
subscriptionPlanName: 'PRO_PLAN',
})
expect(result.isFailed()).toBe(true)
expect(sessionTraceRepository.save).not.toHaveBeenCalled()
})
it('should return an error if username is invalid', async () => {
const result = await createUseCase().execute({
userUuid: '0702b137-4f5c-438a-915e-8f8b46572ce5',
username: '',
subscriptionPlanName: 'PRO_PLAN',
})
expect(result.isFailed()).toBe(true)
expect(sessionTraceRepository.save).not.toHaveBeenCalled()
})
it('should return an error if subscriptionPlanName is invalid', async () => {
const result = await createUseCase().execute({
userUuid: '0702b137-4f5c-438a-915e-8f8b46572ce5',
username: 'username',
subscriptionPlanName: 'foobar',
})
expect(result.isFailed()).toBe(true)
expect(sessionTraceRepository.save).not.toHaveBeenCalled()
})
it('should not save a session trace if creating of the session trace fails', async () => {
const mock = jest.spyOn(SessionTrace, 'create')
mock.mockReturnValue(Result.fail('Oops'))
const result = await createUseCase().execute({
userUuid: '0702b137-4f5c-438a-915e-8f8b46572ce5',
username: 'username',
subscriptionPlanName: 'PRO_PLAN',
})
expect(result.isFailed()).toBe(true)
expect(sessionTraceRepository.save).not.toHaveBeenCalled()
mock.mockRestore()
})
})

View file

@ -0,0 +1,60 @@
import { Result, SubscriptionPlanName, UseCaseInterface, Username, Uuid } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { SessionTrace } from '../../Session/SessionTrace'
import { SessionTraceRepositoryInterface } from '../../Session/SessionTraceRepositoryInterface'
import { TraceSessionDTO } from './TraceSessionDTO'
export class TraceSession implements UseCaseInterface<SessionTrace> {
constructor(
private sessionTraceRepository: SessionTraceRepositoryInterface,
private timer: TimerInterface,
private sessionTraceDaysTTL: number,
) {}
async execute(dto: TraceSessionDTO): Promise<Result<SessionTrace>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(`Failed to trace session: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const usernameOrError = Username.create(dto.username)
if (usernameOrError.isFailed()) {
return Result.fail(`Failed to trace session: ${usernameOrError.getError()}`)
}
const username = usernameOrError.getValue()
let subscriptionPlanName = null
if (dto.subscriptionPlanName !== null) {
const subscriptionPlanNameOrError = SubscriptionPlanName.create(dto.subscriptionPlanName)
if (subscriptionPlanNameOrError.isFailed()) {
return Result.fail(`Failed to trace session: ${subscriptionPlanNameOrError.getError()}`)
}
subscriptionPlanName = subscriptionPlanNameOrError.getValue()
}
const alreadyExistingTrace = await this.sessionTraceRepository.findOneByUserUuidAndDate(userUuid, new Date())
if (alreadyExistingTrace !== null) {
return Result.ok<SessionTrace>(alreadyExistingTrace)
}
const expiresAt = this.timer.getUTCDateNDaysAhead(this.sessionTraceDaysTTL)
const sessionTraceOrError = SessionTrace.create({
userUuid,
username,
subscriptionPlanName,
createdAt: new Date(),
expiresAt,
})
if (sessionTraceOrError.isFailed()) {
return Result.fail(`Failed to trace session: ${sessionTraceOrError.getError()}`)
}
const sessionTrace = sessionTraceOrError.getValue()
await this.sessionTraceRepository.save(sessionTrace)
return Result.ok<SessionTrace>(sessionTrace)
}
}

View file

@ -0,0 +1,5 @@
export interface TraceSessionDTO {
userUuid: string
username: string
subscriptionPlanName: string | null
}

View file

@ -0,0 +1,34 @@
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { Repository } from 'typeorm'
import { SessionTrace } from '../../Domain/Session/SessionTrace'
import { SessionTraceRepositoryInterface } from '../../Domain/Session/SessionTraceRepositoryInterface'
import { TypeORMSessionTrace } from '../TypeORM/TypeORMSessionTrace'
export class MySQLSessionTraceRepository implements SessionTraceRepositoryInterface {
constructor(
private ormRepository: Repository<TypeORMSessionTrace>,
private mapper: MapperInterface<SessionTrace, TypeORMSessionTrace>,
) {}
async findOneByUserUuidAndDate(userUuid: Uuid, date: Date): Promise<SessionTrace | null> {
const typeOrm = await this.ormRepository
.createQueryBuilder('trace')
.where('trace.user_uuid = :userUuid AND trace.creation_date = :creationDate', {
userUuid: userUuid.value,
creationDate: new Date(date.getFullYear(), date.getMonth(), date.getDate()),
})
.getOne()
if (typeOrm === null) {
return null
}
return this.mapper.toDomain(typeOrm)
}
async save(sessionTrace: SessionTrace): Promise<void> {
const persistence = this.mapper.toProjection(sessionTrace)
await this.ormRepository.save(persistence)
}
}

View file

@ -0,0 +1,47 @@
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'session_traces' })
export class TypeORMSessionTrace {
@PrimaryGeneratedColumn('uuid')
declare uuid: string
@Column({
name: 'user_uuid',
length: 36,
})
declare userUuid: string
@Column({
name: 'username',
length: 255,
})
declare username: string
@Column({
name: 'subscription_plan_name',
length: 64,
type: 'varchar',
nullable: true,
})
@Index('subscription_plan_name')
declare subscriptionPlanName: string | null
@Column({
name: 'created_at',
type: 'datetime',
})
declare createdAt: Date
@Column({
name: 'creation_date',
type: 'date',
})
@Index('creation_date')
declare creationDate: Date
@Column({
name: 'expires_at',
type: 'datetime',
})
declare expiresAt: Date
}

View file

@ -0,0 +1,62 @@
import { MapperInterface, SubscriptionPlanName, UniqueEntityId, Username, Uuid } from '@standardnotes/domain-core'
import { SessionTrace } from '../Domain/Session/SessionTrace'
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
export class SessionTracePersistenceMapper implements MapperInterface<SessionTrace, TypeORMSessionTrace> {
toDomain(projection: TypeORMSessionTrace): SessionTrace {
const userUuidOrError = Uuid.create(projection.userUuid)
if (userUuidOrError.isFailed()) {
throw new Error('Failed to create Uuid from persistence.')
}
const userUuid = userUuidOrError.getValue()
const usernameOrError = Username.create(projection.username)
if (usernameOrError.isFailed()) {
throw new Error('Failed to create Username from persistence.')
}
const username = usernameOrError.getValue()
let subscriptionPlanName = null
if (projection.subscriptionPlanName !== null) {
const subscriptionPlanNameOrError = SubscriptionPlanName.create(projection.subscriptionPlanName)
if (subscriptionPlanNameOrError.isFailed()) {
throw new Error('Failed to create SubscriptionPlanName from persistence.')
}
subscriptionPlanName = subscriptionPlanNameOrError.getValue()
}
const sessionTraceOrError = SessionTrace.create(
{
userUuid,
username,
subscriptionPlanName,
createdAt: projection.createdAt,
expiresAt: projection.expiresAt,
},
new UniqueEntityId(projection.uuid),
)
if (sessionTraceOrError.isFailed()) {
throw new Error('Failed to create SessionTrace from persistence.')
}
return sessionTraceOrError.getValue()
}
toProjection(domain: SessionTrace): TypeORMSessionTrace {
const typeOrm = new TypeORMSessionTrace()
typeOrm.uuid = domain.id.toString()
typeOrm.userUuid = domain.props.userUuid.value
typeOrm.username = domain.props.username.value
typeOrm.subscriptionPlanName = domain.props.subscriptionPlanName ? domain.props.subscriptionPlanName.value : null
typeOrm.createdAt = domain.props.createdAt
typeOrm.creationDate = new Date(
domain.props.createdAt.getFullYear(),
domain.props.createdAt.getMonth(),
domain.props.createdAt.getDate(),
)
typeOrm.expiresAt = domain.props.expiresAt
return typeOrm
}
}

View file

@ -25,7 +25,7 @@
"upgrade:snjs": "yarn ncu -u '@standardnotes/*'"
},
"dependencies": {
"@sentry/node": "^7.19.0",
"@sentry/node": "^7.27.0",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",

View file

@ -24,7 +24,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.19.0",
"@sentry/node": "^7.27.0",
"@standardnotes/api": "^1.19.0",
"@standardnotes/common": "workspace:^",
"@standardnotes/domain-core": "workspace:^",

View file

@ -25,7 +25,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.19.0",
"@sentry/node": "^7.27.0",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*",

View file

@ -3,7 +3,7 @@ import 'reflect-metadata'
import 'newrelic'
import * as Sentry from '@sentry/node'
import '@sentry/tracing'
import * as Tracing from '@sentry/tracing'
import { ProfilingIntegration } from '@sentry/profiling-node'
import '../src/Controller/HealthCheckController'
@ -12,7 +12,7 @@ import '../src/Controller/ItemsController'
import helmet from 'helmet'
import * as cors from 'cors'
import { urlencoded, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'
import { urlencoded, json, Request, Response, NextFunction, ErrorRequestHandler } from 'express'
import * as winston from 'winston'
import { InversifyExpressServer } from 'inversify-express-utils'
@ -68,12 +68,19 @@ void container.load().then((container) => {
: 0
Sentry.init({
dsn: env.get('SENTRY_DSN'),
integrations: [new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true }), new ProfilingIntegration()],
integrations: [
new Sentry.Integrations.Http({ tracing: tracesSampleRate !== 0 }),
new ProfilingIntegration(),
new Tracing.Integrations.Express({
app,
}),
],
tracesSampleRate,
profilesSampleRate,
})
app.use(Sentry.Handlers.requestHandler() as RequestHandler)
app.use(Sentry.Handlers.requestHandler())
app.use(Sentry.Handlers.tracingHandler())
}
})

View file

@ -23,7 +23,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.19.0",
"@sentry/node": "^7.27.0",
"@standardnotes/api": "^1.19.0",
"@standardnotes/common": "workspace:^",
"@standardnotes/domain-events": "workspace:^",

View file

@ -23,7 +23,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.19.0",
"@sentry/node": "^7.27.0",
"@standardnotes/api": "^1.19.0",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",

View file

@ -1706,17 +1706,6 @@ __metadata:
languageName: node
linkType: hard
"@sentry/core@npm:7.19.0":
version: 7.19.0
resolution: "@sentry/core@npm:7.19.0"
dependencies:
"@sentry/types": "npm:7.19.0"
"@sentry/utils": "npm:7.19.0"
tslib: "npm:^1.9.3"
checksum: cabd7852ff9ea58287724678f232588f15d63437c7d0bad91543c91c821ac0f19a00835dd986645aed9d9094b2c0a8c82c1e9e4fb43342b9d31c6890010b2ec2
languageName: node
linkType: hard
"@sentry/core@npm:7.27.0":
version: 7.27.0
resolution: "@sentry/core@npm:7.27.0"
@ -1755,21 +1744,6 @@ __metadata:
languageName: node
linkType: hard
"@sentry/node@npm:^7.19.0":
version: 7.19.0
resolution: "@sentry/node@npm:7.19.0"
dependencies:
"@sentry/core": "npm:7.19.0"
"@sentry/types": "npm:7.19.0"
"@sentry/utils": "npm:7.19.0"
cookie: "npm:^0.4.1"
https-proxy-agent: "npm:^5.0.0"
lru_map: "npm:^0.3.3"
tslib: "npm:^1.9.3"
checksum: 3a69647d2ea2a8583bb77f3e30aa7cc587568b8e2f02d8ae7cf0c8928f03e4135fc3b9fcbdc90b35f190cb8973d70590fa7e4ef683d43e7e4a4fa50d22ac568a
languageName: node
linkType: hard
"@sentry/profiling-node@npm:^0.0.12":
version: 0.0.12
resolution: "@sentry/profiling-node@npm:0.0.12"
@ -1798,13 +1772,6 @@ __metadata:
languageName: node
linkType: hard
"@sentry/types@npm:7.19.0":
version: 7.19.0
resolution: "@sentry/types@npm:7.19.0"
checksum: 541e1ef49ae4482bbec605c3a2075a669930db2f707fafa431174010fcd0f2ba57637399822fbe0f4c5750696fe57e450d66c0182c1b7a0a3f4e6be9d030ed55
languageName: node
linkType: hard
"@sentry/types@npm:7.27.0, @sentry/types@npm:^7.16.0":
version: 7.27.0
resolution: "@sentry/types@npm:7.27.0"
@ -1812,16 +1779,6 @@ __metadata:
languageName: node
linkType: hard
"@sentry/utils@npm:7.19.0":
version: 7.19.0
resolution: "@sentry/utils@npm:7.19.0"
dependencies:
"@sentry/types": "npm:7.19.0"
tslib: "npm:^1.9.3"
checksum: 50e4f391fe9e6009f417c54a8ec96ba25ae77615d92c7f8c22a496901b9e3f182b78cecbc412bff0b2853812258642962b1ee38f42f9708e089756b4ebb3790e
languageName: node
linkType: hard
"@sentry/utils@npm:7.27.0, @sentry/utils@npm:^7.16.0":
version: 7.27.0
resolution: "@sentry/utils@npm:7.27.0"
@ -1876,7 +1833,7 @@ __metadata:
resolution: "@standardnotes/analytics@workspace:packages/analytics"
dependencies:
"@newrelic/winston-enricher": "npm:^4.0.0"
"@sentry/node": "npm:^7.19.0"
"@sentry/node": "npm:^7.27.0"
"@standardnotes/common": "workspace:*"
"@standardnotes/domain-core": "workspace:^"
"@standardnotes/domain-events": "workspace:*"
@ -1910,7 +1867,7 @@ __metadata:
resolution: "@standardnotes/api-gateway@workspace:packages/api-gateway"
dependencies:
"@newrelic/winston-enricher": "npm:^4.0.0"
"@sentry/node": "npm:^7.19.0"
"@sentry/node": "npm:^7.27.0"
"@standardnotes/common": "workspace:^"
"@standardnotes/domain-events": "workspace:*"
"@standardnotes/domain-events-infra": "workspace:*"
@ -1968,7 +1925,9 @@ __metadata:
resolution: "@standardnotes/auth-server@workspace:packages/auth"
dependencies:
"@newrelic/winston-enricher": "npm:^4.0.0"
"@sentry/node": "npm:^7.19.0"
"@sentry/node": "npm:^7.27.0"
"@sentry/profiling-node": "npm:^0.0.12"
"@sentry/tracing": "npm:^7.27.0"
"@standardnotes/api": "npm:^1.19.0"
"@standardnotes/common": "workspace:*"
"@standardnotes/domain-core": "workspace:^"
@ -2188,7 +2147,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@standardnotes/files-server@workspace:packages/files"
dependencies:
"@sentry/node": "npm:^7.19.0"
"@sentry/node": "npm:^7.27.0"
"@standardnotes/common": "workspace:*"
"@standardnotes/config": "npm:2.4.3"
"@standardnotes/domain-events": "workspace:*"
@ -2318,7 +2277,7 @@ __metadata:
resolution: "@standardnotes/revisions-server@workspace:packages/revisions"
dependencies:
"@newrelic/winston-enricher": "npm:^4.0.0"
"@sentry/node": "npm:^7.19.0"
"@sentry/node": "npm:^7.27.0"
"@standardnotes/api": "npm:^1.19.0"
"@standardnotes/common": "workspace:^"
"@standardnotes/domain-core": "workspace:^"
@ -2361,7 +2320,7 @@ __metadata:
resolution: "@standardnotes/scheduler-server@workspace:packages/scheduler"
dependencies:
"@newrelic/winston-enricher": "npm:^4.0.0"
"@sentry/node": "npm:^7.19.0"
"@sentry/node": "npm:^7.27.0"
"@standardnotes/common": "workspace:*"
"@standardnotes/domain-core": "workspace:^"
"@standardnotes/domain-events": "workspace:*"
@ -2418,7 +2377,7 @@ __metadata:
"@lerna-lite/cli": "npm:^1.5.1"
"@lerna-lite/list": "npm:^1.5.1"
"@lerna-lite/run": "npm:^1.5.1"
"@sentry/node": "npm:^7.19.0"
"@sentry/node": "npm:^7.27.0"
"@types/jest": "npm:^29.1.1"
"@types/newrelic": "npm:^7.0.4"
"@types/node": "npm:^18.11.9"
@ -2575,7 +2534,7 @@ __metadata:
resolution: "@standardnotes/websockets-server@workspace:packages/websockets"
dependencies:
"@newrelic/winston-enricher": "npm:^4.0.0"
"@sentry/node": "npm:^7.19.0"
"@sentry/node": "npm:^7.27.0"
"@standardnotes/api": "npm:^1.19.0"
"@standardnotes/common": "workspace:^"
"@standardnotes/domain-events": "workspace:^"
@ -2613,7 +2572,7 @@ __metadata:
resolution: "@standardnotes/workspace-server@workspace:packages/workspace"
dependencies:
"@newrelic/winston-enricher": "npm:^4.0.0"
"@sentry/node": "npm:^7.19.0"
"@sentry/node": "npm:^7.27.0"
"@standardnotes/api": "npm:^1.19.0"
"@standardnotes/common": "workspace:*"
"@standardnotes/domain-core": "workspace:^"