Compare commits

...

8 Commits

Author SHA1 Message Date
standardci
060206ddd4 chore(release): publish new version
- @standardnotes/analytics@2.24.4
 - @standardnotes/api-gateway@1.65.1
 - @standardnotes/auth-server@1.120.1
 - @standardnotes/domain-events-infra@1.12.7
 - @standardnotes/domain-events@2.112.1
 - @standardnotes/event-store@1.11.1
 - @standardnotes/files-server@1.19.1
 - @standardnotes/home-server@1.11.16
 - @standardnotes/revisions-server@1.23.5
 - @standardnotes/scheduler-server@1.20.3
 - @standardnotes/security@1.8.1
 - @standardnotes/syncing-server@1.44.6
 - @standardnotes/websockets-server@1.9.4
2023-06-30 10:24:00 +00:00
Mo
0bc0909386 chore: types lint (#630) 2023-06-30 05:07:47 -05:00
standardci
667d528a8c chore(release): publish new version
- @standardnotes/analytics@2.24.3
 - @standardnotes/api-gateway@1.65.0
 - @standardnotes/auth-server@1.120.0
 - @standardnotes/common@1.49.0
 - @standardnotes/domain-events-infra@1.12.6
 - @standardnotes/domain-events@2.112.0
 - @standardnotes/event-store@1.11.0
 - @standardnotes/files-server@1.19.0
 - @standardnotes/home-server@1.11.15
 - @standardnotes/revisions-server@1.23.4
 - @standardnotes/scheduler-server@1.20.2
 - @standardnotes/security@1.8.0
 - @standardnotes/syncing-server@1.44.5
 - @standardnotes/websockets-server@1.9.3
2023-06-30 09:47:02 +00:00
Karol Sójko
fa7fbe26e7 feat: shared vaults functionality in api-gateway,auth,files,common,security,domain-events. (#629)
Co-authored-by: Mo <mo@standardnotes.com>
2023-06-30 11:31:25 +02:00
standardci
ba422a29d0 chore(release): publish new version
- @standardnotes/auth-server@1.119.6
 - @standardnotes/home-server@1.11.14
2023-06-28 16:11:23 +00:00
Karol Sójko
d220ec5bf7 fix(auth): add debug logs for authentication method resolver 2023-06-28 17:56:59 +02:00
standardci
7baf5492bc chore(release): publish new version
- @standardnotes/api-gateway@1.64.3
 - @standardnotes/auth-server@1.119.5
 - @standardnotes/home-server@1.11.13
 - @standardnotes/syncing-server@1.44.4
2023-06-28 12:45:50 +00:00
Karol Sójko
d5a8409bb5 fix: add debug logs for invalid-auth responses 2023-06-28 14:30:39 +02:00
113 changed files with 1145 additions and 237 deletions

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.24.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.24.3...@standardnotes/analytics@2.24.4) (2023-06-30)
**Note:** Version bump only for package @standardnotes/analytics
## [2.24.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.24.2...@standardnotes/analytics@2.24.3) (2023-06-30)
**Note:** Version bump only for package @standardnotes/analytics
## [2.24.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.24.1...@standardnotes/analytics@2.24.2) (2023-06-28)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.24.2",
"version": "2.24.4",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.65.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.65.0...@standardnotes/api-gateway@1.65.1) (2023-06-30)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.65.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.64.3...@standardnotes/api-gateway@1.65.0) (2023-06-30)
### Features
* shared vaults functionality in api-gateway,auth,files,common,security,domain-events. ([#629](https://github.com/standardnotes/api-gateway/issues/629)) ([fa7fbe2](https://github.com/standardnotes/api-gateway/commit/fa7fbe26e7b0707fc21d71e04af76870f5248baf))
## [1.64.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.64.2...@standardnotes/api-gateway@1.64.3) (2023-06-28)
### Bug Fixes
* add debug logs for invalid-auth responses ([d5a8409](https://github.com/standardnotes/api-gateway/commit/d5a8409bb5d35b9caf410a36ea0d5cb747129e8d))
## [1.64.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.64.1...@standardnotes/api-gateway@1.64.2) (2023-06-22)
### Bug Fixes

View File

@@ -16,6 +16,8 @@ import '../src/Controller/v1/OfflineController'
import '../src/Controller/v1/FilesController'
import '../src/Controller/v1/SubscriptionInvitesController'
import '../src/Controller/v1/AuthenticatorsController'
import '../src/Controller/v1/AsymmetricMessagesController'
import '../src/Controller/v1/SharedVaultsController'
import '../src/Controller/v2/PaymentsControllerV2'
import '../src/Controller/v2/ActionsControllerV2'
@@ -45,28 +47,29 @@ void container.load().then((container) => {
response.setHeader('X-API-Gateway-Version', container.get(TYPES.VERSION))
next()
})
/* eslint-disable */
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["https: 'self'"],
baseUri: ["'self'"],
childSrc: ["*", "blob:"],
connectSrc: ["*"],
fontSrc: ["*", "'self'"],
formAction: ["'self'"],
frameAncestors: ["*", "*.standardnotes.org", "*.standardnotes.com"],
frameSrc: ["*", "blob:"],
imgSrc: ["'self'", "*", "data:"],
manifestSrc: ["'self'"],
mediaSrc: ["'self'"],
objectSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"]
}
}
}))
/* eslint-enable */
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["https: 'self'"],
baseUri: ["'self'"],
childSrc: ['*', 'blob:'],
connectSrc: ['*'],
fontSrc: ['*', "'self'"],
formAction: ["'self'"],
frameAncestors: ['*', '*.standardnotes.org', '*.standardnotes.com'],
frameSrc: ['*', 'blob:'],
imgSrc: ["'self'", '*', 'data:'],
manifestSrc: ["'self'"],
mediaSrc: ["'self'"],
objectSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
},
},
}),
)
app.use(json({ limit: '50mb' }))
app.use(
text({

View File

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

View File

@@ -17,7 +17,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
private crossServiceTokenCacheTTL: number,
private crossServiceTokenCache: CrossServiceTokenCacheInterface,
private timer: TimerInterface,
private logger: Logger,
protected logger: Logger,
) {
super()
}

View File

@@ -42,6 +42,8 @@ export class RequiredCrossServiceTokenMiddleware extends AuthMiddleware {
_next: NextFunction,
): boolean {
if (!authHeaderValue) {
this.logger.debug('Missing auth header')
response.status(401).send({
error: {
tag: 'invalid-auth',

View File

@@ -4,6 +4,7 @@ export * from './SubscriptionTokenAuthMiddleware'
export * from './TokenAuthenticationMethod'
export * from './WebSocketAuthMiddleware'
export * from './v1/ActionsController'
export * from './v1/AsymmetricMessagesController'
export * from './v1/AuthenticatorsController'
export * from './v1/FilesController'
export * from './v1/InvoicesController'
@@ -12,6 +13,7 @@ export * from './v1/OfflineController'
export * from './v1/PaymentsController'
export * from './v1/RevisionsController'
export * from './v1/SessionsController'
export * from './v1/SharedVaultsController'
export * from './v1/SubscriptionInvitesController'
export * from './v1/TokensController'
export * from './v1/UsersController'

View File

@@ -0,0 +1,17 @@
import { Request, Response } from 'express'
import { inject } from 'inversify'
import { BaseHttpController, controller, all } from 'inversify-express-utils'
import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
@controller('/v1/asymmetric-messages')
export class AsymmetricMessagesController extends BaseHttpController {
constructor(@inject(TYPES.ServiceProxy) private serviceProxy: ServiceProxyInterface) {
super()
}
@all('*', TYPES.RequiredCrossServiceTokenMiddleware)
async subscriptions(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callSyncingServer(request, response, request.path.replace('/v1/', ''), request.body)
}
}

View File

@@ -0,0 +1,17 @@
import { Request, Response } from 'express'
import { inject } from 'inversify'
import { BaseHttpController, controller, all } from 'inversify-express-utils'
import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
@controller('/v1/shared-vaults')
export class SharedVaultsController extends BaseHttpController {
constructor(@inject(TYPES.ServiceProxy) private serviceProxy: ServiceProxyInterface) {
super()
}
@all('*', TYPES.RequiredCrossServiceTokenMiddleware)
async subscriptions(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callSyncingServer(request, response, request.path.replace('/v1/', ''), request.body)
}
}

View File

@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.120.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.120.0...@standardnotes/auth-server@1.120.1) (2023-06-30)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.120.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.119.6...@standardnotes/auth-server@1.120.0) (2023-06-30)
### Features
* shared vaults functionality in api-gateway,auth,files,common,security,domain-events. ([#629](https://github.com/standardnotes/server/issues/629)) ([fa7fbe2](https://github.com/standardnotes/server/commit/fa7fbe26e7b0707fc21d71e04af76870f5248baf))
## [1.119.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.119.5...@standardnotes/auth-server@1.119.6) (2023-06-28)
### Bug Fixes
* **auth:** add debug logs for authentication method resolver ([d220ec5](https://github.com/standardnotes/server/commit/d220ec5bf7509f9eb19dcda71c3667aaf388a35b))
## [1.119.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.119.4...@standardnotes/auth-server@1.119.5) (2023-06-28)
### Bug Fixes
* add debug logs for invalid-auth responses ([d5a8409](https://github.com/standardnotes/server/commit/d5a8409bb5d35b9caf410a36ea0d5cb747129e8d))
## [1.119.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.119.3...@standardnotes/auth-server@1.119.4) (2023-06-28)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.119.4",
"version": "1.120.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -32,7 +32,8 @@
"weekly-backup:email": "yarn node dist/bin/backup.js email weekly",
"content-recalculation": "yarn node dist/bin/content.js",
"typeorm": "typeorm-ts-node-commonjs",
"upgrade:snjs": "yarn ncu -u '@standardnotes/*'"
"upgrade:snjs": "yarn ncu -u '@standardnotes/*'",
"migrate": "yarn build && yarn typeorm migration:run -d dist/src/Bootstrap/DataSource.js"
},
"dependencies": {
"@aws-sdk/client-sns": "^3.332.0",

View File

@@ -1,9 +1,5 @@
import { ProtocolVersion } from '@standardnotes/common'
import { SimpleUserProjection } from '../../Projection/SimpleUserProjection'
export interface AuthResponse {
user: {
uuid: string
email: string
protocolVersion: ProtocolVersion
}
user: SimpleUserProjection
}

View File

@@ -4,13 +4,13 @@ import {
TokenEncoderInterface,
} from '@standardnotes/security'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { ProtocolVersion } from '@standardnotes/common'
import { SessionBody } from '@standardnotes/responses'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { SimpleUserProjection } from '../../Projection/SimpleUserProjection'
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { KeyParamsFactoryInterface } from '../User/KeyParamsFactoryInterface'
import { User } from '../User/User'
@@ -54,11 +54,7 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
return {
session: sessionPayload,
key_params: this.keyParamsFactory.create(dto.user, true),
user: this.userProjector.projectSimple(dto.user) as {
uuid: string
email: string
protocolVersion: ProtocolVersion
},
user: this.userProjector.projectSimple(dto.user) as SimpleUserProjection,
}
}

View File

@@ -9,6 +9,7 @@ import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { AuthenticationMethodResolver } from './AuthenticationMethodResolver'
import { Logger } from 'winston'
describe('AuthenticationMethodResolver', () => {
let userRepository: UserRepositoryInterface
@@ -18,11 +19,15 @@ describe('AuthenticationMethodResolver', () => {
let user: User
let session: Session
let revokedSession: RevokedSession
let logger: Logger
const createResolver = () =>
new AuthenticationMethodResolver(userRepository, sessionService, sessionTokenDecoder, fallbackTokenDecoder)
new AuthenticationMethodResolver(userRepository, sessionService, sessionTokenDecoder, fallbackTokenDecoder, logger)
beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
user = {} as jest.Mocked<User>
session = {} as jest.Mocked<Session>

View File

@@ -5,6 +5,7 @@ import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { AuthenticationMethod } from './AuthenticationMethod'
import { AuthenticationMethodResolverInterface } from './AuthenticationMethodResolverInterface'
import { Logger } from 'winston'
@injectable()
export class AuthenticationMethodResolver implements AuthenticationMethodResolverInterface {
@@ -14,15 +15,20 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
@inject(TYPES.Auth_SessionTokenDecoder) private sessionTokenDecoder: TokenDecoderInterface<SessionTokenData>,
@inject(TYPES.Auth_FallbackSessionTokenDecoder)
private fallbackSessionTokenDecoder: TokenDecoderInterface<SessionTokenData>,
@inject(TYPES.Auth_Logger) private logger: Logger,
) {}
async resolve(token: string): Promise<AuthenticationMethod | undefined> {
let decodedToken: SessionTokenData | undefined = this.sessionTokenDecoder.decodeToken(token)
if (decodedToken === undefined) {
this.logger.debug('Could not decode token with primary decoder, trying fallback decoder.')
decodedToken = this.fallbackSessionTokenDecoder.decodeToken(token)
}
if (decodedToken) {
this.logger.debug('Token decoded successfully. User found.')
return {
type: 'jwt',
user: await this.userRepository.findOneByUuid(<string>decodedToken.user_uuid),
@@ -32,6 +38,8 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
const session = await this.sessionService.getSessionFromToken(token)
if (session) {
this.logger.debug('Token decoded successfully. Session found.')
return {
type: 'session_token',
user: await this.userRepository.findOneByUuid(session.userUuid),
@@ -41,6 +49,8 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
const revokedSession = await this.sessionService.getRevokedSessionFromToken(token)
if (revokedSession) {
this.logger.debug('Token decoded successfully. Revoked session found.')
return {
type: 'revoked',
revokedSession: await this.sessionService.markRevokedSessionAsReceived(revokedSession),
@@ -48,6 +58,8 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
}
}
this.logger.debug('Could not decode token.')
return undefined
}
}

View File

@@ -37,7 +37,7 @@ describe('SettingService', () => {
user = {
uuid: '4-5-6',
} as jest.Mocked<User>
user.isPotentiallyAVaultAccount = jest.fn().mockReturnValue(false)
user.isPotentiallyAPrivateUsernameAccount = jest.fn().mockReturnValue(false)
setting = {
name: SettingName.NAMES.DropboxBackupToken,
@@ -66,7 +66,7 @@ describe('SettingService', () => {
]),
)
settingsAssociationService.getDefaultSettingsAndValuesForNewVaultAccount = jest.fn().mockReturnValue(
settingsAssociationService.getDefaultSettingsAndValuesForNewPrivateUsernameAccount = jest.fn().mockReturnValue(
new Map([
[
SettingName.NAMES.LogSessionUserAgent,
@@ -98,7 +98,7 @@ describe('SettingService', () => {
})
it('should create default settings for a newly registered vault account', async () => {
user.isPotentiallyAVaultAccount = jest.fn().mockReturnValue(true)
user.isPotentiallyAPrivateUsernameAccount = jest.fn().mockReturnValue(true)
await createService().applyDefaultSettingsUponRegistration(user)

View File

@@ -28,8 +28,9 @@ export class SettingService implements SettingServiceInterface {
async applyDefaultSettingsUponRegistration(user: User): Promise<void> {
let defaultSettingsWithValues = this.settingsAssociationService.getDefaultSettingsAndValuesForNewUser()
if (user.isPotentiallyAVaultAccount()) {
defaultSettingsWithValues = this.settingsAssociationService.getDefaultSettingsAndValuesForNewVaultAccount()
if (user.isPotentiallyAPrivateUsernameAccount()) {
defaultSettingsWithValues =
this.settingsAssociationService.getDefaultSettingsAndValuesForNewPrivateUsernameAccount()
}
for (const settingName of defaultSettingsWithValues.keys()) {

View File

@@ -55,7 +55,7 @@ describe('SettingsAssociationService', () => {
})
it('should return the default set of settings for a newly registered vault account', () => {
const settings = createService().getDefaultSettingsAndValuesForNewVaultAccount()
const settings = createService().getDefaultSettingsAndValuesForNewPrivateUsernameAccount()
const flatSettings = [...(settings as Map<string, SettingDescription>).keys()]
expect(flatSettings).toEqual(['MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])

View File

@@ -66,7 +66,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
],
])
private readonly vaultAccountDefaultSettingsOverwrites = new Map<string, SettingDescription>([
private readonly privateUsernameAccountDefaultSettingsOverwrites = new Map<string, SettingDescription>([
[
SettingName.NAMES.LogSessionUserAgent,
{
@@ -114,16 +114,18 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
return this.defaultSettings
}
getDefaultSettingsAndValuesForNewVaultAccount(): Map<string, SettingDescription> {
const defaultVaultSettings = new Map(this.defaultSettings)
getDefaultSettingsAndValuesForNewPrivateUsernameAccount(): Map<string, SettingDescription> {
const defaultPrivateUsernameSettings = new Map(this.defaultSettings)
for (const vaultAccountDefaultSettingOverwriteKey of this.vaultAccountDefaultSettingsOverwrites.keys()) {
defaultVaultSettings.set(
vaultAccountDefaultSettingOverwriteKey,
this.vaultAccountDefaultSettingsOverwrites.get(vaultAccountDefaultSettingOverwriteKey) as SettingDescription,
for (const privateUsernameAccountDefaultSettingOverwriteKey of this.privateUsernameAccountDefaultSettingsOverwrites.keys()) {
defaultPrivateUsernameSettings.set(
privateUsernameAccountDefaultSettingOverwriteKey,
this.privateUsernameAccountDefaultSettingsOverwrites.get(
privateUsernameAccountDefaultSettingOverwriteKey,
) as SettingDescription,
)
}
return defaultVaultSettings
return defaultPrivateUsernameSettings
}
}

View File

@@ -6,7 +6,7 @@ import { SettingDescription } from './SettingDescription'
export interface SettingsAssociationServiceInterface {
getDefaultSettingsAndValuesForNewUser(): Map<string, SettingDescription>
getDefaultSettingsAndValuesForNewVaultAccount(): Map<string, SettingDescription>
getDefaultSettingsAndValuesForNewPrivateUsernameAccount(): Map<string, SettingDescription>
getPermissionAssociatedWithSetting(settingName: SettingName): PermissionName | undefined
getEncryptionVersionForSetting(settingName: SettingName): EncryptionVersion
getSensitivityForSetting(settingName: SettingName): boolean

View File

@@ -16,6 +16,8 @@ export class AuthenticateRequest implements UseCaseInterface {
async execute(dto: AuthenticateRequestDTO): Promise<AuthenticateRequestResponse> {
if (!dto.authorizationHeader) {
this.logger.debug('Authorization header not provided.')
return {
success: false,
responseCode: 401,

View File

@@ -7,6 +7,7 @@ import { AuthenticateUser } from './AuthenticateUser'
import { RevokedSession } from '../Session/RevokedSession'
import { AuthenticationMethodResolverInterface } from '../Auth/AuthenticationMethodResolverInterface'
import { TimerInterface } from '@standardnotes/time'
import { Logger } from 'winston'
describe('AuthenticateUser', () => {
let user: User
@@ -14,11 +15,15 @@ describe('AuthenticateUser', () => {
let revokedSession: RevokedSession
let authenticationMethodResolver: AuthenticationMethodResolverInterface
let timer: TimerInterface
let logger: Logger
const accessTokenAge = 3600
const createUseCase = () => new AuthenticateUser(authenticationMethodResolver, timer, accessTokenAge)
const createUseCase = () => new AuthenticateUser(authenticationMethodResolver, timer, accessTokenAge, logger)
beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
user = {} as jest.Mocked<User>
user.supportsSessions = jest.fn().mockReturnValue(false)

View File

@@ -9,6 +9,7 @@ import { Session } from '../Session/Session'
import { AuthenticateUserDTO } from './AuthenticateUserDTO'
import { AuthenticateUserResponse } from './AuthenticateUserResponse'
import { UseCaseInterface } from './UseCaseInterface'
import { Logger } from 'winston'
@injectable()
export class AuthenticateUser implements UseCaseInterface {
@@ -17,11 +18,14 @@ export class AuthenticateUser implements UseCaseInterface {
private authenticationMethodResolver: AuthenticationMethodResolverInterface,
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
@inject(TYPES.Auth_ACCESS_TOKEN_AGE) private accessTokenAge: number,
@inject(TYPES.Auth_Logger) private logger: Logger,
) {}
async execute(dto: AuthenticateUserDTO): Promise<AuthenticateUserResponse> {
const authenticationMethod = await this.authenticationMethodResolver.resolve(dto.token)
if (!authenticationMethod) {
this.logger.debug('No authentication method found for token.')
return {
success: false,
failureType: 'INVALID_AUTH',
@@ -37,6 +41,8 @@ export class AuthenticateUser implements UseCaseInterface {
const user = authenticationMethod.user
if (!user) {
this.logger.debug('No user found for authentication method.')
return {
success: false,
failureType: 'INVALID_AUTH',
@@ -44,6 +50,8 @@ export class AuthenticateUser implements UseCaseInterface {
}
if (authenticationMethod.type == 'jwt' && user.supportsSessions()) {
this.logger.debug('User supports sessions but is trying to authenticate with a JWT.')
return {
success: false,
failureType: 'INVALID_AUTH',
@@ -56,6 +64,8 @@ export class AuthenticateUser implements UseCaseInterface {
const encryptedPasswordDigest = crypto.createHash('sha256').update(user.encryptedPassword).digest('hex')
if (!pwHash || !crypto.timingSafeEqual(Buffer.from(pwHash), Buffer.from(encryptedPasswordDigest))) {
this.logger.debug('Password hash does not match.')
return {
success: false,
failureType: 'INVALID_AUTH',
@@ -66,6 +76,8 @@ export class AuthenticateUser implements UseCaseInterface {
case 'session_token': {
const session = authenticationMethod.session
if (!session) {
this.logger.debug('No session found for authentication method.')
return {
success: false,
failureType: 'INVALID_AUTH',
@@ -73,6 +85,8 @@ export class AuthenticateUser implements UseCaseInterface {
}
if (session.refreshExpiration < this.timer.getUTCDate()) {
this.logger.debug('Session refresh token has expired.')
return {
success: false,
failureType: 'INVALID_AUTH',

View File

@@ -2,7 +2,7 @@ import { inject, injectable } from 'inversify'
import { SubscriptionName } from '@standardnotes/common'
import { TimerInterface } from '@standardnotes/time'
import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security'
import { CreateValetTokenPayload, CreateValetTokenResponseData } from '@standardnotes/responses'
import { CreateValetTokenResponseData } from '@standardnotes/responses'
import { SettingName } from '@standardnotes/settings'
import TYPES from '../../../Bootstrap/Types'
@@ -12,6 +12,7 @@ import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionS
import { CreateValetTokenDTO } from './CreateValetTokenDTO'
import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { CreateValetTokenPayload } from '../../ValetToken/CreateValetTokenPayload'
@injectable()
export class CreateValetToken implements UseCaseInterface {

View File

@@ -69,7 +69,7 @@ export class InviteToSharedSubscription implements UseCaseInterface {
sharedSubscriptionInvition.inviterIdentifier = dto.inviterEmail
sharedSubscriptionInvition.inviterIdentifierType = InviterIdentifierType.Email
sharedSubscriptionInvition.inviteeIdentifier = dto.inviteeIdentifier
sharedSubscriptionInvition.inviteeIdentifierType = this.isInviteeIdentifierPotentiallyAVaultAccount(
sharedSubscriptionInvition.inviteeIdentifierType = this.isInviteeIdentifierPotentiallyAPrivateUsernameAccount(
dto.inviteeIdentifier,
)
? InviteeIdentifierType.Hash
@@ -107,7 +107,7 @@ export class InviteToSharedSubscription implements UseCaseInterface {
}
}
private isInviteeIdentifierPotentiallyAVaultAccount(identifier: string): boolean {
private isInviteeIdentifierPotentiallyAPrivateUsernameAccount(identifier: string): boolean {
return identifier.length === 64 && !identifier.includes('@')
}
}

View File

@@ -44,21 +44,13 @@ describe('UpdateUser', () => {
user,
updatedWithUserAgent: 'Mozilla',
apiVersion: '20190520',
version: '004',
pwCost: 11,
pwSalt: 'qweqwe',
pwNonce: undefined,
}),
).toEqual({ success: true, authResponse: { foo: 'bar' } })
expect(userRepository.save).toHaveBeenCalledWith({
createdAt: new Date(1),
pwCost: 11,
email: 'test@test.te',
pwSalt: 'qweqwe',
updatedWithUserAgent: 'Mozilla',
uuid: '123',
version: '004',
updatedAt: new Date(1),
})
})

View File

@@ -17,25 +17,17 @@ export class UpdateUser implements UseCaseInterface {
) {}
async execute(dto: UpdateUserDTO): Promise<UpdateUserResponse> {
const { user, apiVersion, ...updateFields } = dto
dto.user.updatedAt = this.timer.getUTCDate()
Object.keys(updateFields).forEach(
(key) => (updateFields[key] === undefined || updateFields[key] === null) && delete updateFields[key],
)
const updatedUser = await this.userRepository.save(dto.user)
Object.assign(user, updateFields)
user.updatedAt = this.timer.getUTCDate()
await this.userRepository.save(user)
const authResponseFactory = this.authResponseFactoryResolver.resolveAuthResponseFactoryVersion(apiVersion)
const authResponseFactory = this.authResponseFactoryResolver.resolveAuthResponseFactoryVersion(dto.apiVersion)
return {
success: true,
authResponse: await authResponseFactory.createResponse({
user,
apiVersion,
user: updatedUser,
apiVersion: dto.apiVersion,
userAgent: dto.updatedWithUserAgent,
ephemeralSession: false,
readonlyAccess: false,

View File

@@ -1,18 +1,7 @@
import { User } from '../User/User'
export type UpdateUserDTO = {
[key: string]: string | User | Date | undefined | number
user: User
updatedWithUserAgent: string
apiVersion: string
email?: string
pwFunc?: string
pwAlg?: string
pwCost?: number
pwKeySize?: number
pwNonce?: string
pwSalt?: string
kpOrigination?: string
kpCreated?: Date
version?: string
updatedWithUserAgent: string
}

View File

@@ -21,13 +21,13 @@ describe('User', () => {
const user = createUser()
user.email = 'a75a31ce95365904ef0e0a8e6cefc1f5e99adfef81bbdb6d4499eeb10ae0ff67'
expect(user.isPotentiallyAVaultAccount()).toBeTruthy()
expect(user.isPotentiallyAPrivateUsernameAccount()).toBeTruthy()
})
it('should indicate if the user is not a vault account', () => {
const user = createUser()
user.email = 'test@test.te'
expect(user.isPotentiallyAVaultAccount()).toBeFalsy()
expect(user.isPotentiallyAPrivateUsernameAccount()).toBeFalsy()
})
})

View File

@@ -202,7 +202,7 @@ export class User {
return parseInt(this.version) >= parseInt(ProtocolVersion.V004)
}
isPotentiallyAVaultAccount(): boolean {
isPotentiallyAPrivateUsernameAccount(): boolean {
return this.email.length === 64 && !this.email.includes('@')
}
}

View File

@@ -0,0 +1,7 @@
export type CreateValetTokenPayload = {
operation: 'read' | 'write' | 'delete' | 'move'
resources: Array<{
remoteIdentifier: string
unencryptedFileSize?: number
}>
}

View File

@@ -60,15 +60,6 @@ export class HomeServerUsersController extends BaseHttpController {
user: response.locals.user,
updatedWithUserAgent: <string>request.headers['user-agent'],
apiVersion: request.body.api,
pwFunc: request.body.pw_func,
pwAlg: request.body.pw_alg,
pwCost: request.body.pw_cost,
pwKeySize: request.body.pw_key_size,
pwNonce: request.body.pw_nonce,
pwSalt: request.body.pw_salt,
kpOrigination: request.body.origination,
kpCreated: request.body.created,
version: request.body.version,
})
if (updateResult.success) {

View File

@@ -1,10 +1,11 @@
import { ControllerContainerInterface, Uuid } from '@standardnotes/domain-core'
import { Request, Response } from 'express'
import { BaseHttpController, results } from 'inversify-express-utils'
import { ErrorTag } from '@standardnotes/responses'
import { ValetTokenOperation } from '@standardnotes/security'
import { CreateValetToken } from '../../../Domain/UseCase/CreateValetToken/CreateValetToken'
import { CreateValetTokenPayload, ErrorTag } from '@standardnotes/responses'
import { ValetTokenOperation } from '@standardnotes/security'
import { CreateValetTokenPayload } from '../../../Domain/ValetToken/CreateValetTokenPayload'
export class HomeServerValetTokenController extends BaseHttpController {
constructor(protected createValetKey: CreateValetToken, private controllerContainer?: ControllerContainerInterface) {

View File

@@ -99,9 +99,7 @@ describe('InversifyExpressUsersController', () => {
expect(updateUser.execute).toHaveBeenCalledWith({
apiVersion: '20190520',
kpOrigination: 'test',
updatedWithUserAgent: 'Google Chrome',
version: '002',
user: {
uuid: '123',
email: 'test@test.te',
@@ -143,9 +141,7 @@ describe('InversifyExpressUsersController', () => {
expect(updateUser.execute).toHaveBeenCalledWith({
apiVersion: '20190520',
kpOrigination: 'test',
updatedWithUserAgent: 'Google Chrome',
version: '002',
user: {
uuid: '123',
email: 'test@test.te',

View File

@@ -0,0 +1,5 @@
export type SimpleUserProjection = {
uuid: string
email: string
protocolVersion: string
}

View File

@@ -2,10 +2,11 @@ import { injectable } from 'inversify'
import { User } from '../Domain/User/User'
import { ProjectorInterface } from './ProjectorInterface'
import { SimpleUserProjection } from './SimpleUserProjection'
@injectable()
export class UserProjector implements ProjectorInterface<User> {
projectSimple(user: User): Record<string, unknown> {
projectSimple(user: User): SimpleUserProjection {
return {
uuid: user.uuid,
email: user.email,

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.49.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.48.3...@standardnotes/common@1.49.0) (2023-06-30)
### Features
* shared vaults functionality in api-gateway,auth,files,common,security,domain-events. ([#629](https://github.com/standardnotes/server/issues/629)) ([fa7fbe2](https://github.com/standardnotes/server/commit/fa7fbe26e7b0707fc21d71e04af76870f5248baf))
## [1.48.3](https://github.com/standardnotes/server/compare/@standardnotes/common@1.48.2...@standardnotes/common@1.48.3) (2023-06-28)
**Note:** Version bump only for package @standardnotes/common

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/common",
"version": "1.48.3",
"version": "1.49.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,7 +3,6 @@ export enum ProtocolVersion {
V002 = '002',
V003 = '003',
V004 = '004',
V005 = '005',
}
export const ProtocolVersionLatest = ProtocolVersion.V004

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.12.7](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.6...@standardnotes/domain-events-infra@1.12.7) (2023-06-30)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.6](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.5...@standardnotes/domain-events-infra@1.12.6) (2023-06-30)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.5](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.4...@standardnotes/domain-events-infra@1.12.5) (2023-06-01)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.12.5",
"version": "1.12.7",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.112.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.112.0...@standardnotes/domain-events@2.112.1) (2023-06-30)
**Note:** Version bump only for package @standardnotes/domain-events
# [2.112.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.111.4...@standardnotes/domain-events@2.112.0) (2023-06-30)
### Features
* shared vaults functionality in api-gateway,auth,files,common,security,domain-events. ([#629](https://github.com/standardnotes/server/issues/629)) ([fa7fbe2](https://github.com/standardnotes/server/commit/fa7fbe26e7b0707fc21d71e04af76870f5248baf))
## [2.111.4](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.111.3...@standardnotes/domain-events@2.111.4) (2023-06-01)
**Note:** Version bump only for package @standardnotes/domain-events

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.111.4",
"version": "2.112.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -7,7 +7,7 @@ export interface DomainEventInterface {
meta: {
correlation: {
userIdentifier: string
userIdentifierType: 'uuid' | 'email'
userIdentifierType: 'uuid' | 'email' | 'shared-vault-uuid'
}
origin: DomainEventService
target?: DomainEventService

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { SharedVaultFileRemovedEventPayload } from './SharedVaultFileRemovedEventPayload'
export interface SharedVaultFileRemovedEvent extends DomainEventInterface {
type: 'SHARED_VAULT_FILE_REMOVED'
payload: SharedVaultFileRemovedEventPayload
}

View File

@@ -0,0 +1,6 @@
export interface SharedVaultFileRemovedEventPayload {
sharedVaultUuid: string
fileByteSize: number
filePath: string
fileName: string
}

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { SharedVaultFileUploadedEventPayload } from './SharedVaultFileUploadedEventPayload'
export interface SharedVaultFileUploadedEvent extends DomainEventInterface {
type: 'SHARED_VAULT_FILE_UPLOADED'
payload: SharedVaultFileUploadedEventPayload
}

View File

@@ -0,0 +1,6 @@
export interface SharedVaultFileUploadedEventPayload {
sharedVaultUuid: string
fileByteSize: number
filePath: string
fileName: string
}

View File

@@ -62,6 +62,10 @@ export * from './Event/SharedSubscriptionInvitationCanceledEvent'
export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
export * from './Event/SharedSubscriptionInvitationCreatedEvent'
export * from './Event/SharedSubscriptionInvitationCreatedEventPayload'
export * from './Event/SharedVaultFileRemovedEvent'
export * from './Event/SharedVaultFileRemovedEventPayload'
export * from './Event/SharedVaultFileUploadedEvent'
export * from './Event/SharedVaultFileUploadedEventPayload'
export * from './Event/StatisticPersistenceRequestedEvent'
export * from './Event/StatisticPersistenceRequestedEventPayload'
export * from './Event/SubscriptionCancelledEvent'

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.1](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.0...@standardnotes/event-store@1.11.1) (2023-06-30)
**Note:** Version bump only for package @standardnotes/event-store
# [1.11.0](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.10.1...@standardnotes/event-store@1.11.0) (2023-06-30)
### Features
* shared vaults functionality in api-gateway,auth,files,common,security,domain-events. ([#629](https://github.com/standardnotes/server/issues/629)) ([fa7fbe2](https://github.com/standardnotes/server/commit/fa7fbe26e7b0707fc21d71e04af76870f5248baf))
## [1.10.1](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.10.0...@standardnotes/event-store@1.10.1) (2023-06-02)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.10.1",
"version": "1.11.1",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.19.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.0...@standardnotes/files-server@1.19.1) (2023-06-30)
**Note:** Version bump only for package @standardnotes/files-server
# [1.19.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.18.3...@standardnotes/files-server@1.19.0) (2023-06-30)
### Features
* shared vaults functionality in api-gateway,auth,files,common,security,domain-events. ([#629](https://github.com/standardnotes/files/issues/629)) ([fa7fbe2](https://github.com/standardnotes/files/commit/fa7fbe26e7b0707fc21d71e04af76870f5248baf))
## [1.18.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.18.2...@standardnotes/files-server@1.18.3) (2023-06-22)
### Bug Fixes

View File

@@ -4,6 +4,7 @@ import * as busboy from 'connect-busboy'
import '../src/Infra/InversifyExpress/InversifyExpressHealthCheckController'
import '../src/Infra/InversifyExpress/InversifyExpressFilesController'
import '../src/Infra/InversifyExpress/InversifyExpressSharedVaultFilesController'
import helmet from 'helmet'
import * as cors from 'cors'

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.18.3",
"version": "1.19.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -48,6 +48,11 @@ import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountD
import { SharedSubscriptionInvitationCanceledEventHandler } from '../Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler'
import { InMemoryUploadRepository } from '../Infra/InMemory/InMemoryUploadRepository'
import { Transform } from 'stream'
import { FileMoverInterface } from '../Domain/Services/FileMoverInterface'
import { S3FileMover } from '../Infra/S3/S3FileMover'
import { FSFileMover } from '../Infra/FS/FSFileMover'
import { MoveFile } from '../Domain/UseCase/MoveFile/MoveFile'
import { SharedVaultValetTokenAuthMiddleware } from '../Infra/InversifyExpress/Middleware/SharedVaultValetTokenAuthMiddleware'
export class ContainerConfigLoader {
async load(configuration?: {
@@ -177,6 +182,7 @@ export class ContainerConfigLoader {
container.bind<FileDownloaderInterface>(TYPES.Files_FileDownloader).to(S3FileDownloader)
container.bind<FileUploaderInterface>(TYPES.Files_FileUploader).to(S3FileUploader)
container.bind<FileRemoverInterface>(TYPES.Files_FileRemover).to(S3FileRemover)
container.bind<FileMoverInterface>(TYPES.Files_FileMover).to(S3FileMover)
} else {
container.bind<FileDownloaderInterface>(TYPES.Files_FileDownloader).to(FSFileDownloader)
container
@@ -185,6 +191,7 @@ export class ContainerConfigLoader {
new FSFileUploader(container.get(TYPES.Files_FILE_UPLOAD_PATH), container.get(TYPES.Files_Logger)),
)
container.bind<FileRemoverInterface>(TYPES.Files_FileRemover).to(FSFileRemover)
container.bind<FileMoverInterface>(TYPES.Files_FileMover).to(FSFileMover)
}
// use cases
@@ -194,10 +201,14 @@ export class ContainerConfigLoader {
container.bind<FinishUploadSession>(TYPES.Files_FinishUploadSession).to(FinishUploadSession)
container.bind<GetFileMetadata>(TYPES.Files_GetFileMetadata).to(GetFileMetadata)
container.bind<RemoveFile>(TYPES.Files_RemoveFile).to(RemoveFile)
container.bind<MoveFile>(TYPES.Files_MoveFile).to(MoveFile)
container.bind<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
// middleware
container.bind<ValetTokenAuthMiddleware>(TYPES.Files_ValetTokenAuthMiddleware).to(ValetTokenAuthMiddleware)
container
.bind<SharedVaultValetTokenAuthMiddleware>(TYPES.Files_SharedVaultValetTokenAuthMiddleware)
.to(SharedVaultValetTokenAuthMiddleware)
// services
container

View File

@@ -13,6 +13,7 @@ const TYPES = {
Files_FinishUploadSession: Symbol.for('Files_FinishUploadSession'),
Files_GetFileMetadata: Symbol.for('Files_GetFileMetadata'),
Files_RemoveFile: Symbol.for('Files_RemoveFile'),
Files_MoveFile: Symbol.for('Files_MoveFile'),
Files_MarkFilesToBeRemoved: Symbol.for('Files_MarkFilesToBeRemoved'),
// services
@@ -23,12 +24,14 @@ const TYPES = {
Files_FileUploader: Symbol.for('Files_FileUploader'),
Files_FileDownloader: Symbol.for('Files_FileDownloader'),
Files_FileRemover: Symbol.for('Files_FileRemover'),
Files_FileMover: Symbol.for('Files_FileMover'),
// repositories
Files_UploadRepository: Symbol.for('Files_UploadRepository'),
// middleware
Files_ValetTokenAuthMiddleware: Symbol.for('Files_ValetTokenAuthMiddleware'),
Files_SharedVaultValetTokenAuthMiddleware: Symbol.for('Files_SharedVaultValetTokenAuthMiddleware'),
// env vars
Files_S3_ENDPOINT: Symbol.for('Files_S3_ENDPOINT'),

View File

@@ -14,6 +14,60 @@ describe('DomainEventFactory', () => {
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
})
it('should create a SHARED_VAULT_FILE_UPLOADED event', () => {
expect(
createFactory().createSharedVaultFileUploadedEvent({
sharedVaultUuid: '1-2-3',
filePath: 'foo/bar',
fileName: 'baz',
fileByteSize: 123,
}),
).toEqual({
createdAt: new Date(1),
meta: {
correlation: {
userIdentifier: '1-2-3',
userIdentifierType: 'shared-vault-uuid',
},
origin: 'files',
},
payload: {
sharedVaultUuid: '1-2-3',
filePath: 'foo/bar',
fileName: 'baz',
fileByteSize: 123,
},
type: 'SHARED_VAULT_FILE_UPLOADED',
})
})
it('should create a SHARED_VAULT_FILE_REMOVED event', () => {
expect(
createFactory().createSharedVaultFileRemovedEvent({
sharedVaultUuid: '1-2-3',
filePath: 'foo/bar',
fileName: 'baz',
fileByteSize: 123,
}),
).toEqual({
createdAt: new Date(1),
meta: {
correlation: {
userIdentifier: '1-2-3',
userIdentifierType: 'shared-vault-uuid',
},
origin: 'files',
},
payload: {
sharedVaultUuid: '1-2-3',
filePath: 'foo/bar',
fileName: 'baz',
fileByteSize: 123,
},
type: 'SHARED_VAULT_FILE_REMOVED',
})
})
it('should create a FILE_UPLOADED event', () => {
expect(
createFactory().createFileUploadedEvent({

View File

@@ -1,4 +1,10 @@
import { FileUploadedEvent, FileRemovedEvent, DomainEventService } from '@standardnotes/domain-events'
import {
FileUploadedEvent,
FileRemovedEvent,
DomainEventService,
SharedVaultFileUploadedEvent,
SharedVaultFileRemovedEvent,
} from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
@@ -49,4 +55,44 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
payload,
}
}
createSharedVaultFileUploadedEvent(payload: {
sharedVaultUuid: string
filePath: string
fileName: string
fileByteSize: number
}): SharedVaultFileUploadedEvent {
return {
type: 'SHARED_VAULT_FILE_UPLOADED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: payload.sharedVaultUuid,
userIdentifierType: 'shared-vault-uuid',
},
origin: DomainEventService.Files,
},
payload,
}
}
createSharedVaultFileRemovedEvent(payload: {
sharedVaultUuid: string
filePath: string
fileName: string
fileByteSize: number
}): SharedVaultFileRemovedEvent {
return {
type: 'SHARED_VAULT_FILE_REMOVED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: payload.sharedVaultUuid,
userIdentifierType: 'shared-vault-uuid',
},
origin: DomainEventService.Files,
},
payload,
}
}
}

View File

@@ -1,4 +1,9 @@
import { FileUploadedEvent, FileRemovedEvent } from '@standardnotes/domain-events'
import {
FileUploadedEvent,
FileRemovedEvent,
SharedVaultFileRemovedEvent,
SharedVaultFileUploadedEvent,
} from '@standardnotes/domain-events'
export interface DomainEventFactoryInterface {
createFileUploadedEvent(payload: {
@@ -14,4 +19,16 @@ export interface DomainEventFactoryInterface {
fileByteSize: number
regularSubscriptionUuid: string
}): FileRemovedEvent
createSharedVaultFileUploadedEvent(payload: {
sharedVaultUuid: string
filePath: string
fileName: string
fileByteSize: number
}): SharedVaultFileUploadedEvent
createSharedVaultFileRemovedEvent(payload: {
sharedVaultUuid: string
filePath: string
fileName: string
fileByteSize: number
}): SharedVaultFileRemovedEvent
}

View File

@@ -44,7 +44,7 @@ describe('AccountDeletionRequestedEventHandler', () => {
it('should mark files to be remove for user', async () => {
await createHandler().handle(event)
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ ownerUuid: '1-2-3' })
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
@@ -66,7 +66,7 @@ describe('AccountDeletionRequestedEventHandler', () => {
await createHandler().handle(event)
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ ownerUuid: '1-2-3' })
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})

View File

@@ -23,7 +23,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
}
const response = await this.markFilesToBeRemoved.execute({
userUuid: event.payload.userUuid,
ownerUuid: event.payload.userUuid,
})
if (!response.success) {

View File

@@ -44,7 +44,7 @@ describe('SharedSubscriptionInvitationCanceledEventHandler', () => {
it('should mark files to be remove for user', async () => {
await createHandler().handle(event)
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ ownerUuid: '1-2-3' })
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
@@ -66,7 +66,7 @@ describe('SharedSubscriptionInvitationCanceledEventHandler', () => {
await createHandler().handle(event)
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ ownerUuid: '1-2-3' })
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})

View File

@@ -23,7 +23,7 @@ export class SharedSubscriptionInvitationCanceledEventHandler implements DomainE
}
const response = await this.markFilesToBeRemoved.execute({
userUuid: event.payload.inviteeIdentifier,
ownerUuid: event.payload.inviteeIdentifier,
})
if (!response.success) {

View File

@@ -0,0 +1,3 @@
export interface FileMoverInterface {
moveFile(sourcePath: string, destinationPath: string): Promise<void>
}

View File

@@ -33,7 +33,7 @@ describe('CreateUploadSession', () => {
expect(
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
userUuid: '1-2-3',
ownerUuid: '1-2-3',
}),
).toEqual({
success: false,
@@ -44,7 +44,7 @@ describe('CreateUploadSession', () => {
it('should create an upload session', async () => {
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
userUuid: '1-2-3',
ownerUuid: '1-2-3',
})
expect(fileUploader.createUploadSession).toHaveBeenCalledWith('1-2-3/2-3-4')

View File

@@ -20,7 +20,7 @@ export class CreateUploadSession implements UseCaseInterface {
try {
this.logger.debug(`Creating upload session for resource: ${dto.resourceRemoteIdentifier}`)
const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}`
const filePath = `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`
const uploadId = await this.fileUploader.createUploadSession(filePath)

View File

@@ -1,4 +1,4 @@
export type CreateUploadSessionDTO = {
userUuid: string
ownerUuid: string
resourceRemoteIdentifier: string
}

View File

@@ -1,6 +1,10 @@
import 'reflect-metadata'
import { DomainEventPublisherInterface, FileUploadedEvent } from '@standardnotes/domain-events'
import {
DomainEventPublisherInterface,
FileUploadedEvent,
SharedVaultFileUploadedEvent,
} from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { FileUploaderInterface } from '../../Services/FileUploaderInterface'
@@ -31,6 +35,9 @@ describe('FinishUploadSession', () => {
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createFileUploadedEvent = jest.fn().mockReturnValue({} as jest.Mocked<FileUploadedEvent>)
domainEventFactory.createSharedVaultFileUploadedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<SharedVaultFileUploadedEvent>)
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
@@ -43,7 +50,8 @@ describe('FinishUploadSession', () => {
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
userUuid: '1-2-3',
ownerUuid: '1-2-3',
ownerType: 'user',
uploadBytesLimit: 100,
uploadBytesUsed: 0,
})
@@ -60,7 +68,8 @@ describe('FinishUploadSession', () => {
expect(
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
userUuid: '1-2-3',
ownerUuid: '1-2-3',
ownerType: 'user',
uploadBytesLimit: 100,
uploadBytesUsed: 0,
}),
@@ -76,7 +85,23 @@ describe('FinishUploadSession', () => {
it('should finish an upload session', async () => {
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
userUuid: '1-2-3',
ownerUuid: '1-2-3',
ownerType: 'user',
uploadBytesLimit: 100,
uploadBytesUsed: 0,
})
expect(fileUploader.finishUploadSession).toHaveBeenCalledWith('123', '1-2-3/2-3-4', [
{ tag: '123', chunkId: 1, chunkSize: 1 },
])
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('should finish an upload session for a vault shared file', async () => {
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
ownerUuid: '1-2-3',
ownerType: 'shared-vault',
uploadBytesLimit: 100,
uploadBytesUsed: 0,
})
@@ -97,7 +122,8 @@ describe('FinishUploadSession', () => {
expect(
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
userUuid: '1-2-3',
ownerUuid: '1-2-3',
ownerType: 'user',
uploadBytesLimit: 100,
uploadBytesUsed: 20,
}),

View File

@@ -24,7 +24,7 @@ export class FinishUploadSession implements UseCaseInterface {
try {
this.logger.debug(`Finishing upload session for resource: ${dto.resourceRemoteIdentifier}`)
const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}`
const filePath = `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`
const uploadId = await this.uploadRepository.retrieveUploadSessionId(filePath)
if (uploadId === undefined) {
@@ -53,14 +53,25 @@ export class FinishUploadSession implements UseCaseInterface {
await this.fileUploader.finishUploadSession(uploadId, filePath, uploadChunkResults)
await this.domainEventPublisher.publish(
this.domainEventFactory.createFileUploadedEvent({
userUuid: dto.userUuid,
filePath: `${dto.userUuid}/${dto.resourceRemoteIdentifier}`,
fileName: dto.resourceRemoteIdentifier,
fileByteSize: totalFileSize,
}),
)
if (dto.ownerType === 'user') {
await this.domainEventPublisher.publish(
this.domainEventFactory.createFileUploadedEvent({
userUuid: dto.ownerUuid,
filePath: `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
fileName: dto.resourceRemoteIdentifier,
fileByteSize: totalFileSize,
}),
)
} else {
await this.domainEventPublisher.publish(
this.domainEventFactory.createSharedVaultFileUploadedEvent({
sharedVaultUuid: dto.ownerUuid,
filePath: `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
fileName: dto.resourceRemoteIdentifier,
fileByteSize: totalFileSize,
}),
)
}
return {
success: true,

View File

@@ -1,5 +1,6 @@
export type FinishUploadSessionDTO = {
userUuid: string
ownerUuid: string
ownerType: 'user' | 'shared-vault'
resourceRemoteIdentifier: string
uploadBytesUsed: number
uploadBytesLimit: number

View File

@@ -19,7 +19,7 @@ describe('GetFileMetadata', () => {
})
it('should return the file metadata', async () => {
expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', userUuid: '2-3-4' })).toEqual({
expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })).toEqual({
success: true,
size: 123,
})
@@ -30,7 +30,7 @@ describe('GetFileMetadata', () => {
throw new Error('ooops')
})
expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', userUuid: '2-3-4' })).toEqual({
expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })).toEqual({
success: false,
message: 'Could not get file metadata.',
})

View File

@@ -15,14 +15,14 @@ export class GetFileMetadata implements UseCaseInterface {
async execute(dto: GetFileMetadataDTO): Promise<GetFileMetadataResponse> {
try {
const size = await this.fileDownloader.getFileSize(`${dto.userUuid}/${dto.resourceRemoteIdentifier}`)
const size = await this.fileDownloader.getFileSize(`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
return {
success: true,
size,
}
} catch (error) {
this.logger.error(`Could not get file metadata for resource: ${dto.userUuid}/${dto.resourceRemoteIdentifier}`)
this.logger.error(`Could not get file metadata for resource: ${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
return {
success: false,
message: 'Could not get file metadata.',

View File

@@ -1,4 +1,4 @@
export type GetFileMetadataDTO = {
userUuid: string
ownerUuid: string
resourceRemoteIdentifier: string
}

View File

@@ -21,7 +21,7 @@ describe('MarkFilesToBeRemoved', () => {
})
it('should mark files for being removed', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({ success: true })
expect(await createUseCase().execute({ ownerUuid: '1-2-3' })).toEqual({ success: true })
expect(fileRemover.markFilesToBeRemoved).toHaveBeenCalledWith('1-2-3')
})
@@ -31,7 +31,7 @@ describe('MarkFilesToBeRemoved', () => {
throw new Error('Oops')
})
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
expect(await createUseCase().execute({ ownerUuid: '1-2-3' })).toEqual({
success: false,
message: 'Could not mark resources for removal',
})

View File

@@ -16,16 +16,16 @@ export class MarkFilesToBeRemoved implements UseCaseInterface {
async execute(dto: MarkFilesToBeRemovedDTO): Promise<MarkFilesToBeRemovedResponse> {
try {
this.logger.debug(`Marking files for later removal for user: ${dto.userUuid}`)
this.logger.debug(`Marking files for later removal for user: ${dto.ownerUuid}`)
const filesRemoved = await this.fileRemover.markFilesToBeRemoved(dto.userUuid)
const filesRemoved = await this.fileRemover.markFilesToBeRemoved(dto.ownerUuid)
return {
success: true,
filesRemoved,
}
} catch (error) {
this.logger.error(`Could not mark resources for removal: ${dto.userUuid} - ${(error as Error).message}`)
this.logger.error(`Could not mark resources for removal: ${dto.ownerUuid} - ${(error as Error).message}`)
return {
success: false,

View File

@@ -1,3 +1,3 @@
export type MarkFilesToBeRemovedDTO = {
userUuid: string
ownerUuid: string
}

View File

@@ -0,0 +1,50 @@
import 'reflect-metadata'
import { Logger } from 'winston'
import { MoveFile } from './MoveFile'
import { FileMoverInterface } from '../../Services/FileMoverInterface'
describe('MoveFile', () => {
let fileMover: FileMoverInterface
let logger: Logger
const createUseCase = () => new MoveFile(fileMover, logger)
beforeEach(() => {
fileMover = {} as jest.Mocked<FileMoverInterface>
fileMover.moveFile = jest.fn().mockReturnValue(413)
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
logger.error = jest.fn()
logger.warn = jest.fn()
})
it('should move a file', async () => {
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
fromUuid: '1-2-3',
toUuid: '4-5-6',
moveType: 'shared-vault-to-user',
})
expect(fileMover.moveFile).toHaveBeenCalledWith('1-2-3/2-3-4', '4-5-6/2-3-4')
})
it('should indicate an error if moving fails', async () => {
fileMover.moveFile = jest.fn().mockImplementation(() => {
throw new Error('oops')
})
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
fromUuid: '1-2-3',
toUuid: '4-5-6',
moveType: 'shared-vault-to-user',
})
expect(result.isFailed()).toEqual(true)
})
})

View File

@@ -0,0 +1,32 @@
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { FileMoverInterface } from '../../Services/FileMoverInterface'
import { MoveFileDTO } from './MoveFileDTO'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
@injectable()
export class MoveFile implements UseCaseInterface<boolean> {
constructor(
@inject(TYPES.Files_FileMover) private fileMover: FileMoverInterface,
@inject(TYPES.Files_Logger) private logger: Logger,
) {}
async execute(dto: MoveFileDTO): Promise<Result<boolean>> {
try {
const srcPath = `${dto.fromUuid}/${dto.resourceRemoteIdentifier}`
const destPath = `${dto.toUuid}/${dto.resourceRemoteIdentifier}`
this.logger.debug(`Moving file from ${srcPath} to ${destPath}`)
await this.fileMover.moveFile(srcPath, destPath)
return Result.ok()
} catch (error) {
this.logger.error(`Could not move resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`)
return Result.fail('Could not move resource')
}
}
}

View File

@@ -0,0 +1,8 @@
import { SharedVaultMoveType } from '@standardnotes/security'
export interface MoveFileDTO {
moveType: SharedVaultMoveType
fromUuid: string
toUuid: string
resourceRemoteIdentifier: string
}

View File

@@ -1,6 +1,10 @@
import 'reflect-metadata'
import { DomainEventPublisherInterface, FileRemovedEvent } from '@standardnotes/domain-events'
import {
DomainEventPublisherInterface,
FileRemovedEvent,
SharedVaultFileRemovedEvent,
} from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
@@ -24,6 +28,9 @@ describe('RemoveFile', () => {
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createFileRemovedEvent = jest.fn().mockReturnValue({} as jest.Mocked<FileRemovedEvent>)
domainEventFactory.createSharedVaultFileRemovedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<SharedVaultFileRemovedEvent>)
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
@@ -36,25 +43,44 @@ describe('RemoveFile', () => {
throw new Error('oops')
})
expect(
await createUseCase().execute({
const result = await createUseCase().execute({
userInput: {
resourceRemoteIdentifier: '2-3-4',
userUuid: '1-2-3',
regularSubscriptionUuid: '3-4-5',
}),
).toEqual({
success: false,
message: 'Could not remove resource',
},
})
expect(result.isFailed()).toEqual(true)
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should remove a file', async () => {
it('should indicate of an error of no proper input', async () => {
const result = await createUseCase().execute({})
expect(result.isFailed()).toEqual(true)
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should remove a file for user', async () => {
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
userUuid: '1-2-3',
regularSubscriptionUuid: '3-4-5',
userInput: {
resourceRemoteIdentifier: '2-3-4',
userUuid: '1-2-3',
regularSubscriptionUuid: '3-4-5',
},
})
expect(fileRemover.remove).toHaveBeenCalledWith('1-2-3/2-3-4')
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('should remove a file for shared vault', async () => {
await createUseCase().execute({
vaultInput: {
resourceRemoteIdentifier: '2-3-4',
sharedVaultUuid: '1-2-3',
},
})
expect(fileRemover.remove).toHaveBeenCalledWith('1-2-3/2-3-4')

View File

@@ -5,12 +5,11 @@ import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { FileRemoverInterface } from '../../Services/FileRemoverInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { RemoveFileDTO } from './RemoveFileDTO'
import { RemoveFileResponse } from './RemoveFileResponse'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
@injectable()
export class RemoveFile implements UseCaseInterface {
export class RemoveFile implements UseCaseInterface<boolean> {
constructor(
@inject(TYPES.Files_FileRemover) private fileRemover: FileRemoverInterface,
@inject(TYPES.Files_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@@ -18,34 +17,46 @@ export class RemoveFile implements UseCaseInterface {
@inject(TYPES.Files_Logger) private logger: Logger,
) {}
async execute(dto: RemoveFileDTO): Promise<RemoveFileResponse> {
try {
this.logger.debug(`Removing file: ${dto.resourceRemoteIdentifier}`)
async execute(dto: RemoveFileDTO): Promise<Result<boolean>> {
const resourceUuid = dto.userInput?.resourceRemoteIdentifier ?? dto.vaultInput?.resourceRemoteIdentifier
const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}`
const ownerUuid = dto.userInput?.userUuid ?? dto.vaultInput?.sharedVaultUuid
try {
this.logger.debug(`Removing file: ${resourceUuid}`)
const filePath = `${ownerUuid}/${resourceUuid}`
const removedFileSize = await this.fileRemover.remove(filePath)
await this.domainEventPublisher.publish(
this.domainEventFactory.createFileRemovedEvent({
userUuid: dto.userUuid,
filePath: `${dto.userUuid}/${dto.resourceRemoteIdentifier}`,
fileName: dto.resourceRemoteIdentifier,
fileByteSize: removedFileSize,
regularSubscriptionUuid: dto.regularSubscriptionUuid,
}),
)
return {
success: true,
if (dto.userInput !== undefined) {
await this.domainEventPublisher.publish(
this.domainEventFactory.createFileRemovedEvent({
userUuid: dto.userInput.userUuid,
filePath: `${dto.userInput.userUuid}/${dto.userInput.resourceRemoteIdentifier}`,
fileName: dto.userInput.resourceRemoteIdentifier,
fileByteSize: removedFileSize,
regularSubscriptionUuid: dto.userInput.regularSubscriptionUuid,
}),
)
} else if (dto.vaultInput !== undefined) {
await this.domainEventPublisher.publish(
this.domainEventFactory.createSharedVaultFileRemovedEvent({
sharedVaultUuid: dto.vaultInput.sharedVaultUuid,
filePath: `${dto.vaultInput.sharedVaultUuid}/${dto.vaultInput.resourceRemoteIdentifier}`,
fileName: dto.vaultInput.resourceRemoteIdentifier,
fileByteSize: removedFileSize,
}),
)
} else {
return Result.fail('Could not remove file')
}
return Result.ok()
} catch (error) {
this.logger.error(`Could not remove resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`)
this.logger.error(`Could not remove resource: ${resourceUuid} - ${(error as Error).message}`)
return {
success: false,
message: 'Could not remove resource',
}
return Result.fail('Could not remove resource')
}
}
}

View File

@@ -1,5 +1,11 @@
export type RemoveFileDTO = {
userUuid: string
resourceRemoteIdentifier: string
regularSubscriptionUuid: string
export interface RemoveFileDTO {
userInput?: {
userUuid: string
resourceRemoteIdentifier: string
regularSubscriptionUuid: string
}
vaultInput?: {
sharedVaultUuid: string
resourceRemoteIdentifier: string
}
}

View File

@@ -1,8 +0,0 @@
export type RemoveFileResponse =
| {
success: true
}
| {
success: false
message: string
}

View File

@@ -22,7 +22,7 @@ describe('StreamDownloadFile', () => {
it('should stream download file contents from S3', async () => {
const result = await createUseCase().execute({
userUuid: '2-3-4',
ownerUuid: '2-3-4',
resourceRemoteIdentifier: '1-2-3',
startRange: 0,
endRange: 200,
@@ -37,7 +37,7 @@ describe('StreamDownloadFile', () => {
})
const result = await createUseCase().execute({
userUuid: '2-3-4',
ownerUuid: '2-3-4',
resourceRemoteIdentifier: '1-2-3',
startRange: 0,
endRange: 200,

View File

@@ -16,7 +16,7 @@ export class StreamDownloadFile implements UseCaseInterface {
async execute(dto: StreamDownloadFileDTO): Promise<StreamDownloadFileResponse> {
try {
const readStream = await this.fileDownloader.createDownloadStream(
`${dto.userUuid}/${dto.resourceRemoteIdentifier}`,
`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
dto.startRange,
dto.endRange,
)
@@ -27,7 +27,7 @@ export class StreamDownloadFile implements UseCaseInterface {
}
} catch (error) {
this.logger.error(
`Could not create a download stream for resource: ${dto.userUuid}/${dto.resourceRemoteIdentifier}`,
`Could not create a download stream for resource: ${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
)
return {

View File

@@ -1,5 +1,5 @@
export type StreamDownloadFileDTO = {
userUuid: string
ownerUuid: string
resourceRemoteIdentifier: string
startRange: number
endRange: number

View File

@@ -33,7 +33,7 @@ describe('UploadFileChunk', () => {
data: new Uint8Array([]),
resourceRemoteIdentifier: '2-3-4',
resourceUnencryptedFileSize: 123,
userUuid: '1-2-3',
ownerUuid: '1-2-3',
}),
).toEqual({
success: false,
@@ -52,7 +52,7 @@ describe('UploadFileChunk', () => {
data: new Uint8Array([123]),
resourceRemoteIdentifier: '2-3-4',
resourceUnencryptedFileSize: 123,
userUuid: '1-2-3',
ownerUuid: '1-2-3',
})
expect(fileUploader.uploadFileChunk).not.toHaveBeenCalled()
@@ -70,7 +70,7 @@ describe('UploadFileChunk', () => {
data: new Uint8Array([123]),
resourceRemoteIdentifier: '2-3-4',
resourceUnencryptedFileSize: 123,
userUuid: '1-2-3',
ownerUuid: '1-2-3',
}),
).toEqual({
success: false,
@@ -87,7 +87,7 @@ describe('UploadFileChunk', () => {
data: new Uint8Array([123]),
resourceRemoteIdentifier: '2-3-4',
resourceUnencryptedFileSize: 123,
userUuid: '1-2-3',
ownerUuid: '1-2-3',
})
expect(fileUploader.uploadFileChunk).toHaveBeenCalledWith({

View File

@@ -33,7 +33,7 @@ export class UploadFileChunk implements UseCaseInterface {
`Starting upload file chunk ${dto.chunkId} with ${dto.data.byteLength} bytes for resource: ${dto.resourceRemoteIdentifier}`,
)
const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}`
const filePath = `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`
const uploadId = await this.uploadRepository.retrieveUploadSessionId(filePath)
if (uploadId === undefined) {

View File

@@ -3,7 +3,7 @@ import { ChunkId } from '../../Upload/ChunkId'
export type UploadFileChunkDTO = {
data: Uint8Array
chunkId: ChunkId
userUuid: string
ownerUuid: string
resourceRemoteIdentifier: string
resourceUnencryptedFileSize: number
}

View File

@@ -0,0 +1,22 @@
import { inject, injectable } from 'inversify'
import { promises as fsPromises } from 'fs'
import * as path from 'path'
import { FileMoverInterface } from '../../Domain/Services/FileMoverInterface'
import TYPES from '../../Bootstrap/Types'
@injectable()
export class FSFileMover implements FileMoverInterface {
constructor(@inject(TYPES.Files_FILE_UPLOAD_PATH) private fileUploadPath: string) {}
async moveFile(sourcePath: string, destinationPath: string): Promise<void> {
const sourceFullPath = `${this.fileUploadPath}/${sourcePath}`
const destinationFullPath = `${this.fileUploadPath}/${destinationPath}`
const destinationDir = path.dirname(destinationFullPath)
await fsPromises.mkdir(destinationDir, { recursive: true })
await fsPromises.rename(sourceFullPath, destinationFullPath)
}
}

View File

@@ -13,6 +13,7 @@ import { results } from 'inversify-express-utils'
import { RemoveFile } from '../../Domain/UseCase/RemoveFile/RemoveFile'
import { ValetTokenOperation } from '@standardnotes/security'
import { BadRequestErrorMessageResult } from 'inversify-express-utils/lib/results'
import { Result } from '@standardnotes/domain-core'
describe('InversifyExpressFilesController', () => {
let uploadFileChunk: UploadFileChunk
@@ -57,7 +58,7 @@ describe('InversifyExpressFilesController', () => {
getFileMetadata.execute = jest.fn().mockReturnValue({ success: true, size: 555_555 })
removeFile = {} as jest.Mocked<RemoveFile>
removeFile.execute = jest.fn().mockReturnValue({ success: true })
removeFile.execute = jest.fn().mockReturnValue(Result.ok())
request = {
body: {},
@@ -202,7 +203,7 @@ describe('InversifyExpressFilesController', () => {
expect(createUploadSession.execute).toHaveBeenCalledWith({
resourceRemoteIdentifier: '2-3-4',
userUuid: '1-2-3',
ownerUuid: '1-2-3',
})
})
@@ -232,7 +233,8 @@ describe('InversifyExpressFilesController', () => {
expect(finishUploadSession.execute).toHaveBeenCalledWith({
resourceRemoteIdentifier: '2-3-4',
userUuid: '1-2-3',
ownerType: 'user',
ownerUuid: '1-2-3',
})
})
@@ -261,15 +263,17 @@ describe('InversifyExpressFilesController', () => {
await createController().remove(request, response)
expect(removeFile.execute).toHaveBeenCalledWith({
resourceRemoteIdentifier: '2-3-4',
userUuid: '1-2-3',
userInput: {
resourceRemoteIdentifier: '2-3-4',
userUuid: '1-2-3',
},
})
})
it('should return bad request if file removal could not be completed', async () => {
response.locals.permittedOperation = ValetTokenOperation.Delete
removeFile.execute = jest.fn().mockReturnValue({ success: false })
removeFile.execute = jest.fn().mockReturnValue(Result.fail('error'))
const httpResponse = await createController().remove(request, response)
const result = await httpResponse.executeAsync()
@@ -299,7 +303,7 @@ describe('InversifyExpressFilesController', () => {
data: Buffer.from([123]),
resourceRemoteIdentifier: '2-3-4',
resourceUnencryptedFileSize: 123,
userUuid: '1-2-3',
ownerUuid: '1-2-3',
})
})

View File

@@ -35,7 +35,7 @@ export class InversifyExpressFilesController extends BaseHttpController {
}
const result = await this.createUploadSession.execute({
userUuid: response.locals.userUuid,
ownerUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
})
@@ -61,7 +61,7 @@ export class InversifyExpressFilesController extends BaseHttpController {
}
const result = await this.uploadFileChunk.execute({
userUuid: response.locals.userUuid,
ownerUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
resourceUnencryptedFileSize: response.locals.permittedResources[0].unencryptedFileSize,
chunkId,
@@ -85,7 +85,8 @@ export class InversifyExpressFilesController extends BaseHttpController {
}
const result = await this.finishUploadSession.execute({
userUuid: response.locals.userUuid,
ownerUuid: response.locals.userUuid,
ownerType: 'user',
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
uploadBytesLimit: response.locals.uploadBytesLimit,
uploadBytesUsed: response.locals.uploadBytesUsed,
@@ -108,13 +109,15 @@ export class InversifyExpressFilesController extends BaseHttpController {
}
const result = await this.removeFile.execute({
userUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
regularSubscriptionUuid: response.locals.regularSubscriptionUuid,
userInput: {
userUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
regularSubscriptionUuid: response.locals.regularSubscriptionUuid,
},
})
if (!result.success) {
return this.badRequest(result.message)
if (result.isFailed()) {
return this.badRequest(result.getError())
}
return this.json({ success: true, message: 'File removed successfully' })
@@ -140,7 +143,7 @@ export class InversifyExpressFilesController extends BaseHttpController {
}
const fileMetadata = await this.getFileMetadata.execute({
userUuid: response.locals.userUuid,
ownerUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
})
@@ -161,7 +164,7 @@ export class InversifyExpressFilesController extends BaseHttpController {
response.writeHead(206, headers)
const result = await this.streamDownloadFile.execute({
userUuid: response.locals.userUuid,
ownerUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
startRange,
endRange,

View File

@@ -0,0 +1,215 @@
import { BaseHttpController, controller, httpDelete, httpGet, httpPost, results } from 'inversify-express-utils'
import { Request, Response } from 'express'
import { inject } from 'inversify'
import { Writable } from 'stream'
import { SharedVaultValetTokenData, ValetTokenOperation } from '@standardnotes/security'
import TYPES from '../../Bootstrap/Types'
import { CreateUploadSession } from '../../Domain/UseCase/CreateUploadSession/CreateUploadSession'
import { FinishUploadSession } from '../../Domain/UseCase/FinishUploadSession/FinishUploadSession'
import { GetFileMetadata } from '../../Domain/UseCase/GetFileMetadata/GetFileMetadata'
import { MoveFile } from '../../Domain/UseCase/MoveFile/MoveFile'
import { RemoveFile } from '../../Domain/UseCase/RemoveFile/RemoveFile'
import { StreamDownloadFile } from '../../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
import { UploadFileChunk } from '../../Domain/UseCase/UploadFileChunk/UploadFileChunk'
@controller('/v1/shared-vault/files', TYPES.Files_SharedVaultValetTokenAuthMiddleware)
export class InversifyExpressSharedVaultFilesController extends BaseHttpController {
constructor(
@inject(TYPES.Files_UploadFileChunk) private uploadFileChunk: UploadFileChunk,
@inject(TYPES.Files_CreateUploadSession) private createUploadSession: CreateUploadSession,
@inject(TYPES.Files_FinishUploadSession) private finishUploadSession: FinishUploadSession,
@inject(TYPES.Files_StreamDownloadFile) private streamDownloadFile: StreamDownloadFile,
@inject(TYPES.Files_GetFileMetadata) private getFileMetadata: GetFileMetadata,
@inject(TYPES.Files_RemoveFile) private removeFile: RemoveFile,
@inject(TYPES.Files_MoveFile) private moveFile: MoveFile,
@inject(TYPES.Files_MAX_CHUNK_BYTES) private maxChunkBytes: number,
) {
super()
}
@httpPost('/move')
async moveFileRequest(
_request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
const locals = response.locals as SharedVaultValetTokenData
if (locals.permittedOperation !== ValetTokenOperation.Move) {
return this.badRequest('Not permitted for this operation')
}
const moveOperation = locals.moveOperation
if (!moveOperation) {
return this.badRequest('Missing move operation data')
}
const result = await this.moveFile.execute({
moveType: moveOperation.type,
fromUuid: moveOperation.fromUuid,
toUuid: moveOperation.toUuid,
resourceRemoteIdentifier: locals.remoteIdentifier,
})
if (result.isFailed()) {
return this.badRequest(result.getError())
}
return this.json({ success: true })
}
@httpPost('/upload/create-session')
async startUpload(
_request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
const locals = response.locals as SharedVaultValetTokenData
if (locals.permittedOperation !== ValetTokenOperation.Write) {
return this.badRequest('Not permitted for this operation')
}
const result = await this.createUploadSession.execute({
ownerUuid: locals.sharedVaultUuid,
resourceRemoteIdentifier: locals.remoteIdentifier,
})
if (!result.success) {
return this.badRequest(result.message)
}
return this.json({ success: true, uploadId: result.uploadId })
}
@httpPost('/upload/chunk')
async uploadChunk(
request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
const locals = response.locals as SharedVaultValetTokenData
if (locals.permittedOperation !== ValetTokenOperation.Write) {
return this.badRequest('Not permitted for this operation')
}
const chunkId = +(request.headers['x-chunk-id'] as string)
if (!chunkId) {
return this.badRequest('Missing x-chunk-id header in request.')
}
const result = await this.uploadFileChunk.execute({
ownerUuid: locals.sharedVaultUuid,
resourceRemoteIdentifier: locals.remoteIdentifier,
resourceUnencryptedFileSize: locals.unencryptedFileSize as number,
chunkId,
data: request.body,
})
if (!result.success) {
return this.badRequest(result.message)
}
return this.json({ success: true, message: 'Chunk uploaded successfully' })
}
@httpPost('/upload/close-session')
public async finishUpload(
_request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
const locals = response.locals as SharedVaultValetTokenData
if (locals.permittedOperation !== ValetTokenOperation.Write) {
return this.badRequest('Not permitted for this operation')
}
const result = await this.finishUploadSession.execute({
ownerUuid: locals.sharedVaultUuid,
ownerType: 'shared-vault',
resourceRemoteIdentifier: locals.remoteIdentifier,
uploadBytesLimit: locals.uploadBytesLimit,
uploadBytesUsed: locals.uploadBytesUsed,
})
if (!result.success) {
return this.badRequest(result.message)
}
return this.json({ success: true, message: 'File uploaded successfully' })
}
@httpDelete('/')
async remove(
_request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
const locals = response.locals as SharedVaultValetTokenData
if (locals.permittedOperation !== ValetTokenOperation.Delete) {
return this.badRequest('Not permitted for this operation')
}
const result = await this.removeFile.execute({
vaultInput: {
sharedVaultUuid: locals.sharedVaultUuid,
resourceRemoteIdentifier: locals.remoteIdentifier,
},
})
if (result.isFailed()) {
return this.badRequest(result.getError())
}
return this.json({ success: true, message: 'File removed successfully' })
}
@httpGet('/')
async download(
request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | (() => Writable)> {
const locals = response.locals as SharedVaultValetTokenData
if (locals.permittedOperation !== ValetTokenOperation.Read) {
return this.badRequest('Not permitted for this operation')
}
const range = request.headers['range']
if (!range) {
return this.badRequest('File download requires range header to be set.')
}
let chunkSize = +(request.headers['x-chunk-size'] as string)
if (!chunkSize || chunkSize > this.maxChunkBytes) {
chunkSize = this.maxChunkBytes
}
const fileMetadata = await this.getFileMetadata.execute({
ownerUuid: locals.sharedVaultUuid,
resourceRemoteIdentifier: locals.remoteIdentifier,
})
if (!fileMetadata.success) {
return this.badRequest(fileMetadata.message)
}
const startRange = Number(range.replace(/\D/g, ''))
const endRange = Math.min(startRange + chunkSize - 1, fileMetadata.size - 1)
const headers = {
'Content-Range': `bytes ${startRange}-${endRange}/${fileMetadata.size}`,
'Accept-Ranges': 'bytes',
'Content-Length': endRange - startRange + 1,
'Content-Type': 'application/octet-stream',
}
response.writeHead(206, headers)
const result = await this.streamDownloadFile.execute({
ownerUuid: locals.sharedVaultUuid,
resourceRemoteIdentifier: locals.remoteIdentifier,
startRange,
endRange,
})
if (!result.success) {
return this.badRequest(result.message)
}
return () => result.readStream.pipe(response)
}
}

View File

@@ -0,0 +1,81 @@
import { SharedVaultValetTokenData, TokenDecoderInterface } from '@standardnotes/security'
import { Uuid } from '@standardnotes/domain-core'
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 SharedVaultValetTokenAuthMiddleware extends BaseMiddleware {
constructor(
@inject(TYPES.Files_ValetTokenDecoder) private tokenDecoder: TokenDecoderInterface<SharedVaultValetTokenData>,
@inject(TYPES.Files_Logger) private logger: Logger,
) {
super()
}
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
try {
const valetToken = request.headers['x-valet-token'] || request.body.valetToken || request.query.valetToken
if (!valetToken) {
this.logger.debug('SharedVaultValetTokenAuthMiddleware missing valet token.')
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid valet token.',
},
})
return
}
const valetTokenData = this.tokenDecoder.decodeToken(valetToken)
if (valetTokenData === undefined) {
this.logger.debug('SharedVaultValetTokenAuthMiddleware authentication failure.')
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid valet token.',
},
})
return
}
const resourceUuidOrError = Uuid.create(valetTokenData.remoteIdentifier)
if (resourceUuidOrError.isFailed()) {
this.logger.debug('Invalid remote resource identifier in token.')
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid valet token.',
},
})
return
}
const whitelistedData: SharedVaultValetTokenData = {
sharedVaultUuid: valetTokenData.sharedVaultUuid,
remoteIdentifier: valetTokenData.remoteIdentifier,
permittedOperation: valetTokenData.permittedOperation,
uploadBytesUsed: valetTokenData.uploadBytesUsed,
uploadBytesLimit: valetTokenData.uploadBytesLimit,
unencryptedFileSize: valetTokenData.unencryptedFileSize,
moveOperation: valetTokenData.moveOperation,
}
Object.assign(response.locals, whitelistedData)
return next()
} catch (error) {
return next(error)
}
}
}

View File

@@ -0,0 +1,30 @@
import { inject, injectable } from 'inversify'
import { CopyObjectCommand, DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'
import TYPES from '../../Bootstrap/Types'
import { FileMoverInterface } from '../../Domain/Services/FileMoverInterface'
@injectable()
export class S3FileMover implements FileMoverInterface {
constructor(
@inject(TYPES.Files_S3) private s3Client: S3Client,
@inject(TYPES.Files_S3_BUCKET_NAME) private s3BucketName: string,
) {}
async moveFile(sourcePath: string, destinationPath: string): Promise<void> {
await this.s3Client.send(
new CopyObjectCommand({
Bucket: this.s3BucketName,
CopySource: `${this.s3BucketName}/${sourcePath}`,
Key: destinationPath,
}),
)
await this.s3Client.send(
new DeleteObjectCommand({
Bucket: this.s3BucketName,
Key: sourcePath,
}),
)
}
}

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.16](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.15...@standardnotes/home-server@1.11.16) (2023-06-30)
**Note:** Version bump only for package @standardnotes/home-server
## [1.11.15](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.14...@standardnotes/home-server@1.11.15) (2023-06-30)
**Note:** Version bump only for package @standardnotes/home-server
## [1.11.14](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.13...@standardnotes/home-server@1.11.14) (2023-06-28)
**Note:** Version bump only for package @standardnotes/home-server
## [1.11.13](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.12...@standardnotes/home-server@1.11.13) (2023-06-28)
**Note:** Version bump only for package @standardnotes/home-server
## [1.11.12](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.11...@standardnotes/home-server@1.11.12) (2023-06-28)
**Note:** Version bump only for package @standardnotes/home-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.11.12",
"version": "1.11.16",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.23.5](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.23.4...@standardnotes/revisions-server@1.23.5) (2023-06-30)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.23.4](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.23.3...@standardnotes/revisions-server@1.23.4) (2023-06-30)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.23.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.23.2...@standardnotes/revisions-server@1.23.3) (2023-06-28)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

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

Some files were not shown because too many files have changed in this diff Show More