Compare commits

...

8 Commits

Author SHA1 Message Date
standardci
2f94abc9f7 chore(release): publish new version
- @standardnotes/api-gateway@1.58.0
 - @standardnotes/auth-server@1.111.0
 - @standardnotes/home-server@1.5.0
 - @standardnotes/revisions-server@1.16.0
2023-05-25 11:11:19 +00:00
Karol Sójko
c70040fe5d feat: add revisions service to home server (#613)
* feat: add revisions service to home server

* fix: make e2e test suite on home server non-optional

* fix(auth): specs
2023-05-25 12:57:05 +02:00
standardci
4b8a9e448a chore(release): publish new version
- @standardnotes/api-gateway@1.57.0
 - @standardnotes/auth-server@1.110.0
 - @standardnotes/home-server@1.4.4
2023-05-25 07:56:05 +00:00
Karol Sójko
1e4c7d0f31 feat: refactor auth middleware to handle required and optional cross service token scenarios (#612)
* wip: fix variable name

* wip: remove redundant middleware in auth

* fix: auth middleware refactor

* fix(auth): fetching user for key params

* fix(auth): specs

* fix(auth): registering session controller endpoints
2023-05-25 09:43:00 +02:00
Karol Sójko
ec75795a02 fix: session tokens ttl on home-server e2e suite 2023-05-24 13:56:09 +02:00
standardci
ad26b64b28 chore(release): publish new version
- @standardnotes/auth-server@1.109.2
 - @standardnotes/home-server@1.4.3
2023-05-18 10:55:29 +00:00
Karol Sójko
9e4715ebbd fix: skip paid features on the home server e2e test suite 2023-05-18 12:42:20 +02:00
Karol Sójko
cc612296d0 fix(auth): changing user credentials to work both on http proxy and direct code call 2023-05-18 12:42:20 +02:00
78 changed files with 1004 additions and 992 deletions

View File

@@ -50,7 +50,7 @@ jobs:
run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html
e2e-home-server:
name: (WIP - Home Server) E2E Test Suite
name: (Home Server) E2E Test Suite
runs-on: ubuntu-latest
services:
@@ -83,6 +83,9 @@ jobs:
sed -i "s/AUTH_JWT_SECRET=/AUTH_JWT_SECRET=$(openssl rand -hex 32)/g" packages/home-server/.env
sed -i "s/ENCRYPTION_SERVER_KEY=/ENCRYPTION_SERVER_KEY=$(openssl rand -hex 32)/g" packages/home-server/.env
sed -i "s/PSEUDO_KEY_PARAMS_KEY=/PSEUDO_KEY_PARAMS_KEY=$(openssl rand -hex 32)/g" packages/home-server/.env
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
- name: Run Server
run: nohup yarn workspace @standardnotes/home-server start &
@@ -93,5 +96,4 @@ jobs:
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
- name: Run E2E Test Suite
continue-on-error: true
run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html
run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html?skip_paid_features=true

11
.pnp.cjs generated
View File

@@ -4626,6 +4626,7 @@ const RAW_RUNTIME_STATE =
["@standardnotes/auth-server", "workspace:packages/auth"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
["@types/cors", "npm:2.8.13"],\
["@types/express", "npm:4.17.17"],\
@@ -4733,7 +4734,7 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.15"],\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.15"],\
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
["winston", "npm:3.8.2"]\
],\
@@ -4925,7 +4926,7 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.15"],\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.15"],\
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
["ua-parser-js", "npm:1.0.35"],\
["uuid", "npm:9.0.0"],\
@@ -15166,10 +15167,10 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "HARD"\
}],\
["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.15", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-91f15b21d5/0/cache/typeorm-npm-0.3.15-20a6c4f754-db890f14cb.zip/node_modules/typeorm/",\
["virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.15", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-7fe891193c/0/cache/typeorm-npm-0.3.15-20a6c4f754-db890f14cb.zip/node_modules/typeorm/",\
"packageDependencies": [\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.15"],\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.15"],\
["@google-cloud/spanner", null],\
["@sap/hana-client", null],\
["@sqltools/formatter", "npm:1.2.5"],\

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.58.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.57.0...@standardnotes/api-gateway@1.58.0) (2023-05-25)
### Features
* add revisions service to home server ([#613](https://github.com/standardnotes/api-gateway/issues/613)) ([c70040f](https://github.com/standardnotes/api-gateway/commit/c70040fe5dfd35663b9811fbbaa9370bd0298482))
# [1.57.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.56.2...@standardnotes/api-gateway@1.57.0) (2023-05-25)
### Features
* refactor auth middleware to handle required and optional cross service token scenarios ([#612](https://github.com/standardnotes/api-gateway/issues/612)) ([1e4c7d0](https://github.com/standardnotes/api-gateway/commit/1e4c7d0f317d5c2d98065da12ffeb950b10ee5dc))
## [1.56.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.56.1...@standardnotes/api-gateway@1.56.2) (2023-05-18)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.56.2",
"version": "1.58.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -8,7 +8,6 @@ import { Timer, TimerInterface } from '@standardnotes/time'
import { Env } from './Env'
import { TYPES } from './Types'
import { AuthMiddleware } from '../Controller/AuthMiddleware'
import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
import { HttpServiceProxy } from '../Service/Http/HttpServiceProxy'
import { SubscriptionTokenAuthMiddleware } from '../Controller/SubscriptionTokenAuthMiddleware'
@@ -20,6 +19,8 @@ import { DirectCallServiceProxy } from '../Service/Proxy/DirectCallServiceProxy'
import { ServiceContainerInterface } from '@standardnotes/domain-core'
import { EndpointResolverInterface } from '../Service/Resolver/EndpointResolverInterface'
import { EndpointResolver } from '../Service/Resolver/EndpointResolver'
import { RequiredCrossServiceTokenMiddleware } from '../Controller/RequiredCrossServiceTokenMiddleware'
import { OptionalCrossServiceTokenMiddleware } from '../Controller/OptionalCrossServiceTokenMiddleware'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -77,7 +78,12 @@ export class ContainerConfigLoader {
container.bind(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL).toConstantValue(+env.get('CROSS_SERVICE_TOKEN_CACHE_TTL', true))
// Middleware
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware)
container
.bind<RequiredCrossServiceTokenMiddleware>(TYPES.RequiredCrossServiceTokenMiddleware)
.to(RequiredCrossServiceTokenMiddleware)
container
.bind<OptionalCrossServiceTokenMiddleware>(TYPES.OptionalCrossServiceTokenMiddleware)
.to(OptionalCrossServiceTokenMiddleware)
container.bind<WebSocketAuthMiddleware>(TYPES.WebSocketAuthMiddleware).to(WebSocketAuthMiddleware)
container
.bind<SubscriptionTokenAuthMiddleware>(TYPES.SubscriptionTokenAuthMiddleware)

View File

@@ -15,7 +15,8 @@ export const TYPES = {
VERSION: Symbol.for('VERSION'),
CROSS_SERVICE_TOKEN_CACHE_TTL: Symbol.for('CROSS_SERVICE_TOKEN_CACHE_TTL'),
// Middleware
AuthMiddleware: Symbol.for('AuthMiddleware'),
RequiredCrossServiceTokenMiddleware: Symbol.for('RequiredCrossServiceTokenMiddleware'),
OptionalCrossServiceTokenMiddleware: Symbol.for('OptionalCrossServiceTokenMiddleware'),
WebSocketAuthMiddleware: Symbol.for('WebSocketAuthMiddleware'),
SubscriptionTokenAuthMiddleware: Symbol.for('SubscriptionTokenAuthMiddleware'),
// Services

View File

@@ -2,43 +2,33 @@ import { CrossServiceTokenData } from '@standardnotes/security'
import { RoleName } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils'
import { verify } from 'jsonwebtoken'
import { AxiosError } from 'axios'
import { Logger } from 'winston'
import { TYPES } from '../Bootstrap/Types'
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
@injectable()
export class AuthMiddleware extends BaseMiddleware {
export abstract class AuthMiddleware extends BaseMiddleware {
constructor(
@inject(TYPES.ServiceProxy) private serviceProxy: ServiceProxyInterface,
@inject(TYPES.AUTH_JWT_SECRET) private jwtSecret: string,
@inject(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL) private crossServiceTokenCacheTTL: number,
@inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.Logger) private logger: Logger,
private serviceProxy: ServiceProxyInterface,
private jwtSecret: string,
private crossServiceTokenCacheTTL: number,
private crossServiceTokenCache: CrossServiceTokenCacheInterface,
private timer: TimerInterface,
private logger: Logger,
) {
super()
}
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
const authHeaderValue = request.headers.authorization as string
if (!authHeaderValue) {
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid login credentials.',
},
})
if (!this.handleMissingAuthHeader(request.headers.authorization, response, next)) {
return
}
const authHeaderValue = request.headers.authorization as string
try {
let crossServiceTokenFetchedFromCache = true
let crossServiceToken = null
@@ -49,10 +39,7 @@ export class AuthMiddleware extends BaseMiddleware {
if (crossServiceToken === null) {
const authResponse = await this.serviceProxy.validateSession(authHeaderValue)
if (authResponse.status > 200) {
response.setHeader('content-type', authResponse.headers.contentType)
response.status(authResponse.status).send(authResponse.data)
if (!this.handleSessionValidationResponse(authResponse, response, next)) {
return
}
@@ -78,6 +65,7 @@ export class AuthMiddleware extends BaseMiddleware {
}
response.locals.user = decodedToken.user
response.locals.session = decodedToken.session
response.locals.roles = decodedToken.roles
} catch (error) {
const errorMessage = (error as AxiosError).isAxiosError
@@ -105,6 +93,24 @@ export class AuthMiddleware extends BaseMiddleware {
return next()
}
protected abstract handleSessionValidationResponse(
authResponse: {
status: number
data: unknown
headers: {
contentType: string
}
},
response: Response,
next: NextFunction,
): boolean
protected abstract handleMissingAuthHeader(
authHeaderValue: string | undefined,
response: Response,
next: NextFunction,
): boolean
private getCrossServiceTokenCacheExpireTimestamp(token: CrossServiceTokenData): number {
const crossServiceTokenDefaultCacheExpiration = this.timer.getTimestampInSeconds() + this.crossServiceTokenCacheTTL

View File

@@ -29,17 +29,17 @@ export class LegacyController extends BaseHttpController {
])
}
@httpPost('/items/sync', TYPES.AuthMiddleware)
@httpPost('/items/sync', TYPES.RequiredCrossServiceTokenMiddleware)
async legacyItemsSync(request: Request, response: Response): Promise<void> {
await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
}
@httpGet('/items/:item_id/revisions', TYPES.AuthMiddleware)
@httpGet('/items/:item_id/revisions', TYPES.RequiredCrossServiceTokenMiddleware)
async legacyGetRevisions(request: Request, response: Response): Promise<void> {
await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
}
@httpGet('/items/:item_id/revisions/:id', TYPES.AuthMiddleware)
@httpGet('/items/:item_id/revisions/:id', TYPES.RequiredCrossServiceTokenMiddleware)
async legacyGetRevision(request: Request, response: Response): Promise<void> {
await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
}

View File

@@ -0,0 +1,51 @@
import { TimerInterface } from '@standardnotes/time'
import { NextFunction, Response } from 'express'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { TYPES } from '../Bootstrap/Types'
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
import { AuthMiddleware } from './AuthMiddleware'
@injectable()
export class OptionalCrossServiceTokenMiddleware extends AuthMiddleware {
constructor(
@inject(TYPES.ServiceProxy) serviceProxy: ServiceProxyInterface,
@inject(TYPES.AUTH_JWT_SECRET) jwtSecret: string,
@inject(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL) crossServiceTokenCacheTTL: number,
@inject(TYPES.CrossServiceTokenCache) crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Timer) timer: TimerInterface,
@inject(TYPES.Logger) logger: Logger,
) {
super(serviceProxy, jwtSecret, crossServiceTokenCacheTTL, crossServiceTokenCache, timer, logger)
}
protected override handleSessionValidationResponse(
authResponse: { status: number; data: unknown; headers: { contentType: string } },
_response: Response,
next: NextFunction,
): boolean {
if (authResponse.status > 200) {
next()
return false
}
return true
}
protected override handleMissingAuthHeader(
authHeaderValue: string | undefined,
_response: Response,
next: NextFunction,
): boolean {
if (!authHeaderValue) {
next()
return false
}
return true
}
}

View File

@@ -0,0 +1,57 @@
import { TimerInterface } from '@standardnotes/time'
import { NextFunction, Response } from 'express'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { TYPES } from '../Bootstrap/Types'
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
import { AuthMiddleware } from './AuthMiddleware'
@injectable()
export class RequiredCrossServiceTokenMiddleware extends AuthMiddleware {
constructor(
@inject(TYPES.ServiceProxy) serviceProxy: ServiceProxyInterface,
@inject(TYPES.AUTH_JWT_SECRET) jwtSecret: string,
@inject(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL) crossServiceTokenCacheTTL: number,
@inject(TYPES.CrossServiceTokenCache) crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Timer) timer: TimerInterface,
@inject(TYPES.Logger) logger: Logger,
) {
super(serviceProxy, jwtSecret, crossServiceTokenCacheTTL, crossServiceTokenCache, timer, logger)
}
protected override handleSessionValidationResponse(
authResponse: { status: number; data: unknown; headers: { contentType: string } },
response: Response,
_next: NextFunction,
): boolean {
if (authResponse.status > 200) {
response.setHeader('content-type', authResponse.headers.contentType)
response.status(authResponse.status).send(authResponse.data)
return false
}
return true
}
protected override handleMissingAuthHeader(
authHeaderValue: string | undefined,
response: Response,
_next: NextFunction,
): boolean {
if (!authHeaderValue) {
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid login credentials.',
},
})
return false
}
return true
}
}

View File

@@ -24,7 +24,7 @@ export class ActionsController extends BaseHttpController {
)
}
@httpGet('/login-params')
@httpGet('/login-params', TYPES.OptionalCrossServiceTokenMiddleware)
async loginParams(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callAuthServer(
request,
@@ -34,7 +34,7 @@ export class ActionsController extends BaseHttpController {
)
}
@httpPost('/logout')
@httpPost('/logout', TYPES.OptionalCrossServiceTokenMiddleware)
async logout(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callAuthServer(
request,
@@ -54,7 +54,7 @@ export class ActionsController extends BaseHttpController {
)
}
@httpPost('/recovery/codes', TYPES.AuthMiddleware)
@httpPost('/recovery/codes', TYPES.RequiredCrossServiceTokenMiddleware)
async recoveryCodes(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callAuthServer(
request,

View File

@@ -15,7 +15,7 @@ export class AuthenticatorsController extends BaseHttpController {
super()
}
@httpDelete('/:authenticatorId', TYPES.AuthMiddleware)
@httpDelete('/:authenticatorId', TYPES.RequiredCrossServiceTokenMiddleware)
async delete(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -29,7 +29,7 @@ export class AuthenticatorsController extends BaseHttpController {
)
}
@httpGet('/', TYPES.AuthMiddleware)
@httpGet('/', TYPES.RequiredCrossServiceTokenMiddleware)
async list(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -39,7 +39,7 @@ export class AuthenticatorsController extends BaseHttpController {
)
}
@httpGet('/generate-registration-options', TYPES.AuthMiddleware)
@httpGet('/generate-registration-options', TYPES.RequiredCrossServiceTokenMiddleware)
async generateRegistrationOptions(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -59,7 +59,7 @@ export class AuthenticatorsController extends BaseHttpController {
)
}
@httpPost('/verify-registration', TYPES.AuthMiddleware)
@httpPost('/verify-registration', TYPES.RequiredCrossServiceTokenMiddleware)
async verifyRegistration(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -15,7 +15,7 @@ export class FilesController extends BaseHttpController {
super()
}
@httpPost('/valet-tokens', TYPES.AuthMiddleware)
@httpPost('/valet-tokens', TYPES.RequiredCrossServiceTokenMiddleware)
async createToken(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -5,7 +5,7 @@ import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
@controller('/v1/items', TYPES.AuthMiddleware)
@controller('/v1/items', TYPES.RequiredCrossServiceTokenMiddleware)
export class ItemsController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private serviceProxy: ServiceProxyInterface,

View File

@@ -1,7 +1,7 @@
import { BaseHttpController, controller, httpDelete, httpGet, results } from 'inversify-express-utils'
import { TYPES } from '../../Bootstrap/Types'
@controller('/v1/items/:item_id/revisions', TYPES.AuthMiddleware)
@controller('/v1/items/:item_id/revisions', TYPES.RequiredCrossServiceTokenMiddleware)
export class RevisionsController extends BaseHttpController {
@httpGet('/')
async getRevisions(): Promise<results.JsonResult> {

View File

@@ -14,7 +14,7 @@ export class SessionsController extends BaseHttpController {
super()
}
@httpGet('/', TYPES.AuthMiddleware)
@httpGet('/', TYPES.RequiredCrossServiceTokenMiddleware)
async getSessions(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -23,7 +23,7 @@ export class SessionsController extends BaseHttpController {
)
}
@httpDelete('/:uuid', TYPES.AuthMiddleware)
@httpDelete('/:uuid', TYPES.RequiredCrossServiceTokenMiddleware)
async deleteSession(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -35,7 +35,7 @@ export class SessionsController extends BaseHttpController {
)
}
@httpDelete('/', TYPES.AuthMiddleware)
@httpDelete('/', TYPES.RequiredCrossServiceTokenMiddleware)
async deleteSessions(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -15,7 +15,7 @@ export class SubscriptionInvitesController extends BaseHttpController {
super()
}
@httpPost('/', TYPES.AuthMiddleware)
@httpPost('/', TYPES.RequiredCrossServiceTokenMiddleware)
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -25,7 +25,7 @@ export class SubscriptionInvitesController extends BaseHttpController {
)
}
@httpGet('/', TYPES.AuthMiddleware)
@httpGet('/', TYPES.RequiredCrossServiceTokenMiddleware)
async listInvites(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -35,7 +35,7 @@ export class SubscriptionInvitesController extends BaseHttpController {
)
}
@httpDelete('/:inviteUuid', TYPES.AuthMiddleware)
@httpDelete('/:inviteUuid', TYPES.RequiredCrossServiceTokenMiddleware)
async cancelSubscriptionSharing(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -48,7 +48,7 @@ export class SubscriptionInvitesController extends BaseHttpController {
)
}
@httpPost('/:inviteUuid/accept', TYPES.AuthMiddleware)
@httpPost('/:inviteUuid/accept', TYPES.RequiredCrossServiceTokenMiddleware)
async acceptInvite(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -15,7 +15,7 @@ export class TokensController extends BaseHttpController {
super()
}
@httpPost('/', TYPES.AuthMiddleware)
@httpPost('/', TYPES.RequiredCrossServiceTokenMiddleware)
async createToken(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -37,7 +37,7 @@ export class UsersController extends BaseHttpController {
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/send-activation-code', request.body)
}
@httpPatch('/:userId', TYPES.AuthMiddleware)
@httpPatch('/:userId', TYPES.RequiredCrossServiceTokenMiddleware)
async updateUser(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -47,7 +47,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpPut('/:userUuid/password', TYPES.AuthMiddleware)
@httpPut('/:userUuid/password', TYPES.RequiredCrossServiceTokenMiddleware)
async changePassword(request: Request, response: Response): Promise<void> {
this.logger.debug(
'[DEPRECATED] use endpoint /v1/users/:userUuid/attributes/credentials instead of /v1/users/:userUuid/password',
@@ -65,7 +65,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpPut('/:userUuid/attributes/credentials', TYPES.AuthMiddleware)
@httpPut('/:userUuid/attributes/credentials', TYPES.RequiredCrossServiceTokenMiddleware)
async changeCredentials(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -79,7 +79,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userId/params', TYPES.AuthMiddleware)
@httpGet('/:userId/params', TYPES.RequiredCrossServiceTokenMiddleware)
async getKeyParams(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -88,12 +88,12 @@ export class UsersController extends BaseHttpController {
)
}
@all('/:userId/mfa', TYPES.AuthMiddleware)
@all('/:userId/mfa', TYPES.RequiredCrossServiceTokenMiddleware)
async blockMFA(): Promise<results.StatusCodeResult> {
return this.statusCode(401)
}
@httpPost('/:userUuid/integrations/listed', TYPES.AuthMiddleware)
@httpPost('/:userUuid/integrations/listed', TYPES.RequiredCrossServiceTokenMiddleware)
async createListedAccount(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -113,7 +113,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/settings', TYPES.AuthMiddleware)
@httpGet('/:userUuid/settings', TYPES.RequiredCrossServiceTokenMiddleware)
async listSettings(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -126,7 +126,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpPut('/:userUuid/settings', TYPES.AuthMiddleware)
@httpPut('/:userUuid/settings', TYPES.RequiredCrossServiceTokenMiddleware)
async putSetting(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -140,7 +140,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/settings/:settingName', TYPES.AuthMiddleware)
@httpGet('/:userUuid/settings/:settingName', TYPES.RequiredCrossServiceTokenMiddleware)
async getSetting(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -154,7 +154,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpDelete('/:userUuid/settings/:settingName', TYPES.AuthMiddleware)
@httpDelete('/:userUuid/settings/:settingName', TYPES.RequiredCrossServiceTokenMiddleware)
async deleteSetting(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -169,7 +169,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/subscription-settings/:subscriptionSettingName', TYPES.AuthMiddleware)
@httpGet('/:userUuid/subscription-settings/:subscriptionSettingName', TYPES.RequiredCrossServiceTokenMiddleware)
async getSubscriptionSetting(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -183,7 +183,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/features', TYPES.AuthMiddleware)
@httpGet('/:userUuid/features', TYPES.RequiredCrossServiceTokenMiddleware)
async getFeatures(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -196,7 +196,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/subscription', TYPES.AuthMiddleware)
@httpGet('/:userUuid/subscription', TYPES.RequiredCrossServiceTokenMiddleware)
async getSubscription(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -232,12 +232,12 @@ export class UsersController extends BaseHttpController {
)
}
@httpDelete('/:userUuid', TYPES.AuthMiddleware)
@httpDelete('/:userUuid', TYPES.RequiredCrossServiceTokenMiddleware)
async deleteUser(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'api/account', request.body)
}
@httpPost('/:userUuid/requests', TYPES.AuthMiddleware)
@httpPost('/:userUuid/requests', TYPES.RequiredCrossServiceTokenMiddleware)
async submitRequest(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -17,7 +17,7 @@ export class WebSocketsController extends BaseHttpController {
super()
}
@httpPost('/tokens', TYPES.AuthMiddleware)
@httpPost('/tokens', TYPES.RequiredCrossServiceTokenMiddleware)
async createWebSocketConnectionToken(request: Request, response: Response): Promise<void> {
await this.httpService.callWebSocketServer(
request,

View File

@@ -9,7 +9,7 @@ import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolv
@controller('/v2')
export class ActionsControllerV2 extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ServiceProxy) private serviceProxy: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
@@ -17,7 +17,7 @@ export class ActionsControllerV2 extends BaseHttpController {
@httpPost('/login')
async login(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
await this.serviceProxy.callAuthServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'auth/pkce_sign_in'),
@@ -25,9 +25,9 @@ export class ActionsControllerV2 extends BaseHttpController {
)
}
@httpPost('/login-params')
@httpPost('/login-params', TYPES.OptionalCrossServiceTokenMiddleware)
async loginParams(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
await this.serviceProxy.callAuthServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'auth/pkce_params'),

View File

@@ -6,7 +6,7 @@ import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
@controller('/v2/items/:itemUuid/revisions', TYPES.AuthMiddleware)
@controller('/v2/items/:itemUuid/revisions', TYPES.RequiredCrossServiceTokenMiddleware)
export class RevisionsControllerV2 extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@@ -28,7 +28,7 @@ export class RevisionsControllerV2 extends BaseHttpController {
)
}
@httpGet('/:id')
@httpGet('/:uuid')
async getRevision(request: Request, response: Response): Promise<void> {
await this.httpService.callRevisionsServer(
request,
@@ -37,12 +37,12 @@ export class RevisionsControllerV2 extends BaseHttpController {
'GET',
'items/:itemUuid/revisions/:id',
request.params.itemUuid,
request.params.id,
request.params.uuid,
),
)
}
@httpDelete('/:id')
@httpDelete('/:uuid')
async deleteRevision(request: Request, response: Response): Promise<void> {
await this.httpService.callRevisionsServer(
request,
@@ -51,7 +51,7 @@ export class RevisionsControllerV2 extends BaseHttpController {
'DELETE',
'items/:itemUuid/revisions/:id',
request.params.itemUuid,
request.params.id,
request.params.uuid,
),
)
}

View File

@@ -66,7 +66,12 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
throw new Error('Revisions service not found')
}
await service.handleRequest(request, response, endpointOrMethodIdentifier)
const serviceResponse = (await service.handleRequest(request, response, endpointOrMethodIdentifier)) as {
statusCode: number
json: Record<string, unknown>
}
this.sendDecoratedResponse(response, serviceResponse)
}
async callSyncingServer(request: never, response: never, endpointOrMethodIdentifier: string): Promise<void> {

View File

@@ -58,6 +58,10 @@ export class EndpointResolver implements EndpointResolverInterface {
['[POST]:items/sync', 'sync.items.sync'],
['[POST]:items/check-integrity', 'sync.items.check_integrity'],
['[GET]:items/:uuid', 'sync.items.get_item'],
// Revisions Controller V2
['[GET]:items/:itemUuid/revisions', 'revisions.revisions.getRevisions'],
['[GET]:items/:itemUuid/revisions/:id', 'revisions.revisions.getRevision'],
['[DELETE]:items/:itemUuid/revisions/:id', 'revisions.revisions.deleteRevision'],
])
resolveEndpointOrMethodIdentifier(method: string, endpoint: string, ...params: string[]): string {

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.111.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.110.0...@standardnotes/auth-server@1.111.0) (2023-05-25)
### Features
* add revisions service to home server ([#613](https://github.com/standardnotes/server/issues/613)) ([c70040f](https://github.com/standardnotes/server/commit/c70040fe5dfd35663b9811fbbaa9370bd0298482))
# [1.110.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.109.2...@standardnotes/auth-server@1.110.0) (2023-05-25)
### Features
* refactor auth middleware to handle required and optional cross service token scenarios ([#612](https://github.com/standardnotes/server/issues/612)) ([1e4c7d0](https://github.com/standardnotes/server/commit/1e4c7d0f317d5c2d98065da12ffeb950b10ee5dc))
## [1.109.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.109.1...@standardnotes/auth-server@1.109.2) (2023-05-18)
### Bug Fixes
* **auth:** changing user credentials to work both on http proxy and direct code call ([cc61229](https://github.com/standardnotes/server/commit/cc612296d0fbfa7e95556fda45eb9706845e4f58))
## [1.109.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.109.0...@standardnotes/auth-server@1.109.1) (2023-05-18)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.109.1",
"version": "1.111.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -241,17 +241,16 @@ import { InversifyExpressSubscriptionTokensController } from '../Infra/Inversify
import { InversifyExpressSubscriptionSettingsController } from '../Infra/InversifyExpressUtils/InversifyExpressSubscriptionSettingsController'
import { InversifyExpressSettingsController } from '../Infra/InversifyExpressUtils/InversifyExpressSettingsController'
import { SessionMiddleware } from '../Infra/InversifyExpressUtils/Middleware/SessionMiddleware'
import { ApiGatewayAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/ApiGatewayAuthMiddleware'
import { ApiGatewayOfflineAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/ApiGatewayOfflineAuthMiddleware'
import { AuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/AuthMiddleware'
import { OfflineUserAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware'
import { AuthMiddlewareWithoutResponse } from '../Infra/InversifyExpressUtils/Middleware/AuthMiddlewareWithoutResponse'
import { LockMiddleware } from '../Infra/InversifyExpressUtils/Middleware/LockMiddleware'
import { InversifyExpressSessionController } from '../Infra/InversifyExpressUtils/InversifyExpressSessionController'
import { InversifyExpressOfflineController } from '../Infra/InversifyExpressUtils/InversifyExpressOfflineController'
import { InversifyExpressListedController } from '../Infra/InversifyExpressUtils/InversifyExpressListedController'
import { InversifyExpressInternalController } from '../Infra/InversifyExpressUtils/InversifyExpressInternalController'
import { InversifyExpressFeaturesController } from '../Infra/InversifyExpressUtils/InversifyExpressFeaturesController'
import { RequiredCrossServiceTokenMiddleware } from '../Infra/InversifyExpressUtils/Middleware/RequiredCrossServiceTokenMiddleware'
import { OptionalCrossServiceTokenMiddleware } from '../Infra/InversifyExpressUtils/Middleware/OptionalCrossServiceTokenMiddleware'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -448,13 +447,14 @@ export class ContainerConfigLoader {
)
// Middleware
container.bind<AuthMiddleware>(TYPES.Auth_AuthMiddleware).to(AuthMiddleware)
container.bind<SessionMiddleware>(TYPES.Auth_SessionMiddleware).to(SessionMiddleware)
container.bind<LockMiddleware>(TYPES.Auth_LockMiddleware).to(LockMiddleware)
container
.bind<AuthMiddlewareWithoutResponse>(TYPES.Auth_AuthMiddlewareWithoutResponse)
.to(AuthMiddlewareWithoutResponse)
container.bind<ApiGatewayAuthMiddleware>(TYPES.Auth_ApiGatewayAuthMiddleware).to(ApiGatewayAuthMiddleware)
.bind<RequiredCrossServiceTokenMiddleware>(TYPES.Auth_RequiredCrossServiceTokenMiddleware)
.to(RequiredCrossServiceTokenMiddleware)
container
.bind<OptionalCrossServiceTokenMiddleware>(TYPES.Auth_OptionalCrossServiceTokenMiddleware)
.to(OptionalCrossServiceTokenMiddleware)
container
.bind<ApiGatewayOfflineAuthMiddleware>(TYPES.Auth_ApiGatewayOfflineAuthMiddleware)
.to(ApiGatewayOfflineAuthMiddleware)

View File

@@ -51,11 +51,10 @@ const TYPES = {
Auth_ORMAuthenticatorChallengeRepository: Symbol.for('Auth_ORMAuthenticatorChallengeRepository'),
Auth_ORMCacheEntryRepository: Symbol.for('Auth_ORMCacheEntryRepository'),
// Middleware
Auth_AuthMiddleware: Symbol.for('Auth_AuthMiddleware'),
Auth_ApiGatewayAuthMiddleware: Symbol.for('Auth_ApiGatewayAuthMiddleware'),
Auth_RequiredCrossServiceTokenMiddleware: Symbol.for('Auth_RequiredCrossServiceTokenMiddleware'),
Auth_OptionalCrossServiceTokenMiddleware: Symbol.for('Auth_OptionalCrossServiceTokenMiddleware'),
Auth_ApiGatewayOfflineAuthMiddleware: Symbol.for('Auth_ApiGatewayOfflineAuthMiddleware'),
Auth_OfflineUserAuthMiddleware: Symbol.for('Auth_OfflineUserAuthMiddleware'),
Auth_AuthMiddlewareWithoutResponse: Symbol.for('Auth_AuthMiddlewareWithoutResponse'),
Auth_LockMiddleware: Symbol.for('Auth_LockMiddleware'),
Auth_SessionMiddleware: Symbol.for('Auth_SessionMiddleware'),
// Projectors

View File

@@ -11,6 +11,7 @@ import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { ChangeCredentials } from './ChangeCredentials'
import { Username } from '@standardnotes/domain-core'
describe('ChangeCredentials', () => {
let userRepository: UserRepositoryInterface
@@ -25,9 +26,6 @@ describe('ChangeCredentials', () => {
new ChangeCredentials(userRepository, authResponseFactoryResolver, domainEventPublisher, domainEventFactory, timer)
beforeEach(() => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.save = jest.fn()
authResponseFactory = {} as jest.Mocked<AuthResponseFactoryInterface>
authResponseFactory.createResponse = jest.fn().mockReturnValue({ foo: 'bar' })
@@ -39,6 +37,10 @@ describe('ChangeCredentials', () => {
user.uuid = '1-2-3'
user.email = 'test@test.te'
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.save = jest.fn()
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
@@ -52,7 +54,7 @@ describe('ChangeCredentials', () => {
it('should change password', async () => {
expect(
await createUseCase().execute({
user,
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
@@ -82,11 +84,11 @@ describe('ChangeCredentials', () => {
})
it('should change email', async () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValueOnce(user).mockReturnValueOnce(null)
expect(
await createUseCase().execute({
user,
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
@@ -117,11 +119,14 @@ describe('ChangeCredentials', () => {
})
it('should not change email if already taken', async () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue({} as jest.Mocked<User>)
userRepository.findOneByUsernameOrEmail = jest
.fn()
.mockReturnValueOnce(user)
.mockReturnValueOnce({} as jest.Mocked<User>)
expect(
await createUseCase().execute({
user,
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
@@ -144,7 +149,7 @@ describe('ChangeCredentials', () => {
it('should not change email if the new email is invalid', async () => {
expect(
await createUseCase().execute({
user,
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
@@ -164,10 +169,35 @@ describe('ChangeCredentials', () => {
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should not change email if the user is not found', async () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: '',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
}),
).toEqual({
success: false,
errorMessage: 'User not found.',
})
expect(userRepository.save).not.toHaveBeenCalled()
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should not change password if current password is incorrect', async () => {
expect(
await createUseCase().execute({
user,
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'test123',
newPassword: 'test234',
@@ -185,7 +215,7 @@ describe('ChangeCredentials', () => {
it('should update protocol version while changing password', async () => {
expect(
await createUseCase().execute({
user,
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',

View File

@@ -25,14 +25,22 @@ export class ChangeCredentials implements UseCaseInterface {
) {}
async execute(dto: ChangeCredentialsDTO): Promise<ChangeCredentialsResponse> {
if (!(await bcrypt.compare(dto.currentPassword, dto.user.encryptedPassword))) {
const user = await this.userRepository.findOneByUsernameOrEmail(dto.username)
if (!user) {
return {
success: false,
errorMessage: 'User not found.',
}
}
if (!(await bcrypt.compare(dto.currentPassword, user.encryptedPassword))) {
return {
success: false,
errorMessage: 'The current password you entered is incorrect. Please try again.',
}
}
dto.user.encryptedPassword = await bcrypt.hash(dto.newPassword, User.PASSWORD_HASH_COST)
user.encryptedPassword = await bcrypt.hash(dto.newPassword, User.PASSWORD_HASH_COST)
let userEmailChangedEvent: UserEmailChangedEvent | undefined = undefined
if (dto.newEmail !== undefined) {
@@ -54,27 +62,27 @@ export class ChangeCredentials implements UseCaseInterface {
}
userEmailChangedEvent = this.domainEventFactory.createUserEmailChangedEvent(
dto.user.uuid,
dto.user.email,
user.uuid,
user.email,
newUsername.value,
)
dto.user.email = newUsername.value
user.email = newUsername.value
}
dto.user.pwNonce = dto.pwNonce
user.pwNonce = dto.pwNonce
if (dto.protocolVersion) {
dto.user.version = dto.protocolVersion
user.version = dto.protocolVersion
}
if (dto.kpCreated) {
dto.user.kpCreated = dto.kpCreated
user.kpCreated = dto.kpCreated
}
if (dto.kpOrigination) {
dto.user.kpOrigination = dto.kpOrigination
user.kpOrigination = dto.kpOrigination
}
dto.user.updatedAt = this.timer.getUTCDate()
user.updatedAt = this.timer.getUTCDate()
const updatedUser = await this.userRepository.save(dto.user)
const updatedUser = await this.userRepository.save(user)
if (userEmailChangedEvent !== undefined) {
await this.domainEventPublisher.publish(userEmailChangedEvent)

View File

@@ -1,7 +1,7 @@
import { User } from '../../User/User'
import { Username } from '@standardnotes/domain-core'
export type ChangeCredentialsDTO = {
user: User
username: Username
apiVersion: string
currentPassword: string
newPassword: string

View File

@@ -35,9 +35,7 @@ describe('GetUserKeyParams', () => {
})
it('should get key params for an authenticated user - searching by email', async () => {
expect(
await createUseCase().execute({ email: 'test@test.te', authenticated: true, authenticatedUser: user }),
).toEqual({
expect(await createUseCase().execute({ email: 'test@test.te', authenticated: true })).toEqual({
keyParams: {
foo: 'bar',
},
@@ -63,7 +61,7 @@ describe('GetUserKeyParams', () => {
})
it('should get key params for an authenticated user - searching by uuid', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', authenticated: true, authenticatedUser: user })).toEqual({
expect(await createUseCase().execute({ userUuid: '1-2-3', authenticated: true })).toEqual({
keyParams: {
foo: 'bar',
},

View File

@@ -22,16 +22,6 @@ export class GetUserKeyParams implements UseCaseInterface {
) {}
async execute(dto: GetUserKeyParamsDTO): Promise<GetUserKeyParamsResponse> {
if (dto.authenticatedUser) {
this.logger.debug(`Creating key params for authenticated user ${dto.authenticatedUser.email}`)
const keyParams = await this.createKeyParams(dto, dto.authenticatedUser, true)
return {
keyParams,
}
}
let user: User | null = null
if (dto.email !== undefined) {
const usernameOrError = Username.create(dto.email)

View File

@@ -1,8 +1,5 @@
import { User } from '../../User/User'
export type GetUserKeyParamsDTOV1Unchallenged = {
authenticated: boolean
email?: string
userUuid?: string
authenticatedUser?: User
}

View File

@@ -1,9 +1,6 @@
import { User } from '../../User/User'
export type GetUserKeyParamsDTOV2Challenged = {
authenticated: boolean
codeChallenge: string
email?: string
userUuid?: string
authenticatedUser?: User
}

View File

@@ -44,13 +44,12 @@ export class InversifyExpressAuthController extends BaseHttpController {
this.controllerContainer.register('auth.signOut', this.signOut.bind(this))
}
@httpGet('/params', TYPES.Auth_AuthMiddlewareWithoutResponse)
@httpGet('/params', TYPES.Auth_OptionalCrossServiceTokenMiddleware)
async params(request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.session) {
const result = await this.getUserKeyParams.execute({
email: response.locals.user.email,
authenticated: true,
authenticatedUser: response.locals.user,
})
return this.json(result.keyParams)
@@ -155,7 +154,7 @@ export class InversifyExpressAuthController extends BaseHttpController {
return this.json(signInResult.authResponse)
}
@httpPost('/pkce_params', TYPES.Auth_AuthMiddlewareWithoutResponse)
@httpPost('/pkce_params', TYPES.Auth_OptionalCrossServiceTokenMiddleware)
async pkceParams(request: Request, response: Response): Promise<results.JsonResult> {
if (!request.body.code_challenge) {
return this.json(
@@ -172,7 +171,6 @@ export class InversifyExpressAuthController extends BaseHttpController {
const result = await this.getUserKeyParams.execute({
email: response.locals.user.email,
authenticated: true,
authenticatedUser: response.locals.user,
codeChallenge: request.body.code_challenge as string,
})
@@ -261,7 +259,7 @@ export class InversifyExpressAuthController extends BaseHttpController {
return this.json(signInResult.authResponse)
}
@httpPost('/recovery/codes', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPost('/recovery/codes', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async generateRecoveryCodes(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authController.generateRecoveryCodes({
userUuid: response.locals.user.uuid,
@@ -296,7 +294,7 @@ export class InversifyExpressAuthController extends BaseHttpController {
return this.json(result.data, result.status)
}
@httpPost('/sign_out', TYPES.Auth_AuthMiddlewareWithoutResponse)
@httpPost('/sign_out', TYPES.Auth_OptionalCrossServiceTokenMiddleware)
async signOut(request: Request, response: Response): Promise<results.JsonResult | void> {
const result = await this.authController.signOut({
readOnlyAccess: response.locals.readOnlyAccess,

View File

@@ -37,7 +37,7 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
)
}
@httpGet('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async list(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.list({
userUuid: response.locals.user.uuid,
@@ -46,7 +46,7 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
return this.json(result.data, result.status)
}
@httpDelete('/:authenticatorId', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpDelete('/:authenticatorId', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async delete(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.delete({
userUuid: response.locals.user.uuid,
@@ -56,7 +56,7 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
return this.json(result.data, result.status)
}
@httpGet('/generate-registration-options', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/generate-registration-options', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.generateRegistrationOptions({
username: response.locals.user.email,
@@ -66,7 +66,7 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
return this.json(result.data, result.status)
}
@httpPost('/verify-registration', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPost('/verify-registration', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async verifyRegistration(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.verifyRegistrationResponse({
userUuid: response.locals.user.uuid,

View File

@@ -22,7 +22,7 @@ export class InversifyExpressFeaturesController extends BaseHttpController {
this.controllerContainer.register('auth.users.getFeatures', this.getFeatures.bind(this))
}
@httpGet('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async getFeatures(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
return this.json(

View File

@@ -18,7 +18,7 @@ export class InversifyExpressListedController extends BaseHttpController {
this.controllerContainer.register('auth.users.createListedAccount', this.createListedAccount.bind(this))
}
@httpPost('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPost('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async createListedAccount(_request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
return this.json(

View File

@@ -64,16 +64,12 @@ describe('InversifyExpressSessionController', () => {
},
})
await createController().refresh(request, response)
const httpResult = <results.JsonResult>await createController().refresh(request, response)
const result = await httpResult.executeAsync()
expect(response.send).toHaveBeenCalledWith({
session: {
access_token: '1231',
refresh_token: '2341',
access_expiration: 123123,
refresh_expiration: 123123,
},
})
expect(await result.content.readAsStringAsync()).toEqual(
'{"session":{"access_token":"1231","refresh_token":"2341","access_expiration":123123,"refresh_expiration":123123}}',
)
})
it('should return bad request if tokens are missing from refresh token request', async () => {
@@ -113,14 +109,15 @@ describe('InversifyExpressSessionController', () => {
}
request.body.uuid = '123'
await createController().deleteSession(request, response)
const httpResult = <results.JsonResult>await createController().deleteSession(request, response)
const result = await httpResult.executeAsync()
expect(deleteSessionForUser.execute).toBeCalledWith({
userUuid: '123',
sessionUuid: '123',
})
expect(response.status).toHaveBeenCalledWith(204)
expect(result.statusCode).toEqual(204)
})
it('should not delete a specific session is current session has read only access', async () => {
@@ -205,15 +202,16 @@ describe('InversifyExpressSessionController', () => {
uuid: '234',
},
}
await createController().deleteAllSessions(request, response)
const httpResult = <results.JsonResult>await createController().deleteAllSessions(request, response)
const result = await httpResult.executeAsync()
expect(deletePreviousSessionsForUser.execute).toHaveBeenCalledWith({
userUuid: '123',
currentSessionUuid: '234',
})
expect(response.status).toHaveBeenCalledWith(204)
expect(response.send).toHaveBeenCalled()
expect(result.statusCode).toEqual(204)
})
it('should not delete all sessions if current sessions has read only access', async () => {

View File

@@ -26,13 +26,13 @@ export class InversifyExpressSessionController extends BaseHttpController {
) {
super()
this.controllerContainer.register('auth.session.delete', this.deleteSession.bind(this))
this.controllerContainer.register('auth.session.deleteAll', this.deleteAllSessions.bind(this))
this.controllerContainer.register('auth.session.refresh', this.refresh.bind(this))
this.controllerContainer.register('auth.sessions.delete', this.deleteSession.bind(this))
this.controllerContainer.register('auth.sessions.deleteAll', this.deleteAllSessions.bind(this))
this.controllerContainer.register('auth.sessions.refresh', this.refresh.bind(this))
}
@httpDelete('/', TYPES.Auth_AuthMiddleware, TYPES.Auth_SessionMiddleware)
async deleteSession(request: Request, response: Response): Promise<results.JsonResult | void> {
@httpDelete('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)
async deleteSession(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
if (response.locals.readOnlyAccess) {
return this.json(
{
@@ -84,11 +84,15 @@ export class InversifyExpressSessionController extends BaseHttpController {
}
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
response.status(204).send()
return this.statusCode(204)
}
@httpDelete('/all', TYPES.Auth_AuthMiddleware, TYPES.Auth_SessionMiddleware)
async deleteAllSessions(_request: Request, response: Response): Promise<results.JsonResult | void> {
@httpDelete('/all', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)
async deleteAllSessions(
_request: Request,
response: Response,
): Promise<results.JsonResult | results.StatusCodeResult> {
if (response.locals.readOnlyAccess) {
return this.json(
{
@@ -118,11 +122,12 @@ export class InversifyExpressSessionController extends BaseHttpController {
})
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
response.status(204).send()
return this.statusCode(204)
}
@httpPost('/refresh')
async refresh(request: Request, response: Response): Promise<results.JsonResult | void> {
async refresh(request: Request, response: Response): Promise<results.JsonResult> {
if (!request.body.access_token || !request.body.refresh_token) {
return this.json(
{
@@ -152,7 +157,7 @@ export class InversifyExpressSessionController extends BaseHttpController {
}
response.setHeader('x-invalidate-cache', result.userUuid as string)
response.send({
return this.json({
session: result.sessionPayload,
})
}

View File

@@ -62,7 +62,7 @@ export class InversifyExpressSessionsController extends BaseHttpController {
return this.json({ authToken: result.token })
}
@httpGet('/', TYPES.Auth_AuthMiddleware, TYPES.Auth_SessionMiddleware)
@httpGet('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)
async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
return this.json([])

View File

@@ -35,7 +35,7 @@ export class InversifyExpressSettingsController extends BaseHttpController {
this.controllerContainer.register('auth.users.deleteSetting', this.deleteSetting.bind(this))
}
@httpGet('/settings', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/settings', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async getSettings(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
return this.json(
@@ -54,7 +54,7 @@ export class InversifyExpressSettingsController extends BaseHttpController {
return this.json(result)
}
@httpGet('/settings/:settingName', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/settings/:settingName', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async getSetting(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
return this.json(
@@ -77,7 +77,7 @@ export class InversifyExpressSettingsController extends BaseHttpController {
return this.json(result, 400)
}
@httpPut('/settings', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPut('/settings', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
if (response.locals.readOnlyAccess) {
return this.json(
@@ -124,7 +124,7 @@ export class InversifyExpressSettingsController extends BaseHttpController {
return this.json(result, result.statusCode)
}
@httpDelete('/settings/:settingName', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpDelete('/settings/:settingName', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async deleteSetting(request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
return this.json(

View File

@@ -31,15 +31,16 @@ export class InversifyExpressSubscriptionInvitesController extends BaseHttpContr
this.controllerContainer.register('auth.subscriptionInvites.list', this.listInvites.bind(this))
}
@httpPost('/:inviteUuid/accept', TYPES.Auth_ApiGatewayAuthMiddleware)
async acceptInvite(request: Request, response: Response): Promise<void> {
@httpPost('/:inviteUuid/accept', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async acceptInvite(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.subscriptionInvitesController.acceptInvite({
api: request.query.api as ApiVersion,
inviteUuid: request.params.inviteUuid,
})
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
response.status(result.status).send(result.data)
return this.json(result.data, result.status)
}
@httpGet('/:inviteUuid/decline')
@@ -52,7 +53,7 @@ export class InversifyExpressSubscriptionInvitesController extends BaseHttpContr
return this.json(response.data, response.status)
}
@httpPost('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPost('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.subscriptionInvitesController.invite({
...request.body,
@@ -64,7 +65,7 @@ export class InversifyExpressSubscriptionInvitesController extends BaseHttpContr
return this.json(result.data, result.status)
}
@httpDelete('/:inviteUuid', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpDelete('/:inviteUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.subscriptionInvitesController.cancelInvite({
...request.body,
@@ -75,7 +76,7 @@ export class InversifyExpressSubscriptionInvitesController extends BaseHttpContr
return this.json(result.data, result.status)
}
@httpGet('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async listInvites(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.subscriptionInvitesController.listInvites({
...request.body,

View File

@@ -22,7 +22,7 @@ export class InversifyExpressSubscriptionSettingsController extends BaseHttpCont
this.controllerContainer.register('auth.users.getSubscriptionSetting', this.getSubscriptionSetting.bind(this))
}
@httpGet('/subscription-settings/:subscriptionSettingName', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/subscription-settings/:subscriptionSettingName', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.doGetSetting.execute({
userUuid: response.locals.user.uuid,

View File

@@ -37,7 +37,7 @@ export class InversifyExpressSubscriptionTokensController extends BaseHttpContro
this.controllerContainer.register('auth.subscription-tokens.create', this.createToken.bind(this))
}
@httpPost('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPost('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async createToken(_request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
return this.json(

View File

@@ -17,7 +17,7 @@ export class InversifyExpressUserRequestsController extends BaseHttpController {
this.controllerContainer.register('auth.users.createRequest', this.submitRequest.bind(this))
}
@httpPost('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPost('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async submitRequest(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.userRequestsController.submitUserRequest({
requestType: request.body.requestType,

View File

@@ -4,7 +4,7 @@ import * as express from 'express'
import { InversifyExpressUsersController } from './InversifyExpressUsersController'
import { results } from 'inversify-express-utils'
import { ControllerContainerInterface } from '@standardnotes/domain-core'
import { ControllerContainerInterface, Username } from '@standardnotes/domain-core'
import { DeleteAccount } from '../../Domain/UseCase/DeleteAccount/DeleteAccount'
import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
@@ -99,7 +99,8 @@ describe('InversifyExpressUsersController', () => {
updateUser.execute = jest.fn().mockReturnValue({ success: true, authResponse: { foo: 'bar' } })
await createController().update(request, response)
const httpResponse = <results.JsonResult>await createController().update(request, response)
const result = await httpResponse.executeAsync()
expect(updateUser.execute).toHaveBeenCalledWith({
apiVersion: '20190520',
@@ -112,7 +113,7 @@ describe('InversifyExpressUsersController', () => {
},
})
expect(response.send).toHaveBeenCalledWith({ foo: 'bar' })
expect(await result.content.readAsStringAsync()).toEqual('{"foo":"bar"}')
})
it('should not update user if session has read only access', async () => {
@@ -310,7 +311,8 @@ describe('InversifyExpressUsersController', () => {
changeCredentials.execute = jest.fn().mockReturnValue({ success: true, authResponse: { foo: 'bar' } })
await createController().changeCredentials(request, response)
const httpResponse = <results.JsonResult>await createController().changeCredentials(request, response)
const result = await httpResponse.executeAsync()
expect(changeCredentials.execute).toHaveBeenCalledWith({
apiVersion: '20190520',
@@ -321,15 +323,12 @@ describe('InversifyExpressUsersController', () => {
kpOrigination: 'change-password',
pwNonce: 'asdzxc',
protocolVersion: '004',
user: {
uuid: '123',
email: 'test@test.te',
},
username: Username.create('test@test.te').getValue(),
})
expect(clearLoginAttempts.execute).toHaveBeenCalled()
expect(response.send).toHaveBeenCalledWith({ foo: 'bar' })
expect(await result.content.readAsStringAsync()).toEqual('{"foo":"bar"}')
})
it('should not change a password if session has read only access', async () => {

View File

@@ -19,7 +19,7 @@ import { GetUserSubscription } from '../../Domain/UseCase/GetUserSubscription/Ge
import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
import { ControllerContainerInterface } from '@standardnotes/domain-core'
import { ControllerContainerInterface, Username } from '@standardnotes/domain-core'
@controller('/users')
export class InversifyExpressUsersController extends BaseHttpController {
@@ -41,8 +41,8 @@ export class InversifyExpressUsersController extends BaseHttpController {
this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.bind(this))
}
@httpPatch('/:userId', TYPES.Auth_ApiGatewayAuthMiddleware)
async update(request: Request, response: Response): Promise<results.JsonResult | void> {
@httpPatch('/:userId', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async update(request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
return this.json(
{
@@ -83,9 +83,8 @@ export class InversifyExpressUsersController extends BaseHttpController {
if (updateResult.success) {
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
response.send(updateResult.authResponse)
return
return this.json(updateResult.authResponse)
}
return this.json(
@@ -132,7 +131,7 @@ export class InversifyExpressUsersController extends BaseHttpController {
return this.json({ message: result.message }, result.responseCode)
}
@httpGet('/:userUuid/subscription', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/:userUuid/subscription', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async getSubscription(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
return this.json(
@@ -156,8 +155,8 @@ export class InversifyExpressUsersController extends BaseHttpController {
return this.json(result, 400)
}
@httpPut('/:userId/attributes/credentials', TYPES.Auth_AuthMiddleware)
async changeCredentials(request: Request, response: Response): Promise<results.JsonResult | void> {
@httpPut('/:userId/attributes/credentials', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async changeCredentials(request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
return this.json(
{
@@ -203,9 +202,21 @@ export class InversifyExpressUsersController extends BaseHttpController {
400,
)
}
const usernameOrError = Username.create(response.locals.user.email)
if (usernameOrError.isFailed()) {
return this.json(
{
error: {
message: 'Invalid username.',
},
},
400,
)
}
const username = usernameOrError.getValue()
const changeCredentialsResult = await this.changeCredentialsUseCase.execute({
user: response.locals.user,
username,
apiVersion: request.body.api,
currentPassword: request.body.current_password,
newPassword: request.body.new_password,
@@ -233,6 +244,7 @@ export class InversifyExpressUsersController extends BaseHttpController {
await this.clearLoginAttempts.execute({ email: response.locals.user.email })
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
response.send(changeCredentialsResult.authResponse)
return this.json(changeCredentialsResult.authResponse)
}
}

View File

@@ -13,7 +13,7 @@ import { ControllerContainerInterface, Uuid } from '@standardnotes/domain-core'
import TYPES from '../../Bootstrap/Types'
import { CreateValetToken } from '../../Domain/UseCase/CreateValetToken/CreateValetToken'
@controller('/valet-tokens', TYPES.Auth_ApiGatewayAuthMiddleware)
@controller('/valet-tokens', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
export class InversifyExpressValetTokenController extends BaseHttpController {
constructor(
@inject(TYPES.Auth_CreateValetToken) private createValetKey: CreateValetToken,

View File

@@ -1,99 +0,0 @@
import 'reflect-metadata'
import { ApiGatewayAuthMiddleware } from './ApiGatewayAuthMiddleware'
import { NextFunction, Request, Response } from 'express'
import { Logger } from 'winston'
import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/security'
import { RoleName } from '@standardnotes/domain-core'
describe('ApiGatewayAuthMiddleware', () => {
let tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>
let request: Request
let response: Response
let next: NextFunction
const logger = {
debug: jest.fn(),
} as unknown as jest.Mocked<Logger>
const createMiddleware = () => new ApiGatewayAuthMiddleware(tokenDecoder, logger)
beforeEach(() => {
tokenDecoder = {} as jest.Mocked<TokenDecoderInterface<CrossServiceTokenData>>
tokenDecoder.decodeToken = jest.fn().mockReturnValue({
user: {
uuid: '1-2-3',
email: 'test@test.te',
},
roles: [
{
uuid: 'a-b-c',
name: RoleName.NAMES.CoreUser,
},
],
})
request = {
headers: {},
} as jest.Mocked<Request>
response = {
locals: {},
} as jest.Mocked<Response>
response.status = jest.fn().mockReturnThis()
response.send = jest.fn()
next = jest.fn()
})
it('should authorize user', async () => {
request.headers['x-auth-token'] = 'auth-jwt-token'
await createMiddleware().handler(request, response, next)
expect(response.locals.user).toEqual({
uuid: '1-2-3',
email: 'test@test.te',
})
expect(response.locals.roles).toEqual([
{
uuid: 'a-b-c',
name: RoleName.NAMES.CoreUser,
},
])
expect(next).toHaveBeenCalled()
})
it('should not authorize if request is missing auth jwt token in headers', async () => {
await createMiddleware().handler(request, response, next)
expect(response.status).toHaveBeenCalledWith(401)
expect(next).not.toHaveBeenCalled()
})
it('should not authorize if auth jwt token is malformed', async () => {
request.headers['x-auth-token'] = 'auth-jwt-token'
tokenDecoder.decodeToken = jest.fn().mockReturnValue(undefined)
await createMiddleware().handler(request, response, next)
expect(response.status).toHaveBeenCalledWith(401)
expect(next).not.toHaveBeenCalled()
})
it('should pass the error to next middleware if one occurres', async () => {
request.headers['x-auth-token'] = 'auth-jwt-token'
const error = new Error('Ooops')
tokenDecoder.decodeToken = jest.fn().mockImplementation(() => {
throw error
})
await createMiddleware().handler(request, response, next)
expect(response.status).not.toHaveBeenCalled()
expect(next).toHaveBeenCalledWith(error)
})
})

View File

@@ -1,31 +1,16 @@
import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/security'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
@injectable()
export class ApiGatewayAuthMiddleware extends BaseMiddleware {
constructor(
@inject(TYPES.Auth_CrossServiceTokenDecoder) private tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>,
@inject(TYPES.Auth_Logger) private logger: Logger,
) {
export abstract class ApiGatewayAuthMiddleware extends BaseMiddleware {
constructor(private tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>, private logger: Logger) {
super()
}
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
try {
if (!request.headers['x-auth-token']) {
this.logger.debug('ApiGatewayAuthMiddleware missing x-auth-token header.')
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid login credentials.',
},
})
if (!this.handleMissingToken(request, response, next)) {
return
}
@@ -56,4 +41,6 @@ export class ApiGatewayAuthMiddleware extends BaseMiddleware {
return next(error)
}
}
protected abstract handleMissingToken(request: Request, response: Response, next: NextFunction): boolean
}

View File

@@ -1,79 +0,0 @@
import 'reflect-metadata'
import { AuthMiddleware } from './AuthMiddleware'
import { NextFunction, Request, Response } from 'express'
import { User } from '../../../Domain/User/User'
import { AuthenticateRequest } from '../../../Domain/UseCase/AuthenticateRequest'
import { Session } from '../../../Domain/Session/Session'
import { Logger } from 'winston'
describe('AuthMiddleware', () => {
let authenticateRequest: AuthenticateRequest
let request: Request
let response: Response
let next: NextFunction
const logger = {
debug: jest.fn(),
} as unknown as jest.Mocked<Logger>
const createMiddleware = () => new AuthMiddleware(authenticateRequest, logger)
beforeEach(() => {
authenticateRequest = {} as jest.Mocked<AuthenticateRequest>
authenticateRequest.execute = jest.fn()
request = {
headers: {},
} as jest.Mocked<Request>
response = {
locals: {},
} as jest.Mocked<Response>
response.status = jest.fn().mockReturnThis()
response.send = jest.fn()
next = jest.fn()
})
it('should authorize user', async () => {
const user = {} as jest.Mocked<User>
const session = {} as jest.Mocked<Session>
authenticateRequest.execute = jest.fn().mockReturnValue({
success: true,
user,
session,
})
await createMiddleware().handler(request, response, next)
expect(response.locals.user).toEqual(user)
expect(response.locals.session).toEqual(session)
expect(next).toHaveBeenCalled()
})
it('should not authorize if request authentication fails', async () => {
authenticateRequest.execute = jest.fn().mockReturnValue({
success: false,
responseCode: 401,
})
await createMiddleware().handler(request, response, next)
expect(response.status).toHaveBeenCalledWith(401)
expect(next).not.toHaveBeenCalled()
})
it('should pass the error to next middleware if one occurres', async () => {
const error = new Error('Ooops')
authenticateRequest.execute = jest.fn().mockImplementation(() => {
throw error
})
await createMiddleware().handler(request, response, next)
expect(response.status).not.toHaveBeenCalled()
expect(next).toHaveBeenCalledWith(error)
})
})

View File

@@ -1,45 +0,0 @@
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { AuthenticateRequest } from '../../../Domain/UseCase/AuthenticateRequest'
@injectable()
export class AuthMiddleware extends BaseMiddleware {
constructor(
@inject(TYPES.Auth_AuthenticateRequest) private authenticateRequest: AuthenticateRequest,
@inject(TYPES.Auth_Logger) private logger: Logger,
) {
super()
}
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
try {
const authenticateRequestResponse = await this.authenticateRequest.execute({
authorizationHeader: request.headers.authorization,
})
if (!authenticateRequestResponse.success) {
this.logger.debug('AuthMiddleware authentication failure.')
response.status(authenticateRequestResponse.responseCode).send({
error: {
tag: authenticateRequestResponse.errorTag,
message: authenticateRequestResponse.errorMessage,
},
})
return
}
response.locals.user = authenticateRequestResponse.user
response.locals.session = authenticateRequestResponse.session
response.locals.readOnlyAccess = authenticateRequestResponse.session?.readonlyAccess ?? false
return next()
} catch (error) {
return next(error)
}
}
}

View File

@@ -1,68 +0,0 @@
import 'reflect-metadata'
import { AuthMiddlewareWithoutResponse } from './AuthMiddlewareWithoutResponse'
import { NextFunction, Request, Response } from 'express'
import { User } from '../../../Domain/User/User'
import { AuthenticateRequest } from '../../../Domain/UseCase/AuthenticateRequest'
import { Session } from '../../../Domain/Session/Session'
describe('AuthMiddlewareWithoutResponse', () => {
let authenticateRequest: AuthenticateRequest
let request: Request
let response: Response
let next: NextFunction
const createMiddleware = () => new AuthMiddlewareWithoutResponse(authenticateRequest)
beforeEach(() => {
authenticateRequest = {} as jest.Mocked<AuthenticateRequest>
authenticateRequest.execute = jest.fn()
request = {
headers: {},
} as jest.Mocked<Request>
response = {
locals: {},
} as jest.Mocked<Response>
response.status = jest.fn().mockReturnThis()
response.send = jest.fn()
next = jest.fn()
})
it('should authorize user', async () => {
const user = {} as jest.Mocked<User>
const session = {} as jest.Mocked<Session>
authenticateRequest.execute = jest.fn().mockReturnValue({
success: true,
user,
session,
})
await createMiddleware().handler(request, response, next)
expect(response.locals.user).toEqual(user)
expect(response.locals.session).toEqual(session)
expect(next).toHaveBeenCalled()
})
it('should skip middleware if authentication fails', async () => {
authenticateRequest.execute = jest.fn().mockReturnValue({
success: false,
})
await createMiddleware().handler(request, response, next)
expect(next).toHaveBeenCalled()
})
it('should skip middleware if authentication errors', async () => {
authenticateRequest.execute = jest.fn().mockImplementation(() => {
throw new Error('Ooops')
})
await createMiddleware().handler(request, response, next)
expect(next).toHaveBeenCalled()
})
})

View File

@@ -1,32 +0,0 @@
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils'
import TYPES from '../../../Bootstrap/Types'
import { AuthenticateRequest } from '../../../Domain/UseCase/AuthenticateRequest'
@injectable()
export class AuthMiddlewareWithoutResponse extends BaseMiddleware {
constructor(@inject(TYPES.Auth_AuthenticateRequest) private authenticateRequest: AuthenticateRequest) {
super()
}
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
try {
const authenticateRequestResponse = await this.authenticateRequest.execute({
authorizationHeader: request.headers.authorization,
})
if (!authenticateRequestResponse.success) {
return next()
}
response.locals.user = authenticateRequestResponse.user
response.locals.session = authenticateRequestResponse.session
response.locals.readOnlyAccess = authenticateRequestResponse.session?.readonlyAccess ?? false
return next()
} catch (error) {
return next()
}
}
}

View File

@@ -0,0 +1,27 @@
import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/security'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { ApiGatewayAuthMiddleware } from './ApiGatewayAuthMiddleware'
@injectable()
export class OptionalCrossServiceTokenMiddleware extends ApiGatewayAuthMiddleware {
constructor(
@inject(TYPES.Auth_CrossServiceTokenDecoder) tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>,
@inject(TYPES.Auth_Logger) logger: Logger,
) {
super(tokenDecoder, logger)
}
protected override handleMissingToken(request: Request, _response: Response, next: NextFunction): boolean {
if (!request.headers['x-auth-token']) {
next()
return false
}
return true
}
}

View File

@@ -0,0 +1,32 @@
import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/security'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { ApiGatewayAuthMiddleware } from './ApiGatewayAuthMiddleware'
@injectable()
export class RequiredCrossServiceTokenMiddleware extends ApiGatewayAuthMiddleware {
constructor(
@inject(TYPES.Auth_CrossServiceTokenDecoder) tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>,
@inject(TYPES.Auth_Logger) logger: Logger,
) {
super(tokenDecoder, logger)
}
protected override handleMissingToken(request: Request, response: Response, _next: NextFunction): boolean {
if (!request.headers['x-auth-token']) {
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid login credentials.',
},
})
return false
}
return true
}
}

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.5.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.4.4...@standardnotes/home-server@1.5.0) (2023-05-25)
### Features
* add revisions service to home server ([#613](https://github.com/standardnotes/server/issues/613)) ([c70040f](https://github.com/standardnotes/server/commit/c70040fe5dfd35663b9811fbbaa9370bd0298482))
## [1.4.4](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.4.3...@standardnotes/home-server@1.4.4) (2023-05-25)
**Note:** Version bump only for package @standardnotes/home-server
## [1.4.3](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.4.2...@standardnotes/home-server@1.4.3) (2023-05-18)
**Note:** Version bump only for package @standardnotes/home-server
## [1.4.2](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.4.1...@standardnotes/home-server@1.4.2) (2023-05-18)
### Bug Fixes

View File

@@ -5,6 +5,7 @@ import { Service as ApiGatewayService, TYPES as ApiGatewayTYPES } from '@standar
import { DirectCallDomainEventPublisher } from '@standardnotes/domain-events-infra'
import { Service as AuthService } from '@standardnotes/auth-server'
import { Service as SyncingService } from '@standardnotes/syncing-server'
import { Service as RevisionsService } from '@standardnotes/revisions-server'
import { Container } from 'inversify'
import { InversifyExpressServer } from 'inversify-express-utils'
import helmet from 'helmet'
@@ -24,11 +25,13 @@ const startServer = async (): Promise<void> => {
const apiGatewayService = new ApiGatewayService(serviceContainer)
const authService = new AuthService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
const syncingService = new SyncingService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
const revisionsService = new RevisionsService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
const container = Container.merge(
(await apiGatewayService.getContainer()) as Container,
(await authService.getContainer()) as Container,
(await syncingService.getContainer()) as Container,
(await revisionsService.getContainer()) as Container,
)
const env: Env = new Env()

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.4.2",
"version": "1.5.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -22,6 +22,7 @@
"@standardnotes/auth-server": "workspace:^",
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events-infra": "workspace:^",
"@standardnotes/revisions-server": "workspace:^",
"@standardnotes/syncing-server": "workspace:^",
"cors": "2.8.5",
"dotenv": "^16.0.1",

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.16.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.15.1...@standardnotes/revisions-server@1.16.0) (2023-05-25)
### Features
* add revisions service to home server ([#613](https://github.com/standardnotes/server/issues/613)) ([c70040f](https://github.com/standardnotes/server/commit/c70040fe5dfd35663b9811fbbaa9370bd0298482))
## [1.15.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.15.0...@standardnotes/revisions-server@1.15.1) (2023-05-18)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

@@ -9,20 +9,20 @@ import * as winston from 'winston'
import { InversifyExpressServer } from 'inversify-express-utils'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { ServerContainerConfigLoader } from '../src/Bootstrap/ServerContainerConfigLoader'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import '../src/Infra/InversifyExpress/InversifyExpressRevisionsController'
import '../src/Infra/InversifyExpress/InversifyExpressHealthCheckController'
const container = new ServerContainerConfigLoader()
const container = new ContainerConfigLoader()
void container.load().then((container) => {
const env: Env = container.get(TYPES.Env)
const env: Env = container.get(TYPES.Revisions_Env)
const server = new InversifyExpressServer(container)
server.setConfig((app) => {
app.use((_request: Request, response: Response, next: NextFunction) => {
response.setHeader('X-Revisions-Version', container.get(TYPES.VERSION))
response.setHeader('X-Revisions-Version', container.get(TYPES.Revisions_VERSION))
next()
})
app.use(json())
@@ -30,7 +30,7 @@ void container.load().then((container) => {
app.use(cors())
})
const logger: winston.Logger = container.get(TYPES.Logger)
const logger: winston.Logger = container.get(TYPES.Revisions_Logger)
server.setErrorConfig((app) => {
app.use((error: Record<string, unknown>, _request: Request, response: Response, _next: NextFunction) => {

View File

@@ -7,17 +7,19 @@ import { Logger } from 'winston'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { DomainEventSubscriberFactoryInterface } from '@standardnotes/domain-events'
import { WorkerContainerConfigLoader } from '../src/Bootstrap/WorkerContainerConfigLoader'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
const container = new WorkerContainerConfigLoader()
const container = new ContainerConfigLoader()
void container.load().then((container) => {
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Logger)
const logger: Logger = container.get(TYPES.Revisions_Logger)
logger.info('Starting worker...')
const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(TYPES.DomainEventSubscriberFactory)
const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(
TYPES.Revisions_DomainEventSubscriberFactory,
)
subscriberFactory.create().start()
})

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.15.1",
"version": "1.16.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -1,82 +0,0 @@
import { MapperInterface } from '@standardnotes/domain-core'
import { Container, interfaces } from 'inversify'
import { Repository } from 'typeorm'
import * as winston from 'winston'
import { Revision } from '../Domain/Revision/Revision'
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
import { RevisionRepositoryInterface } from '../Domain/Revision/RevisionRepositoryInterface'
import { TypeORMRevisionRepository } from '../Infra/TypeORM/TypeORMRevisionRepository'
import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision'
import { RevisionMetadataPersistenceMapper } from '../Mapping/RevisionMetadataPersistenceMapper'
import { RevisionPersistenceMapper } from '../Mapping/RevisionPersistenceMapper'
import { AppDataSource } from './DataSource'
import { Env } from './Env'
import TYPES from './Types'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
export class CommonContainerConfigLoader {
async load(): Promise<Container> {
const env: Env = new Env()
env.load()
const container = new Container({
defaultScope: 'Singleton',
})
await AppDataSource.initialize()
container.bind<Env>(TYPES.Env).toConstantValue(env)
container.bind<winston.Logger>(TYPES.Logger).toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Env)
const newrelicWinstonFormatter = newrelicFormatter(winston)
const winstonFormatters = [winston.format.splat(), winston.format.json()]
if (env.get('NEW_RELIC_ENABLED', true) === 'true') {
winstonFormatters.push(newrelicWinstonFormatter())
}
const logger = winston.createLogger({
level: env.get('LOG_LEVEL') || 'info',
format: winston.format.combine(...winstonFormatters),
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
defaultMeta: { service: 'revisions' },
})
return logger
})
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
// Map
container
.bind<MapperInterface<RevisionMetadata, TypeORMRevision>>(TYPES.RevisionMetadataPersistenceMapper)
.toDynamicValue(() => new RevisionMetadataPersistenceMapper())
container
.bind<MapperInterface<Revision, TypeORMRevision>>(TYPES.RevisionPersistenceMapper)
.toDynamicValue(() => new RevisionPersistenceMapper())
// ORM
container
.bind<Repository<TypeORMRevision>>(TYPES.ORMRevisionRepository)
.toDynamicValue(() => AppDataSource.getRepository(TypeORMRevision))
// Repositories
container
.bind<RevisionRepositoryInterface>(TYPES.RevisionRepository)
.toDynamicValue((context: interfaces.Context) => {
return new TypeORMRevisionRepository(
context.container.get(TYPES.ORMRevisionRepository),
context.container.get(TYPES.RevisionMetadataPersistenceMapper),
context.container.get(TYPES.RevisionPersistenceMapper),
context.container.get(TYPES.Logger),
)
})
return container
}
}

View File

@@ -0,0 +1,351 @@
import { ControllerContainer, ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core'
import { Container, interfaces } from 'inversify'
import { Repository } from 'typeorm'
import * as winston from 'winston'
import { Revision } from '../Domain/Revision/Revision'
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
import { RevisionRepositoryInterface } from '../Domain/Revision/RevisionRepositoryInterface'
import { TypeORMRevisionRepository } from '../Infra/TypeORM/TypeORMRevisionRepository'
import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision'
import { RevisionMetadataPersistenceMapper } from '../Mapping/RevisionMetadataPersistenceMapper'
import { RevisionPersistenceMapper } from '../Mapping/RevisionPersistenceMapper'
import { AppDataSource } from './DataSource'
import { Env } from './Env'
import TYPES from './Types'
import { TokenDecoderInterface, CrossServiceTokenData, TokenDecoder } from '@standardnotes/security'
import { TimerInterface, Timer } from '@standardnotes/time'
import { ApiGatewayAuthMiddleware } from '../Infra/InversifyExpress/Middleware/ApiGatewayAuthMiddleware'
import { RevisionsController } from '../Controller/RevisionsController'
import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision'
import { GetRequiredRoleToViewRevision } from '../Domain/UseCase/GetRequiredRoleToViewRevision/GetRequiredRoleToViewRevision'
import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision'
import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
import { RevisionHttpMapper } from '../Mapping/RevisionHttpMapper'
import { RevisionMetadataHttpMapper } from '../Mapping/RevisionMetadataHttpMapper'
import { S3Client } from '@aws-sdk/client-s3'
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'
import {
DomainEventMessageHandlerInterface,
DomainEventHandlerInterface,
DomainEventSubscriberFactoryInterface,
} from '@standardnotes/domain-events'
import {
SQSNewRelicEventMessageHandler,
SQSEventMessageHandler,
SQSDomainEventSubscriberFactory,
DirectCallEventMessageHandler,
DirectCallDomainEventPublisher,
} from '@standardnotes/domain-events-infra'
import { DumpRepositoryInterface } from '../Domain/Dump/DumpRepositoryInterface'
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
import { ItemDumpedEventHandler } from '../Domain/Handler/ItemDumpedEventHandler'
import { RevisionsCopyRequestedEventHandler } from '../Domain/Handler/RevisionsCopyRequestedEventHandler'
import { CopyRevisions } from '../Domain/UseCase/CopyRevisions/CopyRevisions'
import { FSDumpRepository } from '../Infra/FS/FSDumpRepository'
import { S3DumpRepository } from '../Infra/S3/S3ItemDumpRepository'
import { RevisionItemStringMapper } from '../Mapping/RevisionItemStringMapper'
import { InversifyExpressRevisionsController } from '../Infra/InversifyExpress/InversifyExpressRevisionsController'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
export class ContainerConfigLoader {
async load(configuration?: {
controllerConatiner?: ControllerContainerInterface
directCallDomainEventPublisher?: DirectCallDomainEventPublisher
}): Promise<Container> {
const directCallDomainEventPublisher =
configuration?.directCallDomainEventPublisher ?? new DirectCallDomainEventPublisher()
const env: Env = new Env()
env.load()
const isConfiguredForHomeServer = env.get('DB_TYPE') === 'sqlite'
const container = new Container({
defaultScope: 'Singleton',
})
await AppDataSource.initialize()
container.bind<Env>(TYPES.Revisions_Env).toConstantValue(env)
container.bind<winston.Logger>(TYPES.Revisions_Logger).toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Revisions_Env)
const newrelicWinstonFormatter = newrelicFormatter(winston)
const winstonFormatters = [winston.format.splat(), winston.format.json()]
if (env.get('NEW_RELIC_ENABLED', true) === 'true') {
winstonFormatters.push(newrelicWinstonFormatter())
}
const logger = winston.createLogger({
level: env.get('LOG_LEVEL') || 'info',
format: winston.format.combine(...winstonFormatters),
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
defaultMeta: { service: 'revisions' },
})
return logger
})
container.bind(TYPES.Revisions_NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
container.bind(TYPES.Revisions_VERSION).toConstantValue(env.get('VERSION'))
// Map
container
.bind<MapperInterface<RevisionMetadata, TypeORMRevision>>(TYPES.Revisions_RevisionMetadataPersistenceMapper)
.toDynamicValue(() => new RevisionMetadataPersistenceMapper())
container
.bind<MapperInterface<Revision, TypeORMRevision>>(TYPES.Revisions_RevisionPersistenceMapper)
.toDynamicValue(() => new RevisionPersistenceMapper())
// ORM
container
.bind<Repository<TypeORMRevision>>(TYPES.Revisions_ORMRevisionRepository)
.toDynamicValue(() => AppDataSource.getRepository(TypeORMRevision))
// Repositories
container
.bind<RevisionRepositoryInterface>(TYPES.Revisions_RevisionRepository)
.toDynamicValue((context: interfaces.Context) => {
return new TypeORMRevisionRepository(
context.container.get(TYPES.Revisions_ORMRevisionRepository),
context.container.get(TYPES.Revisions_RevisionMetadataPersistenceMapper),
context.container.get(TYPES.Revisions_RevisionPersistenceMapper),
context.container.get(TYPES.Revisions_Logger),
)
})
container.bind<TimerInterface>(TYPES.Revisions_Timer).toDynamicValue(() => new Timer())
container
.bind<GetRequiredRoleToViewRevision>(TYPES.Revisions_GetRequiredRoleToViewRevision)
.toDynamicValue((context: interfaces.Context) => {
return new GetRequiredRoleToViewRevision(context.container.get(TYPES.Revisions_Timer))
})
// Map
container
.bind<
MapperInterface<
Revision,
{
uuid: string
item_uuid: string
content: string | null
content_type: string
items_key_id: string | null
enc_item_key: string | null
auth_hash: string | null
created_at: string
updated_at: string
}
>
>(TYPES.Revisions_RevisionHttpMapper)
.toDynamicValue(() => new RevisionHttpMapper())
container
.bind<
MapperInterface<
RevisionMetadata,
{
uuid: string
content_type: string
created_at: string
updated_at: string
}
>
>(TYPES.Revisions_RevisionMetadataHttpMapper)
.toDynamicValue((context: interfaces.Context) => {
return new RevisionMetadataHttpMapper(context.container.get(TYPES.Revisions_GetRequiredRoleToViewRevision))
})
// use cases
container
.bind<GetRevisionsMetada>(TYPES.Revisions_GetRevisionsMetada)
.toDynamicValue((context: interfaces.Context) => {
return new GetRevisionsMetada(context.container.get(TYPES.Revisions_RevisionRepository))
})
container.bind<GetRevision>(TYPES.Revisions_GetRevision).toDynamicValue((context: interfaces.Context) => {
return new GetRevision(context.container.get(TYPES.Revisions_RevisionRepository))
})
container.bind<DeleteRevision>(TYPES.Revisions_DeleteRevision).toDynamicValue((context: interfaces.Context) => {
return new DeleteRevision(context.container.get(TYPES.Revisions_RevisionRepository))
})
// env vars
container.bind(TYPES.Revisions_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
// Controller
container
.bind<ControllerContainerInterface>(TYPES.Revisions_ControllerContainer)
.toConstantValue(configuration?.controllerConatiner ?? new ControllerContainer())
container
.bind<RevisionsController>(TYPES.Revisions_RevisionsController)
.toDynamicValue((context: interfaces.Context) => {
return new RevisionsController(
context.container.get(TYPES.Revisions_GetRevisionsMetada),
context.container.get(TYPES.Revisions_GetRevision),
context.container.get(TYPES.Revisions_DeleteRevision),
context.container.get(TYPES.Revisions_RevisionHttpMapper),
context.container.get(TYPES.Revisions_RevisionMetadataHttpMapper),
context.container.get(TYPES.Revisions_Logger),
)
})
container
.bind<TokenDecoderInterface<CrossServiceTokenData>>(TYPES.Revisions_CrossServiceTokenDecoder)
.toDynamicValue((context: interfaces.Context) => {
return new TokenDecoder<CrossServiceTokenData>(context.container.get(TYPES.Revisions_AUTH_JWT_SECRET))
})
container
.bind<ApiGatewayAuthMiddleware>(TYPES.Revisions_ApiGatewayAuthMiddleware)
.toDynamicValue((context: interfaces.Context) => {
return new ApiGatewayAuthMiddleware(
context.container.get(TYPES.Revisions_CrossServiceTokenDecoder),
context.container.get(TYPES.Revisions_Logger),
)
})
// Map
container
.bind<MapperInterface<Revision, string>>(TYPES.Revisions_RevisionItemStringMapper)
.toDynamicValue(() => new RevisionItemStringMapper())
container
.bind<DumpRepositoryInterface>(TYPES.Revisions_DumpRepository)
.toConstantValue(
env.get('S3_AWS_REGION', true)
? new S3DumpRepository(
container.get(TYPES.Revisions_S3_BACKUP_BUCKET_NAME),
container.get(TYPES.Revisions_S3),
container.get(TYPES.Revisions_RevisionItemStringMapper),
container.get(TYPES.Revisions_Logger),
)
: new FSDumpRepository(container.get(TYPES.Revisions_RevisionItemStringMapper)),
)
if (!isConfiguredForHomeServer) {
// env vars
container.bind(TYPES.Revisions_SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL'))
container.bind(TYPES.Revisions_S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true))
container.bind(TYPES.Revisions_S3_BACKUP_BUCKET_NAME).toConstantValue(env.get('S3_BACKUP_BUCKET_NAME', true))
container.bind<SQSClient>(TYPES.Revisions_SQS).toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Revisions_Env)
const sqsConfig: SQSClientConfig = {
region: env.get('SQS_AWS_REGION'),
}
if (env.get('SQS_ENDPOINT', true)) {
sqsConfig.endpoint = env.get('SQS_ENDPOINT', true)
}
if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) {
sqsConfig.credentials = {
accessKeyId: env.get('SQS_ACCESS_KEY_ID', true),
secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true),
}
}
return new SQSClient(sqsConfig)
})
container.bind<S3Client | undefined>(TYPES.Revisions_S3).toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Revisions_Env)
let s3Client = undefined
if (env.get('S3_AWS_REGION', true)) {
s3Client = new S3Client({
apiVersion: 'latest',
region: env.get('S3_AWS_REGION', true),
})
}
return s3Client
})
}
// use cases
container.bind<CopyRevisions>(TYPES.Revisions_CopyRevisions).toDynamicValue((context: interfaces.Context) => {
return new CopyRevisions(context.container.get(TYPES.Revisions_RevisionRepository))
})
// Handlers
container
.bind<ItemDumpedEventHandler>(TYPES.Revisions_ItemDumpedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new ItemDumpedEventHandler(
context.container.get(TYPES.Revisions_DumpRepository),
context.container.get(TYPES.Revisions_RevisionRepository),
)
})
container
.bind<AccountDeletionRequestedEventHandler>(TYPES.Revisions_AccountDeletionRequestedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new AccountDeletionRequestedEventHandler(
context.container.get(TYPES.Revisions_RevisionRepository),
context.container.get(TYPES.Revisions_Logger),
)
})
container
.bind<RevisionsCopyRequestedEventHandler>(TYPES.Revisions_RevisionsCopyRequestedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new RevisionsCopyRequestedEventHandler(
context.container.get(TYPES.Revisions_CopyRevisions),
context.container.get(TYPES.Revisions_Logger),
)
})
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['ITEM_DUMPED', container.get(TYPES.Revisions_ItemDumpedEventHandler)],
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Revisions_AccountDeletionRequestedEventHandler)],
['REVISIONS_COPY_REQUESTED', container.get(TYPES.Revisions_RevisionsCopyRequestedEventHandler)],
])
if (isConfiguredForHomeServer) {
const directCallEventMessageHandler = new DirectCallEventMessageHandler(
eventHandlers,
container.get(TYPES.Revisions_Logger),
)
directCallDomainEventPublisher.register(directCallEventMessageHandler)
container
.bind<DomainEventMessageHandlerInterface>(TYPES.Revisions_DomainEventMessageHandler)
.toConstantValue(directCallEventMessageHandler)
} else {
container
.bind<DomainEventMessageHandlerInterface>(TYPES.Revisions_DomainEventMessageHandler)
.toConstantValue(
env.get('NEW_RELIC_ENABLED', true) === 'true'
? new SQSNewRelicEventMessageHandler(eventHandlers, container.get(TYPES.Revisions_Logger))
: new SQSEventMessageHandler(eventHandlers, container.get(TYPES.Revisions_Logger)),
)
container
.bind<DomainEventSubscriberFactoryInterface>(TYPES.Revisions_DomainEventSubscriberFactory)
.toDynamicValue((context: interfaces.Context) => {
return new SQSDomainEventSubscriberFactory(
context.container.get(TYPES.Revisions_SQS),
context.container.get(TYPES.Revisions_SQS_QUEUE_URL),
context.container.get(TYPES.Revisions_DomainEventMessageHandler),
)
})
}
// Inversify Controllers
if (isConfiguredForHomeServer) {
container
.bind<InversifyExpressRevisionsController>(TYPES.Revisions_InversifyExpressRevisionsController)
.toConstantValue(
new InversifyExpressRevisionsController(
container.get(TYPES.Revisions_RevisionsController),
container.get(TYPES.Revisions_ControllerContainer),
),
)
}
return container
}
}

View File

@@ -15,55 +15,62 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
const inReplicaMode = env.get('DB_REPLICA_HOST', true) ? true : false
const commonDataSourceOptions = {
maxQueryExecutionTime,
entities: [TypeORMRevision],
migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
migrationsRun: true,
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL', true) ?? 'info',
}
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),
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'),
},
],
removeNodeErrorCount: 10,
restoreNodeTimeout: 5,
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)
}
const commonDataSourceOptions = {
maxQueryExecutionTime,
entities: [TypeORMRevision],
migrations: [`dist/migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
migrationsRun: true,
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),
}
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'),
}
const sqliteDataSourceOptions: SqliteConnectionOptions = {
...commonDataSourceOptions,
type: 'sqlite',
database: `data/${env.get('DB_DATABASE')}.sqlite`,
}
export const AppDataSource = new DataSource(isConfiguredForMySQL ? mySQLDataSourceOptions : sqliteDataSourceOptions)
export const AppDataSource = dataSource

View File

@@ -1,112 +0,0 @@
import { Timer, TimerInterface } from '@standardnotes/time'
import { Container, interfaces } from 'inversify'
import { MapperInterface } from '@standardnotes/domain-core'
import TYPES from './Types'
import { RevisionsController } from '../Controller/RevisionsController'
import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
import { Revision } from '../Domain/Revision/Revision'
import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision'
import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision'
import { RevisionHttpMapper } from '../Mapping/RevisionHttpMapper'
import { RevisionMetadataHttpMapper } from '../Mapping/RevisionMetadataHttpMapper'
import { GetRequiredRoleToViewRevision } from '../Domain/UseCase/GetRequiredRoleToViewRevision/GetRequiredRoleToViewRevision'
import { CommonContainerConfigLoader } from './CommonContainerConfigLoader'
import { ApiGatewayAuthMiddleware } from '../Controller/ApiGatewayAuthMiddleware'
import { CrossServiceTokenData, TokenDecoder, TokenDecoderInterface } from '@standardnotes/security'
import { Env } from './Env'
export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
override async load(): Promise<Container> {
const container = await super.load()
const env: Env = container.get(TYPES.Env)
container.bind<TimerInterface>(TYPES.Timer).toDynamicValue(() => new Timer())
container
.bind<GetRequiredRoleToViewRevision>(TYPES.GetRequiredRoleToViewRevision)
.toDynamicValue((context: interfaces.Context) => {
return new GetRequiredRoleToViewRevision(context.container.get(TYPES.Timer))
})
// Map
container
.bind<
MapperInterface<
Revision,
{
uuid: string
item_uuid: string
content: string | null
content_type: string
items_key_id: string | null
enc_item_key: string | null
auth_hash: string | null
created_at: string
updated_at: string
}
>
>(TYPES.RevisionHttpMapper)
.toDynamicValue(() => new RevisionHttpMapper())
container
.bind<
MapperInterface<
RevisionMetadata,
{
uuid: string
content_type: string
created_at: string
updated_at: string
}
>
>(TYPES.RevisionMetadataHttpMapper)
.toDynamicValue((context: interfaces.Context) => {
return new RevisionMetadataHttpMapper(context.container.get(TYPES.GetRequiredRoleToViewRevision))
})
// use cases
container.bind<GetRevisionsMetada>(TYPES.GetRevisionsMetada).toDynamicValue((context: interfaces.Context) => {
return new GetRevisionsMetada(context.container.get(TYPES.RevisionRepository))
})
container.bind<GetRevision>(TYPES.GetRevision).toDynamicValue((context: interfaces.Context) => {
return new GetRevision(context.container.get(TYPES.RevisionRepository))
})
container.bind<DeleteRevision>(TYPES.DeleteRevision).toDynamicValue((context: interfaces.Context) => {
return new DeleteRevision(context.container.get(TYPES.RevisionRepository))
})
// env vars
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
// Controller
container.bind<RevisionsController>(TYPES.RevisionsController).toDynamicValue((context: interfaces.Context) => {
return new RevisionsController(
context.container.get(TYPES.GetRevisionsMetada),
context.container.get(TYPES.GetRevision),
context.container.get(TYPES.DeleteRevision),
context.container.get(TYPES.RevisionHttpMapper),
context.container.get(TYPES.RevisionMetadataHttpMapper),
context.container.get(TYPES.Logger),
)
})
container
.bind<TokenDecoderInterface<CrossServiceTokenData>>(TYPES.CrossServiceTokenDecoder)
.toDynamicValue((context: interfaces.Context) => {
return new TokenDecoder<CrossServiceTokenData>(context.container.get(TYPES.AUTH_JWT_SECRET))
})
container
.bind<ApiGatewayAuthMiddleware>(TYPES.ApiGatewayAuthMiddleware)
.toDynamicValue((context: interfaces.Context) => {
return new ApiGatewayAuthMiddleware(
context.container.get(TYPES.CrossServiceTokenDecoder),
context.container.get(TYPES.Logger),
)
})
return container
}
}

View File

@@ -0,0 +1,42 @@
import {
ControllerContainerInterface,
ServiceContainerInterface,
ServiceIdentifier,
ServiceInterface,
} from '@standardnotes/domain-core'
import { ContainerConfigLoader } from './Container'
import { DirectCallDomainEventPublisher } from '@standardnotes/domain-events-infra'
export class Service implements ServiceInterface {
constructor(
private serviceContainer: ServiceContainerInterface,
private controllerContainer: ControllerContainerInterface,
private directCallDomainEventPublisher: DirectCallDomainEventPublisher,
) {
this.serviceContainer.register(this.getId(), this)
}
async handleRequest(request: never, response: never, endpointOrMethodIdentifier: string): Promise<unknown> {
const method = this.controllerContainer.get(endpointOrMethodIdentifier)
if (!method) {
throw new Error(`Method ${endpointOrMethodIdentifier} not found`)
}
return method(request, response)
}
async getContainer(): Promise<unknown> {
const config = new ContainerConfigLoader()
return config.load({
controllerConatiner: this.controllerContainer,
directCallDomainEventPublisher: this.directCallDomainEventPublisher,
})
}
getId(): ServiceIdentifier {
return ServiceIdentifier.create(ServiceIdentifier.NAMES.Revisions).getValue()
}
}

View File

@@ -1,46 +1,49 @@
const TYPES = {
DBConnection: Symbol.for('DBConnection'),
Logger: Symbol.for('Logger'),
SQS: Symbol.for('SQS'),
S3: Symbol.for('S3'),
Env: Symbol.for('Env'),
Revisions_DBConnection: Symbol.for('Revisions_DBConnection'),
Revisions_Logger: Symbol.for('Revisions_Logger'),
Revisions_SQS: Symbol.for('Revisions_SQS'),
Revisions_S3: Symbol.for('Revisions_S3'),
Revisions_Env: Symbol.for('Revisions_Env'),
// Map
RevisionMetadataPersistenceMapper: Symbol.for('RevisionMetadataPersistenceMapper'),
RevisionPersistenceMapper: Symbol.for('RevisionPersistenceMapper'),
RevisionItemStringMapper: Symbol.for('RevisionItemStringMapper'),
RevisionHttpMapper: Symbol.for('RevisionHttpMapper'),
RevisionMetadataHttpMapper: Symbol.for('RevisionMetadataHttpMapper'),
Revisions_RevisionMetadataPersistenceMapper: Symbol.for('Revisions_RevisionMetadataPersistenceMapper'),
Revisions_RevisionPersistenceMapper: Symbol.for('Revisions_RevisionPersistenceMapper'),
Revisions_RevisionItemStringMapper: Symbol.for('Revisions_RevisionItemStringMapper'),
Revisions_RevisionHttpMapper: Symbol.for('Revisions_RevisionHttpMapper'),
Revisions_RevisionMetadataHttpMapper: Symbol.for('Revisions_RevisionMetadataHttpMapper'),
// ORM
ORMRevisionRepository: Symbol.for('ORMRevisionRepository'),
Revisions_ORMRevisionRepository: Symbol.for('Revisions_ORMRevisionRepository'),
// Repositories
RevisionRepository: Symbol.for('RevisionRepository'),
DumpRepository: Symbol.for('DumpRepository'),
Revisions_RevisionRepository: Symbol.for('Revisions_RevisionRepository'),
Revisions_DumpRepository: Symbol.for('Revisions_DumpRepository'),
// env vars
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
SQS_QUEUE_URL: Symbol.for('SQS_QUEUE_URL'),
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
S3_AWS_REGION: Symbol.for('S3_AWS_REGION'),
S3_BACKUP_BUCKET_NAME: Symbol.for('S3_BACKUP_BUCKET_NAME'),
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
VERSION: Symbol.for('VERSION'),
Revisions_AUTH_JWT_SECRET: Symbol.for('Revisions_AUTH_JWT_SECRET'),
Revisions_SQS_QUEUE_URL: Symbol.for('Revisions_SQS_QUEUE_URL'),
Revisions_SQS_AWS_REGION: Symbol.for('Revisions_SQS_AWS_REGION'),
Revisions_S3_AWS_REGION: Symbol.for('Revisions_S3_AWS_REGION'),
Revisions_S3_BACKUP_BUCKET_NAME: Symbol.for('Revisions_S3_BACKUP_BUCKET_NAME'),
Revisions_NEW_RELIC_ENABLED: Symbol.for('Revisions_NEW_RELIC_ENABLED'),
Revisions_VERSION: Symbol.for('Revisions_VERSION'),
// use cases
GetRevisionsMetada: Symbol.for('GetRevisionsMetada'),
GetRevision: Symbol.for('GetRevision'),
DeleteRevision: Symbol.for('DeleteRevision'),
CopyRevisions: Symbol.for('CopyRevisions'),
GetRequiredRoleToViewRevision: Symbol.for('GetRequiredRoleToViewRevision'),
Revisions_GetRevisionsMetada: Symbol.for('Revisions_GetRevisionsMetada'),
Revisions_GetRevision: Symbol.for('Revisions_GetRevision'),
Revisions_DeleteRevision: Symbol.for('Revisions_DeleteRevision'),
Revisions_CopyRevisions: Symbol.for('Revisions_CopyRevisions'),
Revisions_GetRequiredRoleToViewRevision: Symbol.for('Revisions_GetRequiredRoleToViewRevision'),
// Controller
RevisionsController: Symbol.for('RevisionsController'),
ApiGatewayAuthMiddleware: Symbol.for('ApiGatewayAuthMiddleware'),
Revisions_ControllerContainer: Symbol.for('Revisions_ControllerContainer'),
Revisions_RevisionsController: Symbol.for('Revisions_RevisionsController'),
Revisions_ApiGatewayAuthMiddleware: Symbol.for('Revisions_ApiGatewayAuthMiddleware'),
// Handlers
ItemDumpedEventHandler: Symbol.for('ItemDumpedEventHandler'),
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
RevisionsCopyRequestedEventHandler: Symbol.for('RevisionsCopyRequestedEventHandler'),
Revisions_ItemDumpedEventHandler: Symbol.for('Revisions_ItemDumpedEventHandler'),
Revisions_AccountDeletionRequestedEventHandler: Symbol.for('Revisions_AccountDeletionRequestedEventHandler'),
Revisions_RevisionsCopyRequestedEventHandler: Symbol.for('Revisions_RevisionsCopyRequestedEventHandler'),
// Services
CrossServiceTokenDecoder: Symbol.for('CrossServiceTokenDecoder'),
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
DomainEventMessageHandler: Symbol.for('DomainEventMessageHandler'),
Timer: Symbol.for('Timer'),
Revisions_CrossServiceTokenDecoder: Symbol.for('Revisions_CrossServiceTokenDecoder'),
Revisions_DomainEventSubscriberFactory: Symbol.for('Revisions_DomainEventSubscriberFactory'),
Revisions_DomainEventMessageHandler: Symbol.for('Revisions_DomainEventMessageHandler'),
Revisions_Timer: Symbol.for('Revisions_Timer'),
// Inversify Express Controllers
Revisions_InversifyExpressRevisionsController: Symbol.for('Revisions_InversifyExpressRevisionsController'),
}
export default TYPES

View File

@@ -1,155 +0,0 @@
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'
import { S3Client } from '@aws-sdk/client-s3'
import { Container, interfaces } from 'inversify'
import {
DomainEventHandlerInterface,
DomainEventMessageHandlerInterface,
DomainEventSubscriberFactoryInterface,
} from '@standardnotes/domain-events'
import {
SQSDomainEventSubscriberFactory,
SQSEventMessageHandler,
SQSNewRelicEventMessageHandler,
} from '@standardnotes/domain-events-infra'
import { MapperInterface } from '@standardnotes/domain-core'
import TYPES from './Types'
import { Revision } from '../Domain/Revision/Revision'
import { RevisionItemStringMapper } from '../Mapping/RevisionItemStringMapper'
import { ItemDumpedEventHandler } from '../Domain/Handler/ItemDumpedEventHandler'
import { DumpRepositoryInterface } from '../Domain/Dump/DumpRepositoryInterface'
import { S3DumpRepository } from '../Infra/S3/S3ItemDumpRepository'
import { FSDumpRepository } from '../Infra/FS/FSDumpRepository'
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
import { RevisionsCopyRequestedEventHandler } from '../Domain/Handler/RevisionsCopyRequestedEventHandler'
import { CopyRevisions } from '../Domain/UseCase/CopyRevisions/CopyRevisions'
import { CommonContainerConfigLoader } from './CommonContainerConfigLoader'
import { Env } from './Env'
export class WorkerContainerConfigLoader extends CommonContainerConfigLoader {
override async load(): Promise<Container> {
const container = await super.load()
const env: Env = container.get(TYPES.Env)
container.bind<SQSClient>(TYPES.SQS).toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Env)
const sqsConfig: SQSClientConfig = {
region: env.get('SQS_AWS_REGION'),
}
if (env.get('SQS_ENDPOINT', true)) {
sqsConfig.endpoint = env.get('SQS_ENDPOINT', true)
}
if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) {
sqsConfig.credentials = {
accessKeyId: env.get('SQS_ACCESS_KEY_ID', true),
secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true),
}
}
return new SQSClient(sqsConfig)
})
container.bind<S3Client | undefined>(TYPES.S3).toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Env)
let s3Client = undefined
if (env.get('S3_AWS_REGION', true)) {
s3Client = new S3Client({
apiVersion: 'latest',
region: env.get('S3_AWS_REGION', true),
})
}
return s3Client
})
// Map
container
.bind<MapperInterface<Revision, string>>(TYPES.RevisionItemStringMapper)
.toDynamicValue(() => new RevisionItemStringMapper())
// env vars
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL'))
container.bind(TYPES.S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true))
container.bind(TYPES.S3_BACKUP_BUCKET_NAME).toConstantValue(env.get('S3_BACKUP_BUCKET_NAME', true))
container.bind<DumpRepositoryInterface>(TYPES.DumpRepository).toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Env)
if (env.get('S3_AWS_REGION', true)) {
return new S3DumpRepository(
context.container.get(TYPES.S3_BACKUP_BUCKET_NAME),
context.container.get(TYPES.S3),
context.container.get(TYPES.RevisionItemStringMapper),
context.container.get(TYPES.Logger),
)
} else {
return new FSDumpRepository(context.container.get(TYPES.RevisionItemStringMapper))
}
})
// use cases
container.bind<CopyRevisions>(TYPES.CopyRevisions).toDynamicValue((context: interfaces.Context) => {
return new CopyRevisions(context.container.get(TYPES.RevisionRepository))
})
// Handlers
container
.bind<ItemDumpedEventHandler>(TYPES.ItemDumpedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new ItemDumpedEventHandler(
context.container.get(TYPES.DumpRepository),
context.container.get(TYPES.RevisionRepository),
)
})
container
.bind<AccountDeletionRequestedEventHandler>(TYPES.AccountDeletionRequestedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new AccountDeletionRequestedEventHandler(
context.container.get(TYPES.RevisionRepository),
context.container.get(TYPES.Logger),
)
})
container
.bind<RevisionsCopyRequestedEventHandler>(TYPES.RevisionsCopyRequestedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new RevisionsCopyRequestedEventHandler(
context.container.get(TYPES.CopyRevisions),
context.container.get(TYPES.Logger),
)
})
container
.bind<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler)
.toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Env)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['ITEM_DUMPED', context.container.get(TYPES.ItemDumpedEventHandler)],
['ACCOUNT_DELETION_REQUESTED', context.container.get(TYPES.AccountDeletionRequestedEventHandler)],
['REVISIONS_COPY_REQUESTED', context.container.get(TYPES.RevisionsCopyRequestedEventHandler)],
])
const handler =
env.get('NEW_RELIC_ENABLED', true) === 'true'
? new SQSNewRelicEventMessageHandler(eventHandlers, context.container.get(TYPES.Logger))
: new SQSEventMessageHandler(eventHandlers, context.container.get(TYPES.Logger))
return handler
})
container
.bind<DomainEventSubscriberFactoryInterface>(TYPES.DomainEventSubscriberFactory)
.toDynamicValue((context: interfaces.Context) => {
return new SQSDomainEventSubscriberFactory(
context.container.get(TYPES.SQS),
context.container.get(TYPES.SQS_QUEUE_URL),
context.container.get(TYPES.DomainEventMessageHandler),
)
})
return container
}
}

View File

@@ -0,0 +1 @@
export * from './Service'

View File

@@ -1,14 +1,22 @@
import { Request, Response } from 'express'
import { BaseHttpController, controller, httpDelete, httpGet, results } from 'inversify-express-utils'
import { inject } from 'inversify'
import { ControllerContainerInterface } from '@standardnotes/domain-core'
import TYPES from '../../Bootstrap/Types'
import { RevisionsController } from '../../Controller/RevisionsController'
@controller('/items/:itemUuid/revisions', TYPES.ApiGatewayAuthMiddleware)
@controller('/items/:itemUuid/revisions', TYPES.Revisions_ApiGatewayAuthMiddleware)
export class InversifyExpressRevisionsController extends BaseHttpController {
constructor(@inject(TYPES.RevisionsController) private revisionsController: RevisionsController) {
constructor(
@inject(TYPES.Revisions_RevisionsController) private revisionsController: RevisionsController,
@inject(TYPES.Revisions_ControllerContainer) private controllerContainer: ControllerContainerInterface,
) {
super()
this.controllerContainer.register('revisions.revisions.getRevisions', this.getRevisions.bind(this))
this.controllerContainer.register('revisions.revisions.getRevision', this.getRevision.bind(this))
this.controllerContainer.register('revisions.revisions.deleteRevision', this.deleteRevision.bind(this))
}
@httpGet('/')

View File

@@ -12,7 +12,10 @@ export class RevisionMetadataPersistenceMapper implements MapperInterface<Revisi
}
const contentType = contentTypeOrError.getValue()
const datesOrError = Dates.create(projection.createdAt, projection.updatedAt)
const createdAt = projection.createdAt instanceof Date ? projection.createdAt : new Date(projection.createdAt)
const updatedAt = projection.updatedAt instanceof Date ? projection.updatedAt : new Date(projection.updatedAt)
const datesOrError = Dates.create(createdAt, updatedAt)
if (datesOrError.isFailed()) {
throw new Error(`Could not create dates: ${datesOrError.getError()}`)
}

View File

@@ -0,0 +1 @@
export * from './Bootstrap'

View File

@@ -4249,6 +4249,7 @@ __metadata:
"@standardnotes/auth-server": "workspace:^"
"@standardnotes/domain-core": "workspace:^"
"@standardnotes/domain-events-infra": "workspace:^"
"@standardnotes/revisions-server": "workspace:^"
"@standardnotes/syncing-server": "workspace:^"
"@types/cors": "npm:^2.8.9"
"@types/express": "npm:^4.17.14"
@@ -4313,7 +4314,7 @@ __metadata:
languageName: node
linkType: hard
"@standardnotes/revisions-server@workspace:packages/revisions":
"@standardnotes/revisions-server@workspace:^, @standardnotes/revisions-server@workspace:packages/revisions":
version: 0.0.0-use.local
resolution: "@standardnotes/revisions-server@workspace:packages/revisions"
dependencies: