mirror of
https://github.com/standardnotes/server
synced 2026-01-17 05:04:27 -05:00
Compare commits
55 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
060206ddd4 | ||
|
|
0bc0909386 | ||
|
|
667d528a8c | ||
|
|
fa7fbe26e7 | ||
|
|
ba422a29d0 | ||
|
|
d220ec5bf7 | ||
|
|
7baf5492bc | ||
|
|
d5a8409bb5 | ||
|
|
f58f90667c | ||
|
|
a388e1a802 | ||
|
|
8811d10a73 | ||
|
|
c7a394cd1a | ||
|
|
b7615a7f2e | ||
|
|
1ca70c1e50 | ||
|
|
253cbb1d0c | ||
|
|
e38a16404c | ||
|
|
f17a1f875c | ||
|
|
2237e0f5df | ||
|
|
0df471585f | ||
|
|
95aac1a7bf | ||
|
|
c078bc958d | ||
|
|
49c27924ea | ||
|
|
c9dd8e7338 | ||
|
|
5ef90cc75b | ||
|
|
063c61d96c | ||
|
|
0cb5e36b20 | ||
|
|
319bab5b34 | ||
|
|
90a4f2111f | ||
|
|
3aba202970 | ||
|
|
c8974b7fa2 | ||
|
|
3654a19586 | ||
|
|
5f0929c1aa | ||
|
|
c0fa83bce6 | ||
|
|
c201ee42a0 | ||
|
|
e6a4cc3098 | ||
|
|
39f2fe2ba1 | ||
|
|
72ce190996 | ||
|
|
527dd1b61b | ||
|
|
af8feaadfe | ||
|
|
3164f76662 | ||
|
|
d6e531d4b6 | ||
|
|
af76878dad | ||
|
|
28cce39fe7 | ||
|
|
a8b806af08 | ||
|
|
fa0b0294b4 | ||
|
|
58ab410b0a | ||
|
|
51c8b20506 | ||
|
|
1e62a3760e | ||
|
|
2f569d4104 | ||
|
|
f23e444ed0 | ||
|
|
e6e9a32f03 | ||
|
|
8237df33a7 | ||
|
|
624b574013 | ||
|
|
d762bc89d1 | ||
|
|
f0cbec07b8 |
1
.github/ci.env
vendored
1
.github/ci.env
vendored
@@ -4,6 +4,7 @@ DB_USERNAME=std_notes_user
|
||||
DB_PASSWORD=changeme123
|
||||
DB_DATABASE=standard_notes_db
|
||||
DB_PORT=3306
|
||||
DB_SQLITE_DATABASE_PATH=standard_notes_db
|
||||
REDIS_PORT=6379
|
||||
REDIS_HOST=cache
|
||||
AUTH_SERVER_ACCESS_TOKEN_AGE=4
|
||||
|
||||
25
.github/workflows/common-e2e.yml
vendored
25
.github/workflows/common-e2e.yml
vendored
@@ -54,6 +54,11 @@ jobs:
|
||||
|
||||
e2e-home-server:
|
||||
name: (Home Server) E2E Test Suite
|
||||
strategy:
|
||||
matrix:
|
||||
db_type: [mysql, sqlite]
|
||||
cache_type: [redis, memory]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
@@ -61,6 +66,19 @@ jobs:
|
||||
image: standardnotes/snjs:${{ inputs.snjs_image_tag }}
|
||||
ports:
|
||||
- 9001:9001
|
||||
cache:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
db:
|
||||
image: mysql
|
||||
ports:
|
||||
- 3306:3306
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: standardnotes
|
||||
MYSQL_USER: standardnotes
|
||||
MYSQL_PASSWORD: standardnotes
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -90,6 +108,13 @@ jobs:
|
||||
echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env
|
||||
echo "REFRESH_TOKEN_AGE=7" >> packages/home-server/.env
|
||||
echo "REVISIONS_FREQUENCY=5" >> packages/home-server/.env
|
||||
echo "DB_HOST=db" >> packages/home-server/.env
|
||||
echo "DB_PORT=3306" >> packages/home-server/.env
|
||||
echo "DB_USERNAME=standardnotes" >> packages/home-server/.env
|
||||
echo "DB_PASSWORD=standardnotes" >> packages/home-server/.env
|
||||
echo "DB_TYPE=${{ matrix.db_type }}" >> packages/home-server/.env
|
||||
echo "REDIS_URL=redis://cache" >> packages/home-server/.env
|
||||
echo "CACHE_TYPE=${{ matrix.cache_type }}" >> packages/home-server/.env
|
||||
|
||||
- name: Run Server
|
||||
run: nohup yarn workspace @standardnotes/home-server start &
|
||||
|
||||
1
.pnp.cjs
generated
1
.pnp.cjs
generated
@@ -4802,6 +4802,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/event-store", "workspace:packages/event-store"],\
|
||||
["@aws-sdk/client-sqs", "npm:3.342.0"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
["@standardnotes/time", "workspace:packages/time"],\
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.24.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.24.3...@standardnotes/analytics@2.24.4) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.24.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.24.2...@standardnotes/analytics@2.24.3) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.24.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.24.1...@standardnotes/analytics@2.24.2) (2023-06-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.24.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.24.0...@standardnotes/analytics@2.24.1) (2023-06-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** streaming logs ([a8b806a](https://github.com/standardnotes/server/commit/a8b806af084b3e3fe8707ff0cb041a74042ee049))
|
||||
|
||||
# [2.24.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.23.7...@standardnotes/analytics@2.24.0) (2023-06-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **home-server:** add overriding environment variables in underlying services ([#621](https://github.com/standardnotes/server/issues/621)) ([f0cbec0](https://github.com/standardnotes/server/commit/f0cbec07b87d60dfad92072944553f76e0bea164))
|
||||
|
||||
## [2.23.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.23.6...@standardnotes/analytics@2.23.7) (2023-06-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.23.7",
|
||||
"version": "2.24.4",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -88,9 +88,9 @@ export class ContainerConfigLoader {
|
||||
}
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: env.get('LOG_LEVEL') || 'info',
|
||||
level: env.get('LOG_LEVEL', true) || 'info',
|
||||
format: winston.format.combine(...winstonFormatters),
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })],
|
||||
})
|
||||
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
|
||||
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
import { AbstractEnv } from '@standardnotes/domain-core'
|
||||
import { config, DotenvParseOutput } from 'dotenv'
|
||||
import { injectable } from 'inversify'
|
||||
|
||||
@injectable()
|
||||
export class Env {
|
||||
private env?: DotenvParseOutput
|
||||
|
||||
public load(): void {
|
||||
export class Env extends AbstractEnv {
|
||||
load(): void {
|
||||
const output = config()
|
||||
this.env = <DotenvParseOutput>output.parsed
|
||||
}
|
||||
|
||||
public get(key: string, optional = false): string {
|
||||
if (!this.env) {
|
||||
this.load()
|
||||
}
|
||||
|
||||
if (!process.env[key] && !optional) {
|
||||
throw new Error(`Environment variable ${key} not set`)
|
||||
}
|
||||
|
||||
return <string>process.env[key]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
MODE=microservice # microservice | home-server
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
VERSION=development
|
||||
|
||||
@@ -3,6 +3,58 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.65.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.65.0...@standardnotes/api-gateway@1.65.1) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.65.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.64.3...@standardnotes/api-gateway@1.65.0) (2023-06-30)
|
||||
|
||||
### Features
|
||||
|
||||
* shared vaults functionality in api-gateway,auth,files,common,security,domain-events. ([#629](https://github.com/standardnotes/api-gateway/issues/629)) ([fa7fbe2](https://github.com/standardnotes/api-gateway/commit/fa7fbe26e7b0707fc21d71e04af76870f5248baf))
|
||||
|
||||
## [1.64.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.64.2...@standardnotes/api-gateway@1.64.3) (2023-06-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add debug logs for invalid-auth responses ([d5a8409](https://github.com/standardnotes/api-gateway/commit/d5a8409bb5d35b9caf410a36ea0d5cb747129e8d))
|
||||
|
||||
## [1.64.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.64.1...@standardnotes/api-gateway@1.64.2) (2023-06-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** add debug logs about container initalizations ([0df4715](https://github.com/standardnotes/api-gateway/commit/0df471585fd5b4626ec2972f3b9a3e33b2830e65))
|
||||
|
||||
## [1.64.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.64.0...@standardnotes/api-gateway@1.64.1) (2023-06-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** direct call service proxy to return 400 responses instead of throwing errors ([e6a4cc3](https://github.com/standardnotes/api-gateway/commit/e6a4cc3098bdf84fc9d48ed0d9098ebb52afb0e7))
|
||||
|
||||
# [1.64.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.63.2...@standardnotes/api-gateway@1.64.0) (2023-06-05)
|
||||
|
||||
### Features
|
||||
|
||||
* **home-server:** allow running the home server with a mysql and redis configuration ([#622](https://github.com/standardnotes/api-gateway/issues/622)) ([d6e531d](https://github.com/standardnotes/api-gateway/commit/d6e531d4b6c1c80a894f6d7ec93632595268dd64))
|
||||
|
||||
## [1.63.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.63.1...@standardnotes/api-gateway@1.63.2) (2023-06-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** streaming logs ([a8b806a](https://github.com/standardnotes/api-gateway/commit/a8b806af084b3e3fe8707ff0cb041a74042ee049))
|
||||
|
||||
## [1.63.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.63.0...@standardnotes/api-gateway@1.63.1) (2023-06-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** add default for VERSION environment variable ([2f569d4](https://github.com/standardnotes/api-gateway/commit/2f569d41047a802eb72ef1a3618ffe4df28a709c))
|
||||
|
||||
# [1.63.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.62.4...@standardnotes/api-gateway@1.63.0) (2023-06-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **home-server:** add overriding environment variables in underlying services ([#621](https://github.com/standardnotes/api-gateway/issues/621)) ([f0cbec0](https://github.com/standardnotes/api-gateway/commit/f0cbec07b87d60dfad92072944553f76e0bea164))
|
||||
|
||||
## [1.62.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.62.3...@standardnotes/api-gateway@1.62.4) (2023-06-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -16,6 +16,8 @@ import '../src/Controller/v1/OfflineController'
|
||||
import '../src/Controller/v1/FilesController'
|
||||
import '../src/Controller/v1/SubscriptionInvitesController'
|
||||
import '../src/Controller/v1/AuthenticatorsController'
|
||||
import '../src/Controller/v1/AsymmetricMessagesController'
|
||||
import '../src/Controller/v1/SharedVaultsController'
|
||||
|
||||
import '../src/Controller/v2/PaymentsControllerV2'
|
||||
import '../src/Controller/v2/ActionsControllerV2'
|
||||
@@ -45,28 +47,29 @@ void container.load().then((container) => {
|
||||
response.setHeader('X-API-Gateway-Version', container.get(TYPES.VERSION))
|
||||
next()
|
||||
})
|
||||
/* eslint-disable */
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["https: 'self'"],
|
||||
baseUri: ["'self'"],
|
||||
childSrc: ["*", "blob:"],
|
||||
connectSrc: ["*"],
|
||||
fontSrc: ["*", "'self'"],
|
||||
formAction: ["'self'"],
|
||||
frameAncestors: ["*", "*.standardnotes.org", "*.standardnotes.com"],
|
||||
frameSrc: ["*", "blob:"],
|
||||
imgSrc: ["'self'", "*", "data:"],
|
||||
manifestSrc: ["'self'"],
|
||||
mediaSrc: ["'self'"],
|
||||
objectSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'"]
|
||||
}
|
||||
}
|
||||
}))
|
||||
/* eslint-enable */
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["https: 'self'"],
|
||||
baseUri: ["'self'"],
|
||||
childSrc: ['*', 'blob:'],
|
||||
connectSrc: ['*'],
|
||||
fontSrc: ['*', "'self'"],
|
||||
formAction: ["'self'"],
|
||||
frameAncestors: ['*', '*.standardnotes.org', '*.standardnotes.com'],
|
||||
frameSrc: ['*', 'blob:'],
|
||||
imgSrc: ["'self'", '*', 'data:'],
|
||||
manifestSrc: ["'self'"],
|
||||
mediaSrc: ["'self'"],
|
||||
objectSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
app.use(json({ limit: '50mb' }))
|
||||
app.use(
|
||||
text({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.62.4",
|
||||
"version": "1.65.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -24,13 +24,18 @@ import { OptionalCrossServiceTokenMiddleware } from '../Controller/OptionalCross
|
||||
import { Transform } from 'stream'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(configuration?: { serviceContainer?: ServiceContainerInterface; logger?: Transform }): Promise<Container> {
|
||||
const env: Env = new Env()
|
||||
async load(configuration?: {
|
||||
serviceContainer?: ServiceContainerInterface
|
||||
logger?: Transform
|
||||
environmentOverrides?: { [name: string]: string }
|
||||
}): Promise<Container> {
|
||||
const env: Env = new Env(configuration?.environmentOverrides)
|
||||
env.load()
|
||||
|
||||
const container = new Container()
|
||||
|
||||
const isConfiguredForHomeServer = env.get('CACHE_TYPE') === 'memory'
|
||||
const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server'
|
||||
const isConfiguredForInMemoryCache = env.get('CACHE_TYPE', true) === 'memory'
|
||||
|
||||
const winstonFormatters = [winston.format.splat(), winston.format.json()]
|
||||
if (env.get('NEW_RELIC_ENABLED', true) === 'true') {
|
||||
@@ -41,19 +46,20 @@ export class ContainerConfigLoader {
|
||||
winstonFormatters.push(newrelicWinstonFormatter())
|
||||
}
|
||||
|
||||
let logger: winston.Logger
|
||||
if (configuration?.logger) {
|
||||
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(configuration.logger as winston.Logger)
|
||||
logger = configuration.logger as winston.Logger
|
||||
} else {
|
||||
const logger = winston.createLogger({
|
||||
level: env.get('LOG_LEVEL') || 'info',
|
||||
logger = winston.createLogger({
|
||||
level: env.get('LOG_LEVEL', true) || 'info',
|
||||
format: winston.format.combine(...winstonFormatters),
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })],
|
||||
defaultMeta: { service: 'api-gateway' },
|
||||
})
|
||||
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
|
||||
}
|
||||
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
|
||||
|
||||
if (!isConfiguredForHomeServer) {
|
||||
if (!isConfiguredForInMemoryCache) {
|
||||
const redisUrl = env.get('REDIS_URL')
|
||||
const isRedisInClusterMode = redisUrl.indexOf(',') > 0
|
||||
let redis
|
||||
@@ -79,7 +85,7 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind(TYPES.HTTP_CALL_TIMEOUT)
|
||||
.toConstantValue(env.get('HTTP_CALL_TIMEOUT', true) ? +env.get('HTTP_CALL_TIMEOUT', true) : 60_000)
|
||||
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
|
||||
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
|
||||
container.bind(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL).toConstantValue(+env.get('CROSS_SERVICE_TOKEN_CACHE_TTL', true))
|
||||
|
||||
// Middleware
|
||||
@@ -120,6 +126,8 @@ export class ContainerConfigLoader {
|
||||
.bind<EndpointResolverInterface>(TYPES.EndpointResolver)
|
||||
.toConstantValue(new EndpointResolver(isConfiguredForHomeServer))
|
||||
|
||||
logger.debug('Configuration complete')
|
||||
|
||||
return container
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
import { AbstractEnv } from '@standardnotes/domain-core'
|
||||
import { config, DotenvParseOutput } from 'dotenv'
|
||||
import { injectable } from 'inversify'
|
||||
|
||||
@injectable()
|
||||
export class Env {
|
||||
private env?: DotenvParseOutput
|
||||
|
||||
public load(): void {
|
||||
export class Env extends AbstractEnv {
|
||||
load(): void {
|
||||
const output = config()
|
||||
this.env = <DotenvParseOutput>output.parsed
|
||||
}
|
||||
|
||||
public get(key: string, optional = false): string {
|
||||
if (!this.env) {
|
||||
this.load()
|
||||
}
|
||||
|
||||
if (!process.env[key] && !optional) {
|
||||
throw new Error(`Environment variable ${key} not set`)
|
||||
}
|
||||
|
||||
return <string>process.env[key]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import { ServiceContainerInterface, ServiceIdentifier, ServiceInterface } from '@standardnotes/domain-core'
|
||||
import {
|
||||
ServiceConfiguration,
|
||||
ServiceContainerInterface,
|
||||
ServiceIdentifier,
|
||||
ServiceInterface,
|
||||
} from '@standardnotes/domain-core'
|
||||
|
||||
import { ContainerConfigLoader } from './Container'
|
||||
import { Transform } from 'stream'
|
||||
|
||||
export class Service implements ServiceInterface {
|
||||
private logger: Transform | undefined
|
||||
|
||||
constructor(private serviceContainer: ServiceContainerInterface) {
|
||||
this.serviceContainer.register(this.getId(), this)
|
||||
}
|
||||
|
||||
setLogger(logger: Transform): void {
|
||||
this.logger = logger
|
||||
}
|
||||
|
||||
async handleRequest(_request: never, _response: never, _endpointOrMethodIdentifier: string): Promise<unknown> {
|
||||
throw new Error('Requests are handled via inversify-express at ApiGateway level')
|
||||
}
|
||||
|
||||
async getContainer(): Promise<unknown> {
|
||||
async getContainer(configuration?: ServiceConfiguration): Promise<unknown> {
|
||||
const config = new ContainerConfigLoader()
|
||||
|
||||
return config.load({
|
||||
serviceContainer: this.serviceContainer,
|
||||
logger: this.logger,
|
||||
logger: configuration?.logger,
|
||||
environmentOverrides: configuration?.environmentOverrides,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
private crossServiceTokenCacheTTL: number,
|
||||
private crossServiceTokenCache: CrossServiceTokenCacheInterface,
|
||||
private timer: TimerInterface,
|
||||
private logger: Logger,
|
||||
protected logger: Logger,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ export class RequiredCrossServiceTokenMiddleware extends AuthMiddleware {
|
||||
_next: NextFunction,
|
||||
): boolean {
|
||||
if (!authHeaderValue) {
|
||||
this.logger.debug('Missing auth header')
|
||||
|
||||
response.status(401).send({
|
||||
error: {
|
||||
tag: 'invalid-auth',
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from './SubscriptionTokenAuthMiddleware'
|
||||
export * from './TokenAuthenticationMethod'
|
||||
export * from './WebSocketAuthMiddleware'
|
||||
export * from './v1/ActionsController'
|
||||
export * from './v1/AsymmetricMessagesController'
|
||||
export * from './v1/AuthenticatorsController'
|
||||
export * from './v1/FilesController'
|
||||
export * from './v1/InvoicesController'
|
||||
@@ -12,6 +13,7 @@ export * from './v1/OfflineController'
|
||||
export * from './v1/PaymentsController'
|
||||
export * from './v1/RevisionsController'
|
||||
export * from './v1/SessionsController'
|
||||
export * from './v1/SharedVaultsController'
|
||||
export * from './v1/SubscriptionInvitesController'
|
||||
export * from './v1/TokensController'
|
||||
export * from './v1/UsersController'
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, all } from 'inversify-express-utils'
|
||||
import { TYPES } from '../../Bootstrap/Types'
|
||||
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
|
||||
|
||||
@controller('/v1/asymmetric-messages')
|
||||
export class AsymmetricMessagesController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.ServiceProxy) private serviceProxy: ServiceProxyInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@all('*', TYPES.RequiredCrossServiceTokenMiddleware)
|
||||
async subscriptions(request: Request, response: Response): Promise<void> {
|
||||
await this.serviceProxy.callSyncingServer(request, response, request.path.replace('/v1/', ''), request.body)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, all } from 'inversify-express-utils'
|
||||
import { TYPES } from '../../Bootstrap/Types'
|
||||
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
|
||||
|
||||
@controller('/v1/shared-vaults')
|
||||
export class SharedVaultsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.ServiceProxy) private serviceProxy: ServiceProxyInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@all('*', TYPES.RequiredCrossServiceTokenMiddleware)
|
||||
async subscriptions(request: Request, response: Response): Promise<void> {
|
||||
await this.serviceProxy.callSyncingServer(request, response, request.path.replace('/v1/', ''), request.body)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { ServiceContainerInterface, ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
|
||||
import { ServiceProxyInterface } from '../Http/ServiceProxyInterface'
|
||||
import { ServiceContainerInterface, ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
|
||||
export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
constructor(private serviceContainer: ServiceContainerInterface, private filesServerUrl: string) {}
|
||||
@@ -34,8 +34,12 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
}
|
||||
}
|
||||
|
||||
async callEmailServer(_request: Request, _response: Response, _endpointOrMethodIdentifier: string): Promise<void> {
|
||||
throw new Error('Email server is not available.')
|
||||
async callEmailServer(_request: Request, response: Response, _endpointOrMethodIdentifier: string): Promise<void> {
|
||||
response.status(400).send({
|
||||
error: {
|
||||
message: 'Email server is not available.',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async callAuthServer(request: never, response: never, endpointOrMethodIdentifier: string): Promise<void> {
|
||||
@@ -54,10 +58,14 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
|
||||
async callAuthServerWithLegacyFormat(
|
||||
_request: Request,
|
||||
_response: Response,
|
||||
response: Response,
|
||||
_endpointOrMethodIdentifier: string,
|
||||
): Promise<void> {
|
||||
throw new Error('Legacy auth endpoints are no longer available.')
|
||||
response.status(400).send({
|
||||
error: {
|
||||
message: 'Legacy auth endpoints are no longer available.',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async callRevisionsServer(request: never, response: never, endpointOrMethodIdentifier: string): Promise<void> {
|
||||
@@ -92,22 +100,30 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
|
||||
async callLegacySyncingServer(
|
||||
_request: Request,
|
||||
_response: Response,
|
||||
response: Response,
|
||||
_endpointOrMethodIdentifier: string,
|
||||
): Promise<void> {
|
||||
throw new Error('Legacy syncing server endpoints are no longer available.')
|
||||
response.status(400).send({
|
||||
error: {
|
||||
message: 'Legacy syncing server endpoints are no longer available.',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async callPaymentsServer(_request: Request, _response: Response, _endpointOrMethodIdentifier: string): Promise<void> {
|
||||
throw new Error('Payments server is not available.')
|
||||
async callPaymentsServer(_request: Request, response: Response, _endpointOrMethodIdentifier: string): Promise<void> {
|
||||
response.status(400).send({
|
||||
error: {
|
||||
message: 'Payments server is not available.',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async callWebSocketServer(
|
||||
_request: Request,
|
||||
_response: Response,
|
||||
_endpointOrMethodIdentifier: string,
|
||||
): Promise<void> {
|
||||
throw new Error('Websockets server is not available.')
|
||||
async callWebSocketServer(_request: Request, response: Response, _endpointOrMethodIdentifier: string): Promise<void> {
|
||||
response.status(400).send({
|
||||
error: {
|
||||
message: 'Websockets server is not available.',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private sendDecoratedResponse(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
MODE=microservice # microservice | home-server
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
VERSION=development
|
||||
@@ -20,8 +21,10 @@ DB_USERNAME=auth
|
||||
DB_PASSWORD=changeme123
|
||||
DB_DATABASE=auth
|
||||
DB_DEBUG_LEVEL=all # "all" | "query" | "schema" | "error" | "warn" | "info" | "log" | "migration"
|
||||
DB_TYPE=mysql
|
||||
|
||||
REDIS_URL=redis://cache
|
||||
CACHE_TYPE=redis
|
||||
|
||||
DISABLE_USER_REGISTRATION=false
|
||||
|
||||
|
||||
@@ -3,6 +3,86 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.120.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.120.0...@standardnotes/auth-server@1.120.1) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.120.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.119.6...@standardnotes/auth-server@1.120.0) (2023-06-30)
|
||||
|
||||
### Features
|
||||
|
||||
* shared vaults functionality in api-gateway,auth,files,common,security,domain-events. ([#629](https://github.com/standardnotes/server/issues/629)) ([fa7fbe2](https://github.com/standardnotes/server/commit/fa7fbe26e7b0707fc21d71e04af76870f5248baf))
|
||||
|
||||
## [1.119.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.119.5...@standardnotes/auth-server@1.119.6) (2023-06-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** add debug logs for authentication method resolver ([d220ec5](https://github.com/standardnotes/server/commit/d220ec5bf7509f9eb19dcda71c3667aaf388a35b))
|
||||
|
||||
## [1.119.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.119.4...@standardnotes/auth-server@1.119.5) (2023-06-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add debug logs for invalid-auth responses ([d5a8409](https://github.com/standardnotes/server/commit/d5a8409bb5d35b9caf410a36ea0d5cb747129e8d))
|
||||
|
||||
## [1.119.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.119.3...@standardnotes/auth-server@1.119.4) (2023-06-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.119.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.119.2...@standardnotes/auth-server@1.119.3) (2023-06-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** add debug logs about container initalizations ([0df4715](https://github.com/standardnotes/server/commit/0df471585fd5b4626ec2972f3b9a3e33b2830e65))
|
||||
|
||||
## [1.119.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.119.1...@standardnotes/auth-server@1.119.2) (2023-06-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** env var determining the sqlite database location ([#626](https://github.com/standardnotes/server/issues/626)) ([0cb5e36](https://github.com/standardnotes/server/commit/0cb5e36b20d9b095ea0edbcd877387e6c0069856))
|
||||
|
||||
## [1.119.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.119.0...@standardnotes/auth-server@1.119.1) (2023-06-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** add default value for valet token ttl ([c201ee4](https://github.com/standardnotes/server/commit/c201ee42a00d9e5402afea2f2c5848a362c1529e))
|
||||
|
||||
# [1.119.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.118.0...@standardnotes/auth-server@1.119.0) (2023-06-09)
|
||||
|
||||
### Features
|
||||
|
||||
* **home-server:** add activating premium features ([#624](https://github.com/standardnotes/server/issues/624)) ([72ce190](https://github.com/standardnotes/server/commit/72ce1909960fbd2ec6a47b8dbdfbe53a4f10e776))
|
||||
|
||||
# [1.118.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.117.0...@standardnotes/auth-server@1.118.0) (2023-06-07)
|
||||
|
||||
### Features
|
||||
|
||||
* configurable path for uploads and db ([#623](https://github.com/standardnotes/server/issues/623)) ([af8feaa](https://github.com/standardnotes/server/commit/af8feaadfe2dd58baab4cca217d6307b4a221326))
|
||||
|
||||
# [1.117.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.116.2...@standardnotes/auth-server@1.117.0) (2023-06-05)
|
||||
|
||||
### Features
|
||||
|
||||
* **home-server:** allow running the home server with a mysql and redis configuration ([#622](https://github.com/standardnotes/server/issues/622)) ([d6e531d](https://github.com/standardnotes/server/commit/d6e531d4b6c1c80a894f6d7ec93632595268dd64))
|
||||
|
||||
## [1.116.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.116.1...@standardnotes/auth-server@1.116.2) (2023-06-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** streaming logs ([a8b806a](https://github.com/standardnotes/server/commit/a8b806af084b3e3fe8707ff0cb041a74042ee049))
|
||||
|
||||
## [1.116.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.116.0...@standardnotes/auth-server@1.116.1) (2023-06-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* initializing data source with already configured environment ([624b574](https://github.com/standardnotes/server/commit/624b574013157e9e044d4a8ed53cadb7fcc567ae))
|
||||
|
||||
# [1.116.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.115.5...@standardnotes/auth-server@1.116.0) (2023-06-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **home-server:** add overriding environment variables in underlying services ([#621](https://github.com/standardnotes/server/issues/621)) ([f0cbec0](https://github.com/standardnotes/server/commit/f0cbec07b87d60dfad92072944553f76e0bea164))
|
||||
|
||||
## [1.115.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.115.4...@standardnotes/auth-server@1.115.5) (2023-06-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.115.5",
|
||||
"version": "1.120.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -32,7 +32,8 @@
|
||||
"weekly-backup:email": "yarn node dist/bin/backup.js email weekly",
|
||||
"content-recalculation": "yarn node dist/bin/content.js",
|
||||
"typeorm": "typeorm-ts-node-commonjs",
|
||||
"upgrade:snjs": "yarn ncu -u '@standardnotes/*'"
|
||||
"upgrade:snjs": "yarn ncu -u '@standardnotes/*'",
|
||||
"migrate": "yarn build && yarn typeorm migration:run -d dist/src/Bootstrap/DataSource.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sns": "^3.332.0",
|
||||
|
||||
5
packages/auth/src/Bootstrap/AuthServiceInterface.ts
Normal file
5
packages/auth/src/Bootstrap/AuthServiceInterface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Result, ServiceInterface } from '@standardnotes/domain-core'
|
||||
|
||||
export interface AuthServiceInterface extends ServiceInterface {
|
||||
activatePremiumFeatures(username: string): Promise<Result<string>>
|
||||
}
|
||||
@@ -251,38 +251,23 @@ import { HomeServerValetTokenController } from '../Infra/InversifyExpressUtils/H
|
||||
import { HomeServerWebSocketsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerWebSocketsController'
|
||||
import { HomeServerSessionsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSessionsController'
|
||||
import { Transform } from 'stream'
|
||||
import { ActivatePremiumFeatures } from '../Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(configuration?: {
|
||||
controllerConatiner?: ControllerContainerInterface
|
||||
directCallDomainEventPublisher?: DirectCallDomainEventPublisher
|
||||
logger?: Transform
|
||||
environmentOverrides?: { [name: string]: string }
|
||||
}): Promise<Container> {
|
||||
const directCallDomainEventPublisher =
|
||||
configuration?.directCallDomainEventPublisher ?? new DirectCallDomainEventPublisher()
|
||||
|
||||
const env: Env = new Env()
|
||||
const env: Env = new Env(configuration?.environmentOverrides)
|
||||
env.load()
|
||||
|
||||
const container = new Container()
|
||||
|
||||
await AppDataSource.initialize()
|
||||
|
||||
const isConfiguredForHomeServer = env.get('DB_TYPE') === 'sqlite'
|
||||
|
||||
if (!isConfiguredForHomeServer) {
|
||||
const redisUrl = env.get('REDIS_URL')
|
||||
const isRedisInClusterMode = redisUrl.indexOf(',') > 0
|
||||
let redis
|
||||
if (isRedisInClusterMode) {
|
||||
redis = new Redis.Cluster(redisUrl.split(','))
|
||||
} else {
|
||||
redis = new Redis(redisUrl)
|
||||
}
|
||||
|
||||
container.bind(TYPES.Auth_Redis).toConstantValue(redis)
|
||||
}
|
||||
|
||||
const winstonFormatters = [winston.format.splat(), winston.format.json()]
|
||||
if (env.get('NEW_RELIC_ENABLED', true) === 'true') {
|
||||
await import('newrelic')
|
||||
@@ -292,16 +277,38 @@ export class ContainerConfigLoader {
|
||||
winstonFormatters.push(newrelicWinstonFormatter())
|
||||
}
|
||||
|
||||
let logger: winston.Logger
|
||||
if (configuration?.logger) {
|
||||
container.bind<winston.Logger>(TYPES.Auth_Logger).toConstantValue(configuration.logger as winston.Logger)
|
||||
logger = configuration.logger as winston.Logger
|
||||
} else {
|
||||
const logger = winston.createLogger({
|
||||
level: env.get('LOG_LEVEL') || 'info',
|
||||
logger = winston.createLogger({
|
||||
level: env.get('LOG_LEVEL', true) || 'info',
|
||||
format: winston.format.combine(...winstonFormatters),
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })],
|
||||
defaultMeta: { service: 'auth' },
|
||||
})
|
||||
container.bind<winston.Logger>(TYPES.Auth_Logger).toConstantValue(logger)
|
||||
}
|
||||
container.bind<winston.Logger>(TYPES.Auth_Logger).toConstantValue(logger)
|
||||
|
||||
const appDataSource = new AppDataSource(env)
|
||||
await appDataSource.initialize()
|
||||
|
||||
logger.debug('Database initialized')
|
||||
|
||||
const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server'
|
||||
const isConfiguredForInMemoryCache = env.get('CACHE_TYPE', true) === 'memory'
|
||||
|
||||
if (!isConfiguredForInMemoryCache) {
|
||||
const redisUrl = env.get('REDIS_URL')
|
||||
const isRedisInClusterMode = redisUrl.indexOf(',') > 0
|
||||
let redis
|
||||
if (isRedisInClusterMode) {
|
||||
redis = new Redis.Cluster(redisUrl.split(','))
|
||||
} else {
|
||||
redis = new Redis(redisUrl)
|
||||
}
|
||||
|
||||
container.bind(TYPES.Auth_Redis).toConstantValue(redis)
|
||||
}
|
||||
|
||||
container.bind<TimerInterface>(TYPES.Auth_Timer).toConstantValue(new Timer())
|
||||
@@ -358,42 +365,42 @@ export class ContainerConfigLoader {
|
||||
// ORM
|
||||
container
|
||||
.bind<Repository<OfflineSetting>>(TYPES.Auth_ORMOfflineSettingRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(OfflineSetting))
|
||||
.toConstantValue(appDataSource.getRepository(OfflineSetting))
|
||||
container
|
||||
.bind<Repository<OfflineUserSubscription>>(TYPES.Auth_ORMOfflineUserSubscriptionRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(OfflineUserSubscription))
|
||||
.toConstantValue(appDataSource.getRepository(OfflineUserSubscription))
|
||||
container
|
||||
.bind<Repository<RevokedSession>>(TYPES.Auth_ORMRevokedSessionRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(RevokedSession))
|
||||
container.bind<Repository<Role>>(TYPES.Auth_ORMRoleRepository).toConstantValue(AppDataSource.getRepository(Role))
|
||||
.toConstantValue(appDataSource.getRepository(RevokedSession))
|
||||
container.bind<Repository<Role>>(TYPES.Auth_ORMRoleRepository).toConstantValue(appDataSource.getRepository(Role))
|
||||
container
|
||||
.bind<Repository<Session>>(TYPES.Auth_ORMSessionRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(Session))
|
||||
.toConstantValue(appDataSource.getRepository(Session))
|
||||
container
|
||||
.bind<Repository<Setting>>(TYPES.Auth_ORMSettingRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(Setting))
|
||||
.toConstantValue(appDataSource.getRepository(Setting))
|
||||
container
|
||||
.bind<Repository<SharedSubscriptionInvitation>>(TYPES.Auth_ORMSharedSubscriptionInvitationRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(SharedSubscriptionInvitation))
|
||||
.toConstantValue(appDataSource.getRepository(SharedSubscriptionInvitation))
|
||||
container
|
||||
.bind<Repository<SubscriptionSetting>>(TYPES.Auth_ORMSubscriptionSettingRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(SubscriptionSetting))
|
||||
container.bind<Repository<User>>(TYPES.Auth_ORMUserRepository).toConstantValue(AppDataSource.getRepository(User))
|
||||
.toConstantValue(appDataSource.getRepository(SubscriptionSetting))
|
||||
container.bind<Repository<User>>(TYPES.Auth_ORMUserRepository).toConstantValue(appDataSource.getRepository(User))
|
||||
container
|
||||
.bind<Repository<UserSubscription>>(TYPES.Auth_ORMUserSubscriptionRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(UserSubscription))
|
||||
.toConstantValue(appDataSource.getRepository(UserSubscription))
|
||||
container
|
||||
.bind<Repository<TypeORMSessionTrace>>(TYPES.Auth_ORMSessionTraceRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(TypeORMSessionTrace))
|
||||
.toConstantValue(appDataSource.getRepository(TypeORMSessionTrace))
|
||||
container
|
||||
.bind<Repository<TypeORMAuthenticator>>(TYPES.Auth_ORMAuthenticatorRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(TypeORMAuthenticator))
|
||||
.toConstantValue(appDataSource.getRepository(TypeORMAuthenticator))
|
||||
container
|
||||
.bind<Repository<TypeORMAuthenticatorChallenge>>(TYPES.Auth_ORMAuthenticatorChallengeRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(TypeORMAuthenticatorChallenge))
|
||||
.toConstantValue(appDataSource.getRepository(TypeORMAuthenticatorChallenge))
|
||||
container
|
||||
.bind<Repository<TypeORMCacheEntry>>(TYPES.Auth_ORMCacheEntryRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(TypeORMCacheEntry))
|
||||
.toConstantValue(appDataSource.getRepository(TypeORMCacheEntry))
|
||||
|
||||
// Repositories
|
||||
container.bind<SessionRepositoryInterface>(TYPES.Auth_SessionRepository).to(TypeORMSessionRepository)
|
||||
@@ -486,7 +493,9 @@ export class ContainerConfigLoader {
|
||||
.bind(TYPES.Auth_AUTH_JWT_TTL)
|
||||
.toConstantValue(env.get('AUTH_JWT_TTL', true) ? +env.get('AUTH_JWT_TTL') : 60_000)
|
||||
container.bind(TYPES.Auth_VALET_TOKEN_SECRET).toConstantValue(env.get('VALET_TOKEN_SECRET', true))
|
||||
container.bind(TYPES.Auth_VALET_TOKEN_TTL).toConstantValue(+env.get('VALET_TOKEN_TTL', true))
|
||||
container
|
||||
.bind(TYPES.Auth_VALET_TOKEN_TTL)
|
||||
.toConstantValue(env.get('VALET_TOKEN_TTL', true) ? +env.get('VALET_TOKEN_TTL', true) : 7200)
|
||||
container
|
||||
.bind(TYPES.Auth_WEB_SOCKET_CONNECTION_TOKEN_SECRET)
|
||||
.toConstantValue(env.get('WEB_SOCKET_CONNECTION_TOKEN_SECRET', true))
|
||||
@@ -548,7 +557,16 @@ export class ContainerConfigLoader {
|
||||
.bind(TYPES.Auth_READONLY_USERS)
|
||||
.toConstantValue(env.get('READONLY_USERS', true) ? env.get('READONLY_USERS', true).split(',') : [])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
if (isConfiguredForInMemoryCache) {
|
||||
container
|
||||
.bind<PKCERepositoryInterface>(TYPES.Auth_PKCERepository)
|
||||
.toConstantValue(
|
||||
new TypeORMPKCERepository(
|
||||
container.get(TYPES.Auth_CacheEntryRepository),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
container.get(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<LockRepositoryInterface>(TYPES.Auth_LockRepository)
|
||||
.toConstantValue(
|
||||
@@ -576,15 +594,6 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<PKCERepositoryInterface>(TYPES.Auth_PKCERepository)
|
||||
.toConstantValue(
|
||||
new TypeORMPKCERepository(
|
||||
container.get(TYPES.Auth_CacheEntryRepository),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
container.get(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SubscriptionTokenRepositoryInterface>(TYPES.Auth_SubscriptionTokenRepository)
|
||||
.toConstantValue(
|
||||
@@ -778,6 +787,16 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_CryptoNode),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ActivatePremiumFeatures>(TYPES.Auth_ActivatePremiumFeatures)
|
||||
.toConstantValue(
|
||||
new ActivatePremiumFeatures(
|
||||
container.get(TYPES.Auth_UserRepository),
|
||||
container.get(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get(TYPES.Auth_RoleService),
|
||||
container.get(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
|
||||
container
|
||||
.bind<CleanupSessionTraces>(TYPES.Auth_CleanupSessionTraces)
|
||||
@@ -1185,6 +1204,8 @@ export class ContainerConfigLoader {
|
||||
)
|
||||
}
|
||||
|
||||
logger.debug('Configuration complete')
|
||||
|
||||
return container
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataSource, LoggerOptions } from 'typeorm'
|
||||
import { DataSource, EntityTarget, LoggerOptions, ObjectLiteral, Repository } from 'typeorm'
|
||||
import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'
|
||||
import { Permission } from '../Domain/Permission/Permission'
|
||||
import { Role } from '../Domain/Role/Role'
|
||||
@@ -19,88 +19,102 @@ import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
|
||||
import { Env } from './Env'
|
||||
import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
|
||||
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
export class AppDataSource {
|
||||
private dataSource: DataSource | undefined
|
||||
|
||||
const isConfiguredForMySQL = env.get('DB_TYPE') === 'mysql'
|
||||
constructor(private env: Env) {}
|
||||
|
||||
const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
: 45_000
|
||||
getRepository<Entity extends ObjectLiteral>(target: EntityTarget<Entity>): Repository<Entity> {
|
||||
if (!this.dataSource) {
|
||||
throw new Error('DataSource not initialized')
|
||||
}
|
||||
|
||||
const commonDataSourceOptions = {
|
||||
maxQueryExecutionTime,
|
||||
entities: [
|
||||
User,
|
||||
UserSubscription,
|
||||
OfflineUserSubscription,
|
||||
Session,
|
||||
RevokedSession,
|
||||
Role,
|
||||
Permission,
|
||||
Setting,
|
||||
OfflineSetting,
|
||||
SharedSubscriptionInvitation,
|
||||
SubscriptionSetting,
|
||||
TypeORMSessionTrace,
|
||||
TypeORMAuthenticator,
|
||||
TypeORMAuthenticatorChallenge,
|
||||
TypeORMEmergencyAccessInvitation,
|
||||
TypeORMCacheEntry,
|
||||
],
|
||||
migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
|
||||
migrationsRun: true,
|
||||
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL', true) ?? 'info',
|
||||
return this.dataSource.getRepository(target)
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
this.env.load()
|
||||
|
||||
const isConfiguredForMySQL = this.env.get('DB_TYPE') === 'mysql'
|
||||
|
||||
const maxQueryExecutionTime = this.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
? +this.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
: 45_000
|
||||
|
||||
const commonDataSourceOptions = {
|
||||
maxQueryExecutionTime,
|
||||
entities: [
|
||||
User,
|
||||
UserSubscription,
|
||||
OfflineUserSubscription,
|
||||
Session,
|
||||
RevokedSession,
|
||||
Role,
|
||||
Permission,
|
||||
Setting,
|
||||
OfflineSetting,
|
||||
SharedSubscriptionInvitation,
|
||||
SubscriptionSetting,
|
||||
TypeORMSessionTrace,
|
||||
TypeORMAuthenticator,
|
||||
TypeORMAuthenticatorChallenge,
|
||||
TypeORMEmergencyAccessInvitation,
|
||||
TypeORMCacheEntry,
|
||||
],
|
||||
migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
|
||||
migrationsRun: true,
|
||||
logging: <LoggerOptions>this.env.get('DB_DEBUG_LEVEL', true) ?? 'info',
|
||||
}
|
||||
|
||||
if (isConfiguredForMySQL) {
|
||||
const inReplicaMode = this.env.get('DB_REPLICA_HOST', true) ? true : false
|
||||
|
||||
const replicationConfig = {
|
||||
master: {
|
||||
host: this.env.get('DB_HOST'),
|
||||
port: parseInt(this.env.get('DB_PORT')),
|
||||
username: this.env.get('DB_USERNAME'),
|
||||
password: this.env.get('DB_PASSWORD'),
|
||||
database: this.env.get('DB_DATABASE'),
|
||||
},
|
||||
slaves: [
|
||||
{
|
||||
host: this.env.get('DB_REPLICA_HOST', true),
|
||||
port: parseInt(this.env.get('DB_PORT')),
|
||||
username: this.env.get('DB_USERNAME'),
|
||||
password: this.env.get('DB_PASSWORD'),
|
||||
database: this.env.get('DB_DATABASE'),
|
||||
},
|
||||
],
|
||||
removeNodeErrorCount: 10,
|
||||
restoreNodeTimeout: 5,
|
||||
}
|
||||
|
||||
const mySQLDataSourceOptions: MysqlConnectionOptions = {
|
||||
...commonDataSourceOptions,
|
||||
type: 'mysql',
|
||||
charset: 'utf8mb4',
|
||||
supportBigNumbers: true,
|
||||
bigNumberStrings: false,
|
||||
replication: inReplicaMode ? replicationConfig : undefined,
|
||||
host: inReplicaMode ? undefined : this.env.get('DB_HOST'),
|
||||
port: inReplicaMode ? undefined : parseInt(this.env.get('DB_PORT')),
|
||||
username: inReplicaMode ? undefined : this.env.get('DB_USERNAME'),
|
||||
password: inReplicaMode ? undefined : this.env.get('DB_PASSWORD'),
|
||||
database: inReplicaMode ? undefined : this.env.get('DB_DATABASE'),
|
||||
}
|
||||
|
||||
this.dataSource = new DataSource(mySQLDataSourceOptions)
|
||||
} else {
|
||||
const sqliteDataSourceOptions: SqliteConnectionOptions = {
|
||||
...commonDataSourceOptions,
|
||||
type: 'sqlite',
|
||||
database: this.env.get('DB_SQLITE_DATABASE_PATH'),
|
||||
}
|
||||
|
||||
this.dataSource = new DataSource(sqliteDataSourceOptions)
|
||||
}
|
||||
|
||||
await this.dataSource.initialize()
|
||||
}
|
||||
}
|
||||
|
||||
let dataSource: DataSource
|
||||
if (isConfiguredForMySQL) {
|
||||
const inReplicaMode = env.get('DB_REPLICA_HOST', true) ? true : false
|
||||
|
||||
const replicationConfig = {
|
||||
master: {
|
||||
host: env.get('DB_HOST'),
|
||||
port: parseInt(env.get('DB_PORT')),
|
||||
username: env.get('DB_USERNAME'),
|
||||
password: env.get('DB_PASSWORD'),
|
||||
database: env.get('DB_DATABASE'),
|
||||
},
|
||||
slaves: [
|
||||
{
|
||||
host: env.get('DB_REPLICA_HOST', true),
|
||||
port: parseInt(env.get('DB_PORT')),
|
||||
username: env.get('DB_USERNAME'),
|
||||
password: env.get('DB_PASSWORD'),
|
||||
database: env.get('DB_DATABASE'),
|
||||
},
|
||||
],
|
||||
removeNodeErrorCount: 10,
|
||||
restoreNodeTimeout: 5,
|
||||
}
|
||||
|
||||
const mySQLDataSourceOptions: MysqlConnectionOptions = {
|
||||
...commonDataSourceOptions,
|
||||
type: 'mysql',
|
||||
charset: 'utf8mb4',
|
||||
supportBigNumbers: true,
|
||||
bigNumberStrings: false,
|
||||
replication: inReplicaMode ? replicationConfig : undefined,
|
||||
host: inReplicaMode ? undefined : env.get('DB_HOST'),
|
||||
port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')),
|
||||
username: inReplicaMode ? undefined : env.get('DB_USERNAME'),
|
||||
password: inReplicaMode ? undefined : env.get('DB_PASSWORD'),
|
||||
database: inReplicaMode ? undefined : env.get('DB_DATABASE'),
|
||||
}
|
||||
|
||||
dataSource = new DataSource(mySQLDataSourceOptions)
|
||||
} else {
|
||||
const sqliteDataSourceOptions: SqliteConnectionOptions = {
|
||||
...commonDataSourceOptions,
|
||||
type: 'sqlite',
|
||||
database: `data/${env.get('DB_DATABASE')}.sqlite`,
|
||||
}
|
||||
|
||||
dataSource = new DataSource(sqliteDataSourceOptions)
|
||||
}
|
||||
|
||||
export const AppDataSource = dataSource
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
import { AbstractEnv } from '@standardnotes/domain-core'
|
||||
import { config, DotenvParseOutput } from 'dotenv'
|
||||
import { injectable } from 'inversify'
|
||||
|
||||
@injectable()
|
||||
export class Env {
|
||||
private env?: DotenvParseOutput
|
||||
|
||||
public load(): void {
|
||||
export class Env extends AbstractEnv {
|
||||
load(): void {
|
||||
const output = config()
|
||||
this.env = <DotenvParseOutput>output.parsed
|
||||
}
|
||||
|
||||
public get(key: string, optional = false): string {
|
||||
if (!this.env) {
|
||||
this.load()
|
||||
}
|
||||
|
||||
if (!process.env[key] && !optional) {
|
||||
throw new Error(`Environment variable ${key} not set`)
|
||||
}
|
||||
|
||||
return <string>process.env[key]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import {
|
||||
ControllerContainerInterface,
|
||||
Result,
|
||||
ServiceConfiguration,
|
||||
ServiceContainerInterface,
|
||||
ServiceIdentifier,
|
||||
ServiceInterface,
|
||||
} from '@standardnotes/domain-core'
|
||||
|
||||
import { ContainerConfigLoader } from './Container'
|
||||
import { DirectCallDomainEventPublisher } from '@standardnotes/domain-events-infra'
|
||||
import { Transform } from 'stream'
|
||||
import TYPES from './Types'
|
||||
import { Container } from 'inversify'
|
||||
import { ActivatePremiumFeatures } from '../Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures'
|
||||
import { AuthServiceInterface } from './AuthServiceInterface'
|
||||
|
||||
export class Service implements ServiceInterface {
|
||||
private logger: Transform | undefined
|
||||
export class Service implements AuthServiceInterface {
|
||||
private container: Container | undefined
|
||||
|
||||
constructor(
|
||||
private serviceContainer: ServiceContainerInterface,
|
||||
@@ -20,8 +24,14 @@ export class Service implements ServiceInterface {
|
||||
this.serviceContainer.register(this.getId(), this)
|
||||
}
|
||||
|
||||
setLogger(logger: Transform): void {
|
||||
this.logger = logger
|
||||
async activatePremiumFeatures(username: string): Promise<Result<string>> {
|
||||
if (!this.container) {
|
||||
return Result.fail('Container not initialized')
|
||||
}
|
||||
|
||||
const activatePremiumFeatures = this.container.get(TYPES.Auth_ActivatePremiumFeatures) as ActivatePremiumFeatures
|
||||
|
||||
return activatePremiumFeatures.execute({ username })
|
||||
}
|
||||
|
||||
async handleRequest(request: never, response: never, endpointOrMethodIdentifier: string): Promise<unknown> {
|
||||
@@ -34,14 +44,19 @@ export class Service implements ServiceInterface {
|
||||
return method(request, response)
|
||||
}
|
||||
|
||||
async getContainer(): Promise<unknown> {
|
||||
async getContainer(configuration?: ServiceConfiguration): Promise<unknown> {
|
||||
const config = new ContainerConfigLoader()
|
||||
|
||||
return config.load({
|
||||
const container = await config.load({
|
||||
controllerConatiner: this.controllerContainer,
|
||||
directCallDomainEventPublisher: this.directCallDomainEventPublisher,
|
||||
logger: this.logger,
|
||||
logger: configuration?.logger,
|
||||
environmentOverrides: configuration?.environmentOverrides,
|
||||
})
|
||||
|
||||
this.container = container
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
getId(): ServiceIdentifier {
|
||||
|
||||
@@ -149,6 +149,7 @@ const TYPES = {
|
||||
Auth_ListAuthenticators: Symbol.for('Auth_ListAuthenticators'),
|
||||
Auth_DeleteAuthenticator: Symbol.for('Auth_DeleteAuthenticator'),
|
||||
Auth_GenerateRecoveryCodes: Symbol.for('Auth_GenerateRecoveryCodes'),
|
||||
Auth_ActivatePremiumFeatures: Symbol.for('Auth_ActivatePremiumFeatures'),
|
||||
Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'),
|
||||
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
|
||||
// Handlers
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './AuthServiceInterface'
|
||||
export * from './Service'
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { SimpleUserProjection } from '../../Projection/SimpleUserProjection'
|
||||
|
||||
export interface AuthResponse {
|
||||
user: {
|
||||
uuid: string
|
||||
email: string
|
||||
protocolVersion: ProtocolVersion
|
||||
}
|
||||
user: SimpleUserProjection
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import {
|
||||
TokenEncoderInterface,
|
||||
} from '@standardnotes/security'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { SessionBody } from '@standardnotes/responses'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
|
||||
import { SimpleUserProjection } from '../../Projection/SimpleUserProjection'
|
||||
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
|
||||
import { KeyParamsFactoryInterface } from '../User/KeyParamsFactoryInterface'
|
||||
import { User } from '../User/User'
|
||||
@@ -54,11 +54,7 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
|
||||
return {
|
||||
session: sessionPayload,
|
||||
key_params: this.keyParamsFactory.create(dto.user, true),
|
||||
user: this.userProjector.projectSimple(dto.user) as {
|
||||
uuid: string
|
||||
email: string
|
||||
protocolVersion: ProtocolVersion
|
||||
},
|
||||
user: this.userProjector.projectSimple(dto.user) as SimpleUserProjection,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
|
||||
import { AuthenticationMethodResolver } from './AuthenticationMethodResolver'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
describe('AuthenticationMethodResolver', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
@@ -18,11 +19,15 @@ describe('AuthenticationMethodResolver', () => {
|
||||
let user: User
|
||||
let session: Session
|
||||
let revokedSession: RevokedSession
|
||||
let logger: Logger
|
||||
|
||||
const createResolver = () =>
|
||||
new AuthenticationMethodResolver(userRepository, sessionService, sessionTokenDecoder, fallbackTokenDecoder)
|
||||
new AuthenticationMethodResolver(userRepository, sessionService, sessionTokenDecoder, fallbackTokenDecoder, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
session = {} as jest.Mocked<Session>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SessionServiceInterface } from '../Session/SessionServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { AuthenticationMethod } from './AuthenticationMethod'
|
||||
import { AuthenticationMethodResolverInterface } from './AuthenticationMethodResolverInterface'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
@injectable()
|
||||
export class AuthenticationMethodResolver implements AuthenticationMethodResolverInterface {
|
||||
@@ -14,15 +15,20 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
|
||||
@inject(TYPES.Auth_SessionTokenDecoder) private sessionTokenDecoder: TokenDecoderInterface<SessionTokenData>,
|
||||
@inject(TYPES.Auth_FallbackSessionTokenDecoder)
|
||||
private fallbackSessionTokenDecoder: TokenDecoderInterface<SessionTokenData>,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async resolve(token: string): Promise<AuthenticationMethod | undefined> {
|
||||
let decodedToken: SessionTokenData | undefined = this.sessionTokenDecoder.decodeToken(token)
|
||||
if (decodedToken === undefined) {
|
||||
this.logger.debug('Could not decode token with primary decoder, trying fallback decoder.')
|
||||
|
||||
decodedToken = this.fallbackSessionTokenDecoder.decodeToken(token)
|
||||
}
|
||||
|
||||
if (decodedToken) {
|
||||
this.logger.debug('Token decoded successfully. User found.')
|
||||
|
||||
return {
|
||||
type: 'jwt',
|
||||
user: await this.userRepository.findOneByUuid(<string>decodedToken.user_uuid),
|
||||
@@ -32,6 +38,8 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
|
||||
|
||||
const session = await this.sessionService.getSessionFromToken(token)
|
||||
if (session) {
|
||||
this.logger.debug('Token decoded successfully. Session found.')
|
||||
|
||||
return {
|
||||
type: 'session_token',
|
||||
user: await this.userRepository.findOneByUuid(session.userUuid),
|
||||
@@ -41,6 +49,8 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
|
||||
|
||||
const revokedSession = await this.sessionService.getRevokedSessionFromToken(token)
|
||||
if (revokedSession) {
|
||||
this.logger.debug('Token decoded successfully. Revoked session found.')
|
||||
|
||||
return {
|
||||
type: 'revoked',
|
||||
revokedSession: await this.sessionService.markRevokedSessionAsReceived(revokedSession),
|
||||
@@ -48,6 +58,8 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug('Could not decode token.')
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('SettingService', () => {
|
||||
user = {
|
||||
uuid: '4-5-6',
|
||||
} as jest.Mocked<User>
|
||||
user.isPotentiallyAVaultAccount = jest.fn().mockReturnValue(false)
|
||||
user.isPotentiallyAPrivateUsernameAccount = jest.fn().mockReturnValue(false)
|
||||
|
||||
setting = {
|
||||
name: SettingName.NAMES.DropboxBackupToken,
|
||||
@@ -66,7 +66,7 @@ describe('SettingService', () => {
|
||||
]),
|
||||
)
|
||||
|
||||
settingsAssociationService.getDefaultSettingsAndValuesForNewVaultAccount = jest.fn().mockReturnValue(
|
||||
settingsAssociationService.getDefaultSettingsAndValuesForNewPrivateUsernameAccount = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
SettingName.NAMES.LogSessionUserAgent,
|
||||
@@ -98,7 +98,7 @@ describe('SettingService', () => {
|
||||
})
|
||||
|
||||
it('should create default settings for a newly registered vault account', async () => {
|
||||
user.isPotentiallyAVaultAccount = jest.fn().mockReturnValue(true)
|
||||
user.isPotentiallyAPrivateUsernameAccount = jest.fn().mockReturnValue(true)
|
||||
|
||||
await createService().applyDefaultSettingsUponRegistration(user)
|
||||
|
||||
|
||||
@@ -28,8 +28,9 @@ export class SettingService implements SettingServiceInterface {
|
||||
|
||||
async applyDefaultSettingsUponRegistration(user: User): Promise<void> {
|
||||
let defaultSettingsWithValues = this.settingsAssociationService.getDefaultSettingsAndValuesForNewUser()
|
||||
if (user.isPotentiallyAVaultAccount()) {
|
||||
defaultSettingsWithValues = this.settingsAssociationService.getDefaultSettingsAndValuesForNewVaultAccount()
|
||||
if (user.isPotentiallyAPrivateUsernameAccount()) {
|
||||
defaultSettingsWithValues =
|
||||
this.settingsAssociationService.getDefaultSettingsAndValuesForNewPrivateUsernameAccount()
|
||||
}
|
||||
|
||||
for (const settingName of defaultSettingsWithValues.keys()) {
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('SettingsAssociationService', () => {
|
||||
})
|
||||
|
||||
it('should return the default set of settings for a newly registered vault account', () => {
|
||||
const settings = createService().getDefaultSettingsAndValuesForNewVaultAccount()
|
||||
const settings = createService().getDefaultSettingsAndValuesForNewPrivateUsernameAccount()
|
||||
const flatSettings = [...(settings as Map<string, SettingDescription>).keys()]
|
||||
expect(flatSettings).toEqual(['MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
|
||||
],
|
||||
])
|
||||
|
||||
private readonly vaultAccountDefaultSettingsOverwrites = new Map<string, SettingDescription>([
|
||||
private readonly privateUsernameAccountDefaultSettingsOverwrites = new Map<string, SettingDescription>([
|
||||
[
|
||||
SettingName.NAMES.LogSessionUserAgent,
|
||||
{
|
||||
@@ -114,16 +114,18 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
|
||||
return this.defaultSettings
|
||||
}
|
||||
|
||||
getDefaultSettingsAndValuesForNewVaultAccount(): Map<string, SettingDescription> {
|
||||
const defaultVaultSettings = new Map(this.defaultSettings)
|
||||
getDefaultSettingsAndValuesForNewPrivateUsernameAccount(): Map<string, SettingDescription> {
|
||||
const defaultPrivateUsernameSettings = new Map(this.defaultSettings)
|
||||
|
||||
for (const vaultAccountDefaultSettingOverwriteKey of this.vaultAccountDefaultSettingsOverwrites.keys()) {
|
||||
defaultVaultSettings.set(
|
||||
vaultAccountDefaultSettingOverwriteKey,
|
||||
this.vaultAccountDefaultSettingsOverwrites.get(vaultAccountDefaultSettingOverwriteKey) as SettingDescription,
|
||||
for (const privateUsernameAccountDefaultSettingOverwriteKey of this.privateUsernameAccountDefaultSettingsOverwrites.keys()) {
|
||||
defaultPrivateUsernameSettings.set(
|
||||
privateUsernameAccountDefaultSettingOverwriteKey,
|
||||
this.privateUsernameAccountDefaultSettingsOverwrites.get(
|
||||
privateUsernameAccountDefaultSettingOverwriteKey,
|
||||
) as SettingDescription,
|
||||
)
|
||||
}
|
||||
|
||||
return defaultVaultSettings
|
||||
return defaultPrivateUsernameSettings
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SettingDescription } from './SettingDescription'
|
||||
|
||||
export interface SettingsAssociationServiceInterface {
|
||||
getDefaultSettingsAndValuesForNewUser(): Map<string, SettingDescription>
|
||||
getDefaultSettingsAndValuesForNewVaultAccount(): Map<string, SettingDescription>
|
||||
getDefaultSettingsAndValuesForNewPrivateUsernameAccount(): Map<string, SettingDescription>
|
||||
getPermissionAssociatedWithSetting(settingName: SettingName): PermissionName | undefined
|
||||
getEncryptionVersionForSetting(settingName: SettingName): EncryptionVersion
|
||||
getSensitivityForSetting(settingName: SettingName): boolean
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
|
||||
import { ActivatePremiumFeatures } from './ActivatePremiumFeatures'
|
||||
import { User } from '../../User/User'
|
||||
|
||||
describe('ActivatePremiumFeatures', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let roleService: RoleServiceInterface
|
||||
let timer: TimerInterface
|
||||
let user: User
|
||||
|
||||
const createUseCase = () =>
|
||||
new ActivatePremiumFeatures(userRepository, userSubscriptionRepository, roleService, timer)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockResolvedValue(user)
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.save = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.getUTCDateNDaysAhead = jest.fn().mockReturnValue(new Date('2024-01-01T00:00:00.000Z'))
|
||||
})
|
||||
|
||||
it('should return error when username is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({ username: '' })
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Username cannot be empty')
|
||||
})
|
||||
|
||||
it('should return error when user is not found', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockResolvedValue(null)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({ username: 'test@test.te' })
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('User not found with username: test@test.te')
|
||||
})
|
||||
|
||||
it('should save a subscription and add role to user', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({ username: 'test@test.te' })
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
|
||||
expect(userSubscriptionRepository.save).toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Result, SubscriptionPlanName, UseCaseInterface, Username } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
|
||||
import { ActivatePremiumFeaturesDTO } from './ActivatePremiumFeaturesDTO'
|
||||
|
||||
export class ActivatePremiumFeatures implements UseCaseInterface<string> {
|
||||
constructor(
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
private roleService: RoleServiceInterface,
|
||||
private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: ActivatePremiumFeaturesDTO): Promise<Result<string>> {
|
||||
const usernameOrError = Username.create(dto.username)
|
||||
if (usernameOrError.isFailed()) {
|
||||
return Result.fail(usernameOrError.getError())
|
||||
}
|
||||
const username = usernameOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUsernameOrEmail(username)
|
||||
if (user === null) {
|
||||
return Result.fail(`User not found with username: ${username.value}`)
|
||||
}
|
||||
|
||||
const timestamp = this.timer.getTimestampInMicroseconds()
|
||||
|
||||
const subscription = new UserSubscription()
|
||||
subscription.planName = SubscriptionPlanName.NAMES.ProPlan
|
||||
subscription.user = Promise.resolve(user)
|
||||
subscription.createdAt = timestamp
|
||||
subscription.updatedAt = timestamp
|
||||
subscription.endsAt = this.timer.convertDateToMicroseconds(this.timer.getUTCDateNDaysAhead(365))
|
||||
subscription.cancelled = false
|
||||
subscription.subscriptionId = 1
|
||||
subscription.subscriptionType = UserSubscriptionType.Regular
|
||||
|
||||
await this.userSubscriptionRepository.save(subscription)
|
||||
|
||||
await this.roleService.addUserRole(user, SubscriptionPlanName.NAMES.ProPlan)
|
||||
|
||||
return Result.ok('Premium features activated.')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface ActivatePremiumFeaturesDTO {
|
||||
username: string
|
||||
}
|
||||
@@ -16,6 +16,8 @@ export class AuthenticateRequest implements UseCaseInterface {
|
||||
|
||||
async execute(dto: AuthenticateRequestDTO): Promise<AuthenticateRequestResponse> {
|
||||
if (!dto.authorizationHeader) {
|
||||
this.logger.debug('Authorization header not provided.')
|
||||
|
||||
return {
|
||||
success: false,
|
||||
responseCode: 401,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AuthenticateUser } from './AuthenticateUser'
|
||||
import { RevokedSession } from '../Session/RevokedSession'
|
||||
import { AuthenticationMethodResolverInterface } from '../Auth/AuthenticationMethodResolverInterface'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
describe('AuthenticateUser', () => {
|
||||
let user: User
|
||||
@@ -14,11 +15,15 @@ describe('AuthenticateUser', () => {
|
||||
let revokedSession: RevokedSession
|
||||
let authenticationMethodResolver: AuthenticationMethodResolverInterface
|
||||
let timer: TimerInterface
|
||||
let logger: Logger
|
||||
const accessTokenAge = 3600
|
||||
|
||||
const createUseCase = () => new AuthenticateUser(authenticationMethodResolver, timer, accessTokenAge)
|
||||
const createUseCase = () => new AuthenticateUser(authenticationMethodResolver, timer, accessTokenAge, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
|
||||
user = {} as jest.Mocked<User>
|
||||
user.supportsSessions = jest.fn().mockReturnValue(false)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Session } from '../Session/Session'
|
||||
import { AuthenticateUserDTO } from './AuthenticateUserDTO'
|
||||
import { AuthenticateUserResponse } from './AuthenticateUserResponse'
|
||||
import { UseCaseInterface } from './UseCaseInterface'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
@injectable()
|
||||
export class AuthenticateUser implements UseCaseInterface {
|
||||
@@ -17,11 +18,14 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
private authenticationMethodResolver: AuthenticationMethodResolverInterface,
|
||||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Auth_ACCESS_TOKEN_AGE) private accessTokenAge: number,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: AuthenticateUserDTO): Promise<AuthenticateUserResponse> {
|
||||
const authenticationMethod = await this.authenticationMethodResolver.resolve(dto.token)
|
||||
if (!authenticationMethod) {
|
||||
this.logger.debug('No authentication method found for token.')
|
||||
|
||||
return {
|
||||
success: false,
|
||||
failureType: 'INVALID_AUTH',
|
||||
@@ -37,6 +41,8 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
|
||||
const user = authenticationMethod.user
|
||||
if (!user) {
|
||||
this.logger.debug('No user found for authentication method.')
|
||||
|
||||
return {
|
||||
success: false,
|
||||
failureType: 'INVALID_AUTH',
|
||||
@@ -44,6 +50,8 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
}
|
||||
|
||||
if (authenticationMethod.type == 'jwt' && user.supportsSessions()) {
|
||||
this.logger.debug('User supports sessions but is trying to authenticate with a JWT.')
|
||||
|
||||
return {
|
||||
success: false,
|
||||
failureType: 'INVALID_AUTH',
|
||||
@@ -56,6 +64,8 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
const encryptedPasswordDigest = crypto.createHash('sha256').update(user.encryptedPassword).digest('hex')
|
||||
|
||||
if (!pwHash || !crypto.timingSafeEqual(Buffer.from(pwHash), Buffer.from(encryptedPasswordDigest))) {
|
||||
this.logger.debug('Password hash does not match.')
|
||||
|
||||
return {
|
||||
success: false,
|
||||
failureType: 'INVALID_AUTH',
|
||||
@@ -66,6 +76,8 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
case 'session_token': {
|
||||
const session = authenticationMethod.session
|
||||
if (!session) {
|
||||
this.logger.debug('No session found for authentication method.')
|
||||
|
||||
return {
|
||||
success: false,
|
||||
failureType: 'INVALID_AUTH',
|
||||
@@ -73,6 +85,8 @@ export class AuthenticateUser implements UseCaseInterface {
|
||||
}
|
||||
|
||||
if (session.refreshExpiration < this.timer.getUTCDate()) {
|
||||
this.logger.debug('Session refresh token has expired.')
|
||||
|
||||
return {
|
||||
success: false,
|
||||
failureType: 'INVALID_AUTH',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { inject, injectable } from 'inversify'
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security'
|
||||
import { CreateValetTokenPayload, CreateValetTokenResponseData } from '@standardnotes/responses'
|
||||
import { CreateValetTokenResponseData } from '@standardnotes/responses'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
@@ -12,6 +12,7 @@ import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionS
|
||||
import { CreateValetTokenDTO } from './CreateValetTokenDTO'
|
||||
import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface'
|
||||
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
|
||||
import { CreateValetTokenPayload } from '../../ValetToken/CreateValetTokenPayload'
|
||||
|
||||
@injectable()
|
||||
export class CreateValetToken implements UseCaseInterface {
|
||||
|
||||
@@ -69,7 +69,7 @@ export class InviteToSharedSubscription implements UseCaseInterface {
|
||||
sharedSubscriptionInvition.inviterIdentifier = dto.inviterEmail
|
||||
sharedSubscriptionInvition.inviterIdentifierType = InviterIdentifierType.Email
|
||||
sharedSubscriptionInvition.inviteeIdentifier = dto.inviteeIdentifier
|
||||
sharedSubscriptionInvition.inviteeIdentifierType = this.isInviteeIdentifierPotentiallyAVaultAccount(
|
||||
sharedSubscriptionInvition.inviteeIdentifierType = this.isInviteeIdentifierPotentiallyAPrivateUsernameAccount(
|
||||
dto.inviteeIdentifier,
|
||||
)
|
||||
? InviteeIdentifierType.Hash
|
||||
@@ -107,7 +107,7 @@ export class InviteToSharedSubscription implements UseCaseInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private isInviteeIdentifierPotentiallyAVaultAccount(identifier: string): boolean {
|
||||
private isInviteeIdentifierPotentiallyAPrivateUsernameAccount(identifier: string): boolean {
|
||||
return identifier.length === 64 && !identifier.includes('@')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,21 +44,13 @@ describe('UpdateUser', () => {
|
||||
user,
|
||||
updatedWithUserAgent: 'Mozilla',
|
||||
apiVersion: '20190520',
|
||||
version: '004',
|
||||
pwCost: 11,
|
||||
pwSalt: 'qweqwe',
|
||||
pwNonce: undefined,
|
||||
}),
|
||||
).toEqual({ success: true, authResponse: { foo: 'bar' } })
|
||||
|
||||
expect(userRepository.save).toHaveBeenCalledWith({
|
||||
createdAt: new Date(1),
|
||||
pwCost: 11,
|
||||
email: 'test@test.te',
|
||||
pwSalt: 'qweqwe',
|
||||
updatedWithUserAgent: 'Mozilla',
|
||||
uuid: '123',
|
||||
version: '004',
|
||||
updatedAt: new Date(1),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,25 +17,17 @@ export class UpdateUser implements UseCaseInterface {
|
||||
) {}
|
||||
|
||||
async execute(dto: UpdateUserDTO): Promise<UpdateUserResponse> {
|
||||
const { user, apiVersion, ...updateFields } = dto
|
||||
dto.user.updatedAt = this.timer.getUTCDate()
|
||||
|
||||
Object.keys(updateFields).forEach(
|
||||
(key) => (updateFields[key] === undefined || updateFields[key] === null) && delete updateFields[key],
|
||||
)
|
||||
const updatedUser = await this.userRepository.save(dto.user)
|
||||
|
||||
Object.assign(user, updateFields)
|
||||
|
||||
user.updatedAt = this.timer.getUTCDate()
|
||||
|
||||
await this.userRepository.save(user)
|
||||
|
||||
const authResponseFactory = this.authResponseFactoryResolver.resolveAuthResponseFactoryVersion(apiVersion)
|
||||
const authResponseFactory = this.authResponseFactoryResolver.resolveAuthResponseFactoryVersion(dto.apiVersion)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
authResponse: await authResponseFactory.createResponse({
|
||||
user,
|
||||
apiVersion,
|
||||
user: updatedUser,
|
||||
apiVersion: dto.apiVersion,
|
||||
userAgent: dto.updatedWithUserAgent,
|
||||
ephemeralSession: false,
|
||||
readonlyAccess: false,
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
import { User } from '../User/User'
|
||||
|
||||
export type UpdateUserDTO = {
|
||||
[key: string]: string | User | Date | undefined | number
|
||||
user: User
|
||||
updatedWithUserAgent: string
|
||||
apiVersion: string
|
||||
email?: string
|
||||
pwFunc?: string
|
||||
pwAlg?: string
|
||||
pwCost?: number
|
||||
pwKeySize?: number
|
||||
pwNonce?: string
|
||||
pwSalt?: string
|
||||
kpOrigination?: string
|
||||
kpCreated?: Date
|
||||
version?: string
|
||||
updatedWithUserAgent: string
|
||||
}
|
||||
|
||||
@@ -21,13 +21,13 @@ describe('User', () => {
|
||||
const user = createUser()
|
||||
user.email = 'a75a31ce95365904ef0e0a8e6cefc1f5e99adfef81bbdb6d4499eeb10ae0ff67'
|
||||
|
||||
expect(user.isPotentiallyAVaultAccount()).toBeTruthy()
|
||||
expect(user.isPotentiallyAPrivateUsernameAccount()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should indicate if the user is not a vault account', () => {
|
||||
const user = createUser()
|
||||
user.email = 'test@test.te'
|
||||
|
||||
expect(user.isPotentiallyAVaultAccount()).toBeFalsy()
|
||||
expect(user.isPotentiallyAPrivateUsernameAccount()).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -202,7 +202,7 @@ export class User {
|
||||
return parseInt(this.version) >= parseInt(ProtocolVersion.V004)
|
||||
}
|
||||
|
||||
isPotentiallyAVaultAccount(): boolean {
|
||||
isPotentiallyAPrivateUsernameAccount(): boolean {
|
||||
return this.email.length === 64 && !this.email.includes('@')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export type CreateValetTokenPayload = {
|
||||
operation: 'read' | 'write' | 'delete' | 'move'
|
||||
resources: Array<{
|
||||
remoteIdentifier: string
|
||||
unencryptedFileSize?: number
|
||||
}>
|
||||
}
|
||||
@@ -60,15 +60,6 @@ export class HomeServerUsersController extends BaseHttpController {
|
||||
user: response.locals.user,
|
||||
updatedWithUserAgent: <string>request.headers['user-agent'],
|
||||
apiVersion: request.body.api,
|
||||
pwFunc: request.body.pw_func,
|
||||
pwAlg: request.body.pw_alg,
|
||||
pwCost: request.body.pw_cost,
|
||||
pwKeySize: request.body.pw_key_size,
|
||||
pwNonce: request.body.pw_nonce,
|
||||
pwSalt: request.body.pw_salt,
|
||||
kpOrigination: request.body.origination,
|
||||
kpCreated: request.body.created,
|
||||
version: request.body.version,
|
||||
})
|
||||
|
||||
if (updateResult.success) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ControllerContainerInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { Request, Response } from 'express'
|
||||
import { BaseHttpController, results } from 'inversify-express-utils'
|
||||
import { ErrorTag } from '@standardnotes/responses'
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
|
||||
import { CreateValetToken } from '../../../Domain/UseCase/CreateValetToken/CreateValetToken'
|
||||
import { CreateValetTokenPayload, ErrorTag } from '@standardnotes/responses'
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
import { CreateValetTokenPayload } from '../../../Domain/ValetToken/CreateValetTokenPayload'
|
||||
|
||||
export class HomeServerValetTokenController extends BaseHttpController {
|
||||
constructor(protected createValetKey: CreateValetToken, private controllerContainer?: ControllerContainerInterface) {
|
||||
|
||||
@@ -99,9 +99,7 @@ describe('InversifyExpressUsersController', () => {
|
||||
|
||||
expect(updateUser.execute).toHaveBeenCalledWith({
|
||||
apiVersion: '20190520',
|
||||
kpOrigination: 'test',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
version: '002',
|
||||
user: {
|
||||
uuid: '123',
|
||||
email: 'test@test.te',
|
||||
@@ -143,9 +141,7 @@ describe('InversifyExpressUsersController', () => {
|
||||
|
||||
expect(updateUser.execute).toHaveBeenCalledWith({
|
||||
apiVersion: '20190520',
|
||||
kpOrigination: 'test',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
version: '002',
|
||||
user: {
|
||||
uuid: '123',
|
||||
email: 'test@test.te',
|
||||
|
||||
5
packages/auth/src/Projection/SimpleUserProjection.ts
Normal file
5
packages/auth/src/Projection/SimpleUserProjection.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type SimpleUserProjection = {
|
||||
uuid: string
|
||||
email: string
|
||||
protocolVersion: string
|
||||
}
|
||||
@@ -2,10 +2,11 @@ import { injectable } from 'inversify'
|
||||
|
||||
import { User } from '../Domain/User/User'
|
||||
import { ProjectorInterface } from './ProjectorInterface'
|
||||
import { SimpleUserProjection } from './SimpleUserProjection'
|
||||
|
||||
@injectable()
|
||||
export class UserProjector implements ProjectorInterface<User> {
|
||||
projectSimple(user: User): Record<string, unknown> {
|
||||
projectSimple(user: User): SimpleUserProjection {
|
||||
return {
|
||||
uuid: user.uuid,
|
||||
email: user.email,
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.49.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.48.3...@standardnotes/common@1.49.0) (2023-06-30)
|
||||
|
||||
### Features
|
||||
|
||||
* shared vaults functionality in api-gateway,auth,files,common,security,domain-events. ([#629](https://github.com/standardnotes/server/issues/629)) ([fa7fbe2](https://github.com/standardnotes/server/commit/fa7fbe26e7b0707fc21d71e04af76870f5248baf))
|
||||
|
||||
## [1.48.3](https://github.com/standardnotes/server/compare/@standardnotes/common@1.48.2...@standardnotes/common@1.48.3) (2023-06-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/common
|
||||
|
||||
## [1.48.2](https://github.com/standardnotes/server/compare/@standardnotes/common@1.48.0...@standardnotes/common@1.48.2) (2023-05-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/common",
|
||||
"version": "1.48.2",
|
||||
"version": "1.49.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
export enum ContentType {
|
||||
Any = '*',
|
||||
Item = 'SF|Item',
|
||||
KeySystemItemsKey = 'SN|KeySystemItemsKey',
|
||||
KeySystemRootKey = 'SN|KeySystemRootKey',
|
||||
TrustedContact = 'SN|TrustedContact',
|
||||
VaultListing = 'SN|VaultListing',
|
||||
RootKey = 'SN|RootKey|NoSync',
|
||||
ItemsKey = 'SN|ItemsKey',
|
||||
EncryptedStorage = 'SN|EncryptedStorage',
|
||||
|
||||
@@ -3,7 +3,6 @@ export enum ProtocolVersion {
|
||||
V002 = '002',
|
||||
V003 = '003',
|
||||
V004 = '004',
|
||||
V005 = '005',
|
||||
}
|
||||
|
||||
export const ProtocolVersionLatest = ProtocolVersion.V004
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.18.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.17.0...@standardnotes/domain-core@1.18.0) (2023-06-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **home-server:** add overriding environment variables in underlying services ([#621](https://github.com/standardnotes/server/issues/621)) ([f0cbec0](https://github.com/standardnotes/server/commit/f0cbec07b87d60dfad92072944553f76e0bea164))
|
||||
|
||||
# [1.17.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.16.2...@standardnotes/domain-core@1.17.0) (2023-05-31)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-core",
|
||||
"version": "1.17.0",
|
||||
"version": "1.18.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
26
packages/domain-core/src/Domain/Env/AbstractEnv.ts
Normal file
26
packages/domain-core/src/Domain/Env/AbstractEnv.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export abstract class AbstractEnv {
|
||||
protected env?: { [key: string]: string } = {}
|
||||
protected overrides: { [key: string]: string }
|
||||
|
||||
constructor(overrides: { [key: string]: string } = {}) {
|
||||
this.overrides = overrides
|
||||
}
|
||||
|
||||
abstract load(): void
|
||||
|
||||
get(key: string, optional = false): string {
|
||||
if (!this.env) {
|
||||
this.load()
|
||||
}
|
||||
|
||||
if (this.overrides[key]) {
|
||||
return this.overrides[key]
|
||||
}
|
||||
|
||||
if (!process.env[key] && !optional) {
|
||||
throw new Error(`Environment variable ${key} not set`)
|
||||
}
|
||||
|
||||
return <string>process.env[key]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Transform } from 'stream'
|
||||
|
||||
export interface ServiceConfiguration {
|
||||
logger?: Transform
|
||||
environmentOverrides?: { [name: string]: string }
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ServiceConfiguration } from './ServiceConfiguration'
|
||||
import { ServiceIdentifier } from './ServiceIdentifier'
|
||||
import { Transform } from 'stream'
|
||||
|
||||
export interface ServiceInterface {
|
||||
getContainer(): Promise<unknown>
|
||||
setLogger(logger: Transform): void
|
||||
getContainer(configuration?: ServiceConfiguration): Promise<unknown>
|
||||
getId(): ServiceIdentifier
|
||||
handleRequest(request: never, response: never, endpointOrMethodIdentifier: string): Promise<unknown>
|
||||
}
|
||||
|
||||
@@ -37,8 +37,11 @@ export * from './DI/ControllerContainerInterface'
|
||||
export * from './Email/EmailLevel'
|
||||
export * from './Email/EmailLevelProps'
|
||||
|
||||
export * from './Env/AbstractEnv'
|
||||
|
||||
export * from './Mapping/MapperInterface'
|
||||
|
||||
export * from './Service/ServiceConfiguration'
|
||||
export * from './Service/ServiceContainer'
|
||||
export * from './Service/ServiceContainerInterface'
|
||||
export * from './Service/ServiceIdentifier'
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.12.7](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.6...@standardnotes/domain-events-infra@1.12.7) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.12.6](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.5...@standardnotes/domain-events-infra@1.12.6) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.12.5](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.4...@standardnotes/domain-events-infra@1.12.5) (2023-06-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.12.5",
|
||||
"version": "1.12.7",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.112.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.112.0...@standardnotes/domain-events@2.112.1) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
# [2.112.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.111.4...@standardnotes/domain-events@2.112.0) (2023-06-30)
|
||||
|
||||
### Features
|
||||
|
||||
* shared vaults functionality in api-gateway,auth,files,common,security,domain-events. ([#629](https://github.com/standardnotes/server/issues/629)) ([fa7fbe2](https://github.com/standardnotes/server/commit/fa7fbe26e7b0707fc21d71e04af76870f5248baf))
|
||||
|
||||
## [2.111.4](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.111.3...@standardnotes/domain-events@2.111.4) (2023-06-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.111.4",
|
||||
"version": "2.112.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface DomainEventInterface {
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: string
|
||||
userIdentifierType: 'uuid' | 'email'
|
||||
userIdentifierType: 'uuid' | 'email' | 'shared-vault-uuid'
|
||||
}
|
||||
origin: DomainEventService
|
||||
target?: DomainEventService
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { SharedVaultFileRemovedEventPayload } from './SharedVaultFileRemovedEventPayload'
|
||||
|
||||
export interface SharedVaultFileRemovedEvent extends DomainEventInterface {
|
||||
type: 'SHARED_VAULT_FILE_REMOVED'
|
||||
payload: SharedVaultFileRemovedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface SharedVaultFileRemovedEventPayload {
|
||||
sharedVaultUuid: string
|
||||
fileByteSize: number
|
||||
filePath: string
|
||||
fileName: string
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { SharedVaultFileUploadedEventPayload } from './SharedVaultFileUploadedEventPayload'
|
||||
|
||||
export interface SharedVaultFileUploadedEvent extends DomainEventInterface {
|
||||
type: 'SHARED_VAULT_FILE_UPLOADED'
|
||||
payload: SharedVaultFileUploadedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface SharedVaultFileUploadedEventPayload {
|
||||
sharedVaultUuid: string
|
||||
fileByteSize: number
|
||||
filePath: string
|
||||
fileName: string
|
||||
}
|
||||
@@ -62,6 +62,10 @@ export * from './Event/SharedSubscriptionInvitationCanceledEvent'
|
||||
export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
|
||||
export * from './Event/SharedSubscriptionInvitationCreatedEvent'
|
||||
export * from './Event/SharedSubscriptionInvitationCreatedEventPayload'
|
||||
export * from './Event/SharedVaultFileRemovedEvent'
|
||||
export * from './Event/SharedVaultFileRemovedEventPayload'
|
||||
export * from './Event/SharedVaultFileUploadedEvent'
|
||||
export * from './Event/SharedVaultFileUploadedEventPayload'
|
||||
export * from './Event/StatisticPersistenceRequestedEvent'
|
||||
export * from './Event/StatisticPersistenceRequestedEventPayload'
|
||||
export * from './Event/SubscriptionCancelledEvent'
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.11.1](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.0...@standardnotes/event-store@1.11.1) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
# [1.11.0](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.10.1...@standardnotes/event-store@1.11.0) (2023-06-30)
|
||||
|
||||
### Features
|
||||
|
||||
* shared vaults functionality in api-gateway,auth,files,common,security,domain-events. ([#629](https://github.com/standardnotes/server/issues/629)) ([fa7fbe2](https://github.com/standardnotes/server/commit/fa7fbe26e7b0707fc21d71e04af76870f5248baf))
|
||||
|
||||
## [1.10.1](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.10.0...@standardnotes/event-store@1.10.1) (2023-06-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** streaming logs ([a8b806a](https://github.com/standardnotes/server/commit/a8b806af084b3e3fe8707ff0cb041a74042ee049))
|
||||
|
||||
# [1.10.0](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.9.6...@standardnotes/event-store@1.10.0) (2023-06-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **home-server:** add overriding environment variables in underlying services ([#621](https://github.com/standardnotes/server/issues/621)) ([f0cbec0](https://github.com/standardnotes/server/commit/f0cbec07b87d60dfad92072944553f76e0bea164))
|
||||
|
||||
## [1.9.6](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.9.5...@standardnotes/event-store@1.9.6) (2023-06-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.9.6",
|
||||
"version": "1.11.1",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
@@ -33,6 +33,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sqs": "^3.332.0",
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
"@standardnotes/time": "workspace:*",
|
||||
|
||||
@@ -49,9 +49,9 @@ export class ContainerConfigLoader {
|
||||
}
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: env.get('LOG_LEVEL') || 'info',
|
||||
level: env.get('LOG_LEVEL', true) || 'info',
|
||||
format: winston.format.combine(winston.format.splat(), winston.format.json()),
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })],
|
||||
})
|
||||
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
|
||||
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
import { AbstractEnv } from '@standardnotes/domain-core'
|
||||
import { config, DotenvParseOutput } from 'dotenv'
|
||||
import { injectable } from 'inversify'
|
||||
|
||||
@injectable()
|
||||
export class Env {
|
||||
private env?: DotenvParseOutput
|
||||
|
||||
public load(): void {
|
||||
export class Env extends AbstractEnv {
|
||||
load(): void {
|
||||
const output = config()
|
||||
this.env = <DotenvParseOutput>output.parsed
|
||||
}
|
||||
|
||||
public get(key: string, optional = false): string {
|
||||
if (!this.env) {
|
||||
this.load()
|
||||
}
|
||||
|
||||
if (!process.env[key] && !optional) {
|
||||
throw new Error(`Environment variable ${key} not set`)
|
||||
}
|
||||
|
||||
return <string>process.env[key]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
MODE=microservice # microservice | home-server
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
VERSION=development
|
||||
|
||||
@@ -3,6 +3,52 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.19.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.0...@standardnotes/files-server@1.19.1) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
# [1.19.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.18.3...@standardnotes/files-server@1.19.0) (2023-06-30)
|
||||
|
||||
### Features
|
||||
|
||||
* shared vaults functionality in api-gateway,auth,files,common,security,domain-events. ([#629](https://github.com/standardnotes/files/issues/629)) ([fa7fbe2](https://github.com/standardnotes/files/commit/fa7fbe26e7b0707fc21d71e04af76870f5248baf))
|
||||
|
||||
## [1.18.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.18.2...@standardnotes/files-server@1.18.3) (2023-06-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** add debug logs about container initalizations ([0df4715](https://github.com/standardnotes/files/commit/0df471585fd5b4626ec2972f3b9a3e33b2830e65))
|
||||
|
||||
## [1.18.2](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.18.1...@standardnotes/files-server@1.18.2) (2023-06-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** accept application/octet-stream requests for files ([#625](https://github.com/standardnotes/files/issues/625)) ([c8974b7](https://github.com/standardnotes/files/commit/c8974b7fa229ff4f1e026e1ebff50d1081cc5f8b))
|
||||
|
||||
## [1.18.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.18.0...@standardnotes/files-server@1.18.1) (2023-06-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **files:** add debug logs for checking chunks upon finishing upload session ([5f0929c](https://github.com/standardnotes/files/commit/5f0929c1aa7b83661fb102bad34644db69ddf7eb))
|
||||
|
||||
# [1.18.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.17.1...@standardnotes/files-server@1.18.0) (2023-06-05)
|
||||
|
||||
### Features
|
||||
|
||||
* **home-server:** allow running the home server with a mysql and redis configuration ([#622](https://github.com/standardnotes/files/issues/622)) ([d6e531d](https://github.com/standardnotes/files/commit/d6e531d4b6c1c80a894f6d7ec93632595268dd64))
|
||||
|
||||
## [1.17.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.17.0...@standardnotes/files-server@1.17.1) (2023-06-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** streaming logs ([a8b806a](https://github.com/standardnotes/files/commit/a8b806af084b3e3fe8707ff0cb041a74042ee049))
|
||||
|
||||
# [1.17.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.16.5...@standardnotes/files-server@1.17.0) (2023-06-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **home-server:** add overriding environment variables in underlying services ([#621](https://github.com/standardnotes/files/issues/621)) ([f0cbec0](https://github.com/standardnotes/files/commit/f0cbec07b87d60dfad92072944553f76e0bea164))
|
||||
|
||||
## [1.16.5](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.16.4...@standardnotes/files-server@1.16.5) (2023-06-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as busboy from 'connect-busboy'
|
||||
|
||||
import '../src/Infra/InversifyExpress/InversifyExpressHealthCheckController'
|
||||
import '../src/Infra/InversifyExpress/InversifyExpressFilesController'
|
||||
import '../src/Infra/InversifyExpress/InversifyExpressSharedVaultFilesController'
|
||||
|
||||
import helmet from 'helmet'
|
||||
import * as cors from 'cors'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.16.5",
|
||||
"version": "1.19.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -48,16 +48,22 @@ import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountD
|
||||
import { SharedSubscriptionInvitationCanceledEventHandler } from '../Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler'
|
||||
import { InMemoryUploadRepository } from '../Infra/InMemory/InMemoryUploadRepository'
|
||||
import { Transform } from 'stream'
|
||||
import { FileMoverInterface } from '../Domain/Services/FileMoverInterface'
|
||||
import { S3FileMover } from '../Infra/S3/S3FileMover'
|
||||
import { FSFileMover } from '../Infra/FS/FSFileMover'
|
||||
import { MoveFile } from '../Domain/UseCase/MoveFile/MoveFile'
|
||||
import { SharedVaultValetTokenAuthMiddleware } from '../Infra/InversifyExpress/Middleware/SharedVaultValetTokenAuthMiddleware'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(configuration?: {
|
||||
directCallDomainEventPublisher?: DirectCallDomainEventPublisher
|
||||
logger?: Transform
|
||||
environmentOverrides?: { [name: string]: string }
|
||||
}): Promise<Container> {
|
||||
const directCallDomainEventPublisher =
|
||||
configuration?.directCallDomainEventPublisher ?? new DirectCallDomainEventPublisher()
|
||||
|
||||
const env: Env = new Env()
|
||||
const env: Env = new Env(configuration?.environmentOverrides)
|
||||
env.load()
|
||||
|
||||
const container = new Container()
|
||||
@@ -66,30 +72,24 @@ export class ContainerConfigLoader {
|
||||
await import('newrelic')
|
||||
}
|
||||
|
||||
const isConfiguredForHomeServer = env.get('CACHE_TYPE') === 'memory'
|
||||
const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server'
|
||||
const isConfiguredForInMemoryCache = env.get('CACHE_TYPE', true) === 'memory'
|
||||
|
||||
let logger: winston.Logger
|
||||
if (configuration?.logger) {
|
||||
container.bind<winston.Logger>(TYPES.Files_Logger).toConstantValue(configuration.logger as winston.Logger)
|
||||
logger = configuration.logger as winston.Logger
|
||||
} else {
|
||||
container.bind<winston.Logger>(TYPES.Files_Logger).toConstantValue(this.createLogger({ env }))
|
||||
logger = this.createLogger({ env })
|
||||
}
|
||||
container.bind<winston.Logger>(TYPES.Files_Logger).toConstantValue(logger)
|
||||
|
||||
container.bind<TimerInterface>(TYPES.Files_Timer).toConstantValue(new Timer())
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
if (isConfiguredForInMemoryCache) {
|
||||
container
|
||||
.bind<UploadRepositoryInterface>(TYPES.Files_UploadRepository)
|
||||
.toConstantValue(new InMemoryUploadRepository(container.get(TYPES.Files_Timer)))
|
||||
|
||||
container
|
||||
.bind<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher)
|
||||
.toConstantValue(directCallDomainEventPublisher)
|
||||
} else {
|
||||
container.bind(TYPES.Files_S3_BUCKET_NAME).toConstantValue(env.get('S3_BUCKET_NAME', true))
|
||||
container.bind(TYPES.Files_S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true))
|
||||
container.bind(TYPES.Files_SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN'))
|
||||
container.bind(TYPES.Files_SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
|
||||
container.bind(TYPES.Files_SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL'))
|
||||
container.bind(TYPES.Files_REDIS_URL).toConstantValue(env.get('REDIS_URL'))
|
||||
|
||||
const redisUrl = container.get(TYPES.Files_REDIS_URL) as string
|
||||
@@ -103,6 +103,20 @@ export class ContainerConfigLoader {
|
||||
|
||||
container.bind(TYPES.Files_Redis).toConstantValue(redis)
|
||||
|
||||
container.bind<UploadRepositoryInterface>(TYPES.Files_UploadRepository).to(RedisUploadRepository)
|
||||
}
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
container
|
||||
.bind<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher)
|
||||
.toConstantValue(directCallDomainEventPublisher)
|
||||
} else {
|
||||
container.bind(TYPES.Files_S3_BUCKET_NAME).toConstantValue(env.get('S3_BUCKET_NAME', true))
|
||||
container.bind(TYPES.Files_S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true))
|
||||
container.bind(TYPES.Files_SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN'))
|
||||
container.bind(TYPES.Files_SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
|
||||
container.bind(TYPES.Files_SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL'))
|
||||
|
||||
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||
const snsConfig: SNSClientConfig = {
|
||||
apiVersion: 'latest',
|
||||
@@ -136,8 +150,6 @@ export class ContainerConfigLoader {
|
||||
container.bind<SQSClient>(TYPES.Files_SQS).toConstantValue(new SQSClient(sqsConfig))
|
||||
}
|
||||
|
||||
container.bind<UploadRepositoryInterface>(TYPES.Files_UploadRepository).to(RedisUploadRepository)
|
||||
|
||||
container
|
||||
.bind<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher)
|
||||
.toConstantValue(
|
||||
@@ -155,7 +167,7 @@ export class ContainerConfigLoader {
|
||||
.bind(TYPES.Files_FILE_UPLOAD_PATH)
|
||||
.toConstantValue(env.get('FILE_UPLOAD_PATH', true) ?? `${__dirname}/../../uploads`)
|
||||
|
||||
if (env.get('S3_AWS_REGION', true) || env.get('S3_ENDPOINT', true)) {
|
||||
if (!isConfiguredForHomeServer && (env.get('S3_AWS_REGION', true) || env.get('S3_ENDPOINT', true))) {
|
||||
const s3Opts: S3ClientConfig = {
|
||||
apiVersion: 'latest',
|
||||
}
|
||||
@@ -170,6 +182,7 @@ export class ContainerConfigLoader {
|
||||
container.bind<FileDownloaderInterface>(TYPES.Files_FileDownloader).to(S3FileDownloader)
|
||||
container.bind<FileUploaderInterface>(TYPES.Files_FileUploader).to(S3FileUploader)
|
||||
container.bind<FileRemoverInterface>(TYPES.Files_FileRemover).to(S3FileRemover)
|
||||
container.bind<FileMoverInterface>(TYPES.Files_FileMover).to(S3FileMover)
|
||||
} else {
|
||||
container.bind<FileDownloaderInterface>(TYPES.Files_FileDownloader).to(FSFileDownloader)
|
||||
container
|
||||
@@ -178,6 +191,7 @@ export class ContainerConfigLoader {
|
||||
new FSFileUploader(container.get(TYPES.Files_FILE_UPLOAD_PATH), container.get(TYPES.Files_Logger)),
|
||||
)
|
||||
container.bind<FileRemoverInterface>(TYPES.Files_FileRemover).to(FSFileRemover)
|
||||
container.bind<FileMoverInterface>(TYPES.Files_FileMover).to(FSFileMover)
|
||||
}
|
||||
|
||||
// use cases
|
||||
@@ -187,10 +201,14 @@ export class ContainerConfigLoader {
|
||||
container.bind<FinishUploadSession>(TYPES.Files_FinishUploadSession).to(FinishUploadSession)
|
||||
container.bind<GetFileMetadata>(TYPES.Files_GetFileMetadata).to(GetFileMetadata)
|
||||
container.bind<RemoveFile>(TYPES.Files_RemoveFile).to(RemoveFile)
|
||||
container.bind<MoveFile>(TYPES.Files_MoveFile).to(MoveFile)
|
||||
container.bind<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
|
||||
|
||||
// middleware
|
||||
container.bind<ValetTokenAuthMiddleware>(TYPES.Files_ValetTokenAuthMiddleware).to(ValetTokenAuthMiddleware)
|
||||
container
|
||||
.bind<SharedVaultValetTokenAuthMiddleware>(TYPES.Files_SharedVaultValetTokenAuthMiddleware)
|
||||
.to(SharedVaultValetTokenAuthMiddleware)
|
||||
|
||||
// services
|
||||
container
|
||||
@@ -244,14 +262,16 @@ export class ContainerConfigLoader {
|
||||
)
|
||||
}
|
||||
|
||||
logger.debug('Configuration complete')
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
createLogger({ env }: { env: Env }): winston.Logger {
|
||||
return winston.createLogger({
|
||||
level: env.get('LOG_LEVEL') || 'info',
|
||||
level: env.get('LOG_LEVEL', true) || 'info',
|
||||
format: winston.format.combine(winston.format.splat(), winston.format.json()),
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })],
|
||||
defaultMeta: { service: 'files' },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
import { AbstractEnv } from '@standardnotes/domain-core'
|
||||
import { config, DotenvParseOutput } from 'dotenv'
|
||||
import { injectable } from 'inversify'
|
||||
|
||||
@injectable()
|
||||
export class Env {
|
||||
private env?: DotenvParseOutput
|
||||
|
||||
public load(): void {
|
||||
export class Env extends AbstractEnv {
|
||||
load(): void {
|
||||
const output = config()
|
||||
this.env = <DotenvParseOutput>output.parsed
|
||||
}
|
||||
|
||||
public get(key: string, optional = false): string {
|
||||
if (!this.env) {
|
||||
this.load()
|
||||
}
|
||||
|
||||
if (!process.env[key] && !optional) {
|
||||
throw new Error(`Environment variable ${key} not set`)
|
||||
}
|
||||
|
||||
return <string>process.env[key]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { ServiceContainerInterface, ServiceIdentifier, ServiceInterface } from '@standardnotes/domain-core'
|
||||
import {
|
||||
ServiceConfiguration,
|
||||
ServiceContainerInterface,
|
||||
ServiceIdentifier,
|
||||
ServiceInterface,
|
||||
} from '@standardnotes/domain-core'
|
||||
import { DirectCallDomainEventPublisher } from '@standardnotes/domain-events-infra'
|
||||
|
||||
import { ContainerConfigLoader } from './Container'
|
||||
import { Transform } from 'stream'
|
||||
|
||||
export class Service implements ServiceInterface {
|
||||
private logger: Transform | undefined
|
||||
|
||||
constructor(
|
||||
private serviceContainer: ServiceContainerInterface,
|
||||
private directCallDomainEventPublisher: DirectCallDomainEventPublisher,
|
||||
@@ -14,20 +16,17 @@ export class Service implements ServiceInterface {
|
||||
this.serviceContainer.register(this.getId(), this)
|
||||
}
|
||||
|
||||
setLogger(logger: Transform): void {
|
||||
this.logger = logger
|
||||
}
|
||||
|
||||
async handleRequest(_request: never, _response: never, _endpointOrMethodIdentifier: string): Promise<unknown> {
|
||||
throw new Error('Requests are handled via inversify-express at ApiGateway level')
|
||||
}
|
||||
|
||||
async getContainer(): Promise<unknown> {
|
||||
async getContainer(configuration?: ServiceConfiguration): Promise<unknown> {
|
||||
const config = new ContainerConfigLoader()
|
||||
|
||||
return config.load({
|
||||
directCallDomainEventPublisher: this.directCallDomainEventPublisher,
|
||||
logger: this.logger,
|
||||
logger: configuration?.logger,
|
||||
environmentOverrides: configuration?.environmentOverrides,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ const TYPES = {
|
||||
Files_FinishUploadSession: Symbol.for('Files_FinishUploadSession'),
|
||||
Files_GetFileMetadata: Symbol.for('Files_GetFileMetadata'),
|
||||
Files_RemoveFile: Symbol.for('Files_RemoveFile'),
|
||||
Files_MoveFile: Symbol.for('Files_MoveFile'),
|
||||
Files_MarkFilesToBeRemoved: Symbol.for('Files_MarkFilesToBeRemoved'),
|
||||
|
||||
// services
|
||||
@@ -23,12 +24,14 @@ const TYPES = {
|
||||
Files_FileUploader: Symbol.for('Files_FileUploader'),
|
||||
Files_FileDownloader: Symbol.for('Files_FileDownloader'),
|
||||
Files_FileRemover: Symbol.for('Files_FileRemover'),
|
||||
Files_FileMover: Symbol.for('Files_FileMover'),
|
||||
|
||||
// repositories
|
||||
Files_UploadRepository: Symbol.for('Files_UploadRepository'),
|
||||
|
||||
// middleware
|
||||
Files_ValetTokenAuthMiddleware: Symbol.for('Files_ValetTokenAuthMiddleware'),
|
||||
Files_SharedVaultValetTokenAuthMiddleware: Symbol.for('Files_SharedVaultValetTokenAuthMiddleware'),
|
||||
|
||||
// env vars
|
||||
Files_S3_ENDPOINT: Symbol.for('Files_S3_ENDPOINT'),
|
||||
|
||||
@@ -14,6 +14,60 @@ describe('DomainEventFactory', () => {
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
|
||||
})
|
||||
|
||||
it('should create a SHARED_VAULT_FILE_UPLOADED event', () => {
|
||||
expect(
|
||||
createFactory().createSharedVaultFileUploadedEvent({
|
||||
sharedVaultUuid: '1-2-3',
|
||||
filePath: 'foo/bar',
|
||||
fileName: 'baz',
|
||||
fileByteSize: 123,
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: new Date(1),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '1-2-3',
|
||||
userIdentifierType: 'shared-vault-uuid',
|
||||
},
|
||||
origin: 'files',
|
||||
},
|
||||
payload: {
|
||||
sharedVaultUuid: '1-2-3',
|
||||
filePath: 'foo/bar',
|
||||
fileName: 'baz',
|
||||
fileByteSize: 123,
|
||||
},
|
||||
type: 'SHARED_VAULT_FILE_UPLOADED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a SHARED_VAULT_FILE_REMOVED event', () => {
|
||||
expect(
|
||||
createFactory().createSharedVaultFileRemovedEvent({
|
||||
sharedVaultUuid: '1-2-3',
|
||||
filePath: 'foo/bar',
|
||||
fileName: 'baz',
|
||||
fileByteSize: 123,
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: new Date(1),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '1-2-3',
|
||||
userIdentifierType: 'shared-vault-uuid',
|
||||
},
|
||||
origin: 'files',
|
||||
},
|
||||
payload: {
|
||||
sharedVaultUuid: '1-2-3',
|
||||
filePath: 'foo/bar',
|
||||
fileName: 'baz',
|
||||
fileByteSize: 123,
|
||||
},
|
||||
type: 'SHARED_VAULT_FILE_REMOVED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a FILE_UPLOADED event', () => {
|
||||
expect(
|
||||
createFactory().createFileUploadedEvent({
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { FileUploadedEvent, FileRemovedEvent, DomainEventService } from '@standardnotes/domain-events'
|
||||
import {
|
||||
FileUploadedEvent,
|
||||
FileRemovedEvent,
|
||||
DomainEventService,
|
||||
SharedVaultFileUploadedEvent,
|
||||
SharedVaultFileRemovedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
@@ -49,4 +55,44 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
createSharedVaultFileUploadedEvent(payload: {
|
||||
sharedVaultUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}): SharedVaultFileUploadedEvent {
|
||||
return {
|
||||
type: 'SHARED_VAULT_FILE_UPLOADED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: payload.sharedVaultUuid,
|
||||
userIdentifierType: 'shared-vault-uuid',
|
||||
},
|
||||
origin: DomainEventService.Files,
|
||||
},
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
createSharedVaultFileRemovedEvent(payload: {
|
||||
sharedVaultUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}): SharedVaultFileRemovedEvent {
|
||||
return {
|
||||
type: 'SHARED_VAULT_FILE_REMOVED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: payload.sharedVaultUuid,
|
||||
userIdentifierType: 'shared-vault-uuid',
|
||||
},
|
||||
origin: DomainEventService.Files,
|
||||
},
|
||||
payload,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { FileUploadedEvent, FileRemovedEvent } from '@standardnotes/domain-events'
|
||||
import {
|
||||
FileUploadedEvent,
|
||||
FileRemovedEvent,
|
||||
SharedVaultFileRemovedEvent,
|
||||
SharedVaultFileUploadedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createFileUploadedEvent(payload: {
|
||||
@@ -14,4 +19,16 @@ export interface DomainEventFactoryInterface {
|
||||
fileByteSize: number
|
||||
regularSubscriptionUuid: string
|
||||
}): FileRemovedEvent
|
||||
createSharedVaultFileUploadedEvent(payload: {
|
||||
sharedVaultUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}): SharedVaultFileUploadedEvent
|
||||
createSharedVaultFileRemovedEvent(payload: {
|
||||
sharedVaultUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}): SharedVaultFileRemovedEvent
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('AccountDeletionRequestedEventHandler', () => {
|
||||
it('should mark files to be remove for user', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
|
||||
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ ownerUuid: '1-2-3' })
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
@@ -66,7 +66,7 @@ describe('AccountDeletionRequestedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
|
||||
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ ownerUuid: '1-2-3' })
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
}
|
||||
|
||||
const response = await this.markFilesToBeRemoved.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
ownerUuid: event.payload.userUuid,
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('SharedSubscriptionInvitationCanceledEventHandler', () => {
|
||||
it('should mark files to be remove for user', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
|
||||
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ ownerUuid: '1-2-3' })
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
@@ -66,7 +66,7 @@ describe('SharedSubscriptionInvitationCanceledEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
|
||||
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ ownerUuid: '1-2-3' })
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ export class SharedSubscriptionInvitationCanceledEventHandler implements DomainE
|
||||
}
|
||||
|
||||
const response = await this.markFilesToBeRemoved.execute({
|
||||
userUuid: event.payload.inviteeIdentifier,
|
||||
ownerUuid: event.payload.inviteeIdentifier,
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
|
||||
3
packages/files/src/Domain/Services/FileMoverInterface.ts
Normal file
3
packages/files/src/Domain/Services/FileMoverInterface.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface FileMoverInterface {
|
||||
moveFile(sourcePath: string, destinationPath: string): Promise<void>
|
||||
}
|
||||
@@ -33,7 +33,7 @@ describe('CreateUploadSession', () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
ownerUuid: '1-2-3',
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
@@ -44,7 +44,7 @@ describe('CreateUploadSession', () => {
|
||||
it('should create an upload session', async () => {
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
ownerUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(fileUploader.createUploadSession).toHaveBeenCalledWith('1-2-3/2-3-4')
|
||||
|
||||
@@ -20,7 +20,7 @@ export class CreateUploadSession implements UseCaseInterface {
|
||||
try {
|
||||
this.logger.debug(`Creating upload session for resource: ${dto.resourceRemoteIdentifier}`)
|
||||
|
||||
const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}`
|
||||
const filePath = `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`
|
||||
|
||||
const uploadId = await this.fileUploader.createUploadSession(filePath)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user