Compare commits

...

32 Commits

Author SHA1 Message Date
standardci
cec2005436 chore(release): publish new version
- @standardnotes/analytics@2.25.12
 - @standardnotes/api-gateway@1.70.4
 - @standardnotes/auth-server@1.131.3
 - @standardnotes/domain-core@1.25.2
 - @standardnotes/event-store@1.11.19
 - @standardnotes/files-server@1.20.3
 - @standardnotes/home-server@1.13.42
 - @standardnotes/revisions-server@1.26.6
 - @standardnotes/scheduler-server@1.20.21
 - @standardnotes/settings@1.21.24
 - @standardnotes/syncing-server@1.78.5
 - @standardnotes/websockets-server@1.10.16
2023-08-09 16:31:35 +00:00
Karol Sójko
0eb86c0096 Revert "tmp: disable fetching shared vault items"
This reverts commit 18eddea6f8.
2023-08-09 18:01:16 +02:00
Karol Sójko
b8e39d76c1 Revert "tmp: skip ci"
This reverts commit f8c9e67063.
2023-08-09 18:01:09 +02:00
Karol Sójko
1c3ff526b7 Revert "Revert "feat(syncing-server): notify shared vault users upon file uploads or removals (#692)""
This reverts commit d261c81cd0.
2023-08-09 18:00:49 +02:00
standardci
373767248c chore(release): publish new version
- @standardnotes/home-server@1.13.41
 - @standardnotes/syncing-server@1.78.4
2023-08-09 15:47:05 +00:00
Karol Sójko
d7965b2748 fix(syncing-server): casting handlers 2023-08-09 17:40:48 +02:00
Karol Sójko
cbcd2ec87a Revert "Revert "fix(syncing-server): update storage quota used in a shared vault (#691)""
This reverts commit 66f9352a06.
2023-08-09 17:36:59 +02:00
standardci
c74d37fc48 chore(release): publish new version
- @standardnotes/home-server@1.13.40
 - @standardnotes/syncing-server@1.78.3
2023-08-09 15:29:30 +00:00
Karol Sójko
66f9352a06 Revert "fix(syncing-server): update storage quota used in a shared vault (#691)"
This reverts commit 3415cae093.
2023-08-09 17:21:59 +02:00
standardci
e5eef3aba0 chore(release): publish new version
- @standardnotes/analytics@2.25.11
 - @standardnotes/api-gateway@1.70.3
 - @standardnotes/auth-server@1.131.2
 - @standardnotes/domain-core@1.25.1
 - @standardnotes/event-store@1.11.18
 - @standardnotes/files-server@1.20.2
 - @standardnotes/home-server@1.13.39
 - @standardnotes/revisions-server@1.26.5
 - @standardnotes/scheduler-server@1.20.20
 - @standardnotes/settings@1.21.23
 - @standardnotes/syncing-server@1.78.2
 - @standardnotes/websockets-server@1.10.15
2023-08-09 14:51:38 +00:00
Karol Sójko
d261c81cd0 Revert "feat(syncing-server): notify shared vault users upon file uploads or removals (#692)"
This reverts commit 46867c1a4d.
2023-08-09 16:43:33 +02:00
standardci
634e3bbb67 chore(release): publish new version
- @standardnotes/home-server@1.13.38
 - @standardnotes/syncing-server@1.78.1
2023-08-09 14:41:32 +00:00
Karol Sójko
f8c9e67063 tmp: skip ci 2023-08-09 16:33:59 +02:00
Karol Sójko
18eddea6f8 tmp: disable fetching shared vault items 2023-08-09 16:17:53 +02:00
standardci
c6d655c5f5 chore(release): publish new version
- @standardnotes/analytics@2.25.10
 - @standardnotes/api-gateway@1.70.2
 - @standardnotes/auth-server@1.131.1
 - @standardnotes/domain-core@1.25.0
 - @standardnotes/event-store@1.11.17
 - @standardnotes/files-server@1.20.1
 - @standardnotes/home-server@1.13.37
 - @standardnotes/revisions-server@1.26.4
 - @standardnotes/scheduler-server@1.20.19
 - @standardnotes/settings@1.21.22
 - @standardnotes/syncing-server@1.78.0
 - @standardnotes/websockets-server@1.10.14
2023-08-09 13:46:50 +00:00
Karol Sójko
46867c1a4d feat(syncing-server): notify shared vault users upon file uploads or removals (#692) 2023-08-09 15:08:17 +02:00
standardci
d29903bab6 chore(release): publish new version
- @standardnotes/home-server@1.13.36
 - @standardnotes/syncing-server@1.77.2
2023-08-09 08:37:21 +00:00
Karol Sójko
3415cae093 fix(syncing-server): update storage quota used in a shared vault (#691) 2023-08-09 10:05:48 +02:00
standardci
408fd5a0c6 chore(release): publish new version
- @standardnotes/home-server@1.13.35
 - @standardnotes/syncing-server@1.77.1
2023-08-08 13:05:40 +00:00
Karol Sójko
0a16ee64fe fix(syncing-server): inviting already existing members to shared vault (#690)
* fix(syncing-server): inviting already existing members to shared vault

* fix(syncing-server): finding method for existing members
2023-08-08 14:31:23 +02:00
standardci
22b00479b4 chore(release): publish new version
- @standardnotes/analytics@2.25.9
 - @standardnotes/api-gateway@1.70.1
 - @standardnotes/auth-server@1.131.0
 - @standardnotes/domain-events-infra@1.12.11
 - @standardnotes/domain-events@2.115.0
 - @standardnotes/event-store@1.11.16
 - @standardnotes/files-server@1.20.0
 - @standardnotes/home-server@1.13.34
 - @standardnotes/revisions-server@1.26.3
 - @standardnotes/scheduler-server@1.20.18
 - @standardnotes/security@1.9.0
 - @standardnotes/syncing-server@1.77.0
 - @standardnotes/websockets-server@1.10.13
2023-08-08 12:06:10 +00:00
Karol Sójko
5311e74266 feat: update storage quota used for user based on shared vault files (#689)
* feat: update storage quota used for user based on shared vault files

* fix: use case binding

* fix: increase file upload bytes limit for shared vaults
2023-08-08 13:36:35 +02:00
standardci
5be7db7788 chore(release): publish new version
- @standardnotes/home-server@1.13.33
 - @standardnotes/syncing-server@1.76.1
2023-08-08 09:29:54 +00:00
Karol Sójko
3bd1547ce3 fix(syncing-server): race condition when adding admin user to newly created shared vault (#688) 2023-08-08 11:02:10 +02:00
standardci
a1fe15f7a9 chore(release): publish new version
- @standardnotes/api-gateway@1.70.0
 - @standardnotes/home-server@1.13.32
 - @standardnotes/syncing-server@1.76.0
2023-08-07 16:09:21 +00:00
Karol Sójko
19b8921f28 feat(syncing-server): limit shared vaults creation based on role (#687)
* feat(syncing-server): limit shared vaults creation based on role

* fix: add role names emptyness validation

* fix: roles passing to response locals
2023-08-07 17:35:47 +02:00
standardci
6b7879ba15 chore(release): publish new version
- @standardnotes/auth-server@1.130.1
 - @standardnotes/home-server@1.13.31
2023-08-07 11:50:26 +00:00
Karol Sójko
bd5f492a73 fix(auth): update user agent upon refreshing session token (#685) 2023-08-07 13:21:21 +02:00
standardci
67311cc002 chore(release): publish new version
- @standardnotes/auth-server@1.130.0
 - @standardnotes/home-server@1.13.30
2023-08-07 08:32:08 +00:00
Karol Sójko
f39d3aca5b feat(auth): invalidate other sessions for user if the email or password are changed (#684)
* feat(auth): invalidate other sessions for user if the email or password are changed

* fix(auth): handling credentials change in a legacy protocol scenario

* fix(auth): leave only the newly created session when changing credentials
2023-08-07 10:02:47 +02:00
standardci
8e47491e3c chore(release): publish new version
- @standardnotes/home-server@1.13.29
 - @standardnotes/syncing-server@1.75.4
2023-08-03 13:38:37 +00:00
Karol Sójko
0036d527bd fix(syncing-server): skip retrieval of items with invalid uuids (#683) 2023-08-03 15:05:59 +02:00
132 changed files with 1976 additions and 1123 deletions

View File

@@ -19,7 +19,8 @@
"publish": "lerna publish from-git --yes --no-verify-access --loglevel verbose",
"postversion": "./scripts/push-tags-one-by-one.sh",
"upgrade:snjs": "yarn workspaces foreach --verbose run upgrade:snjs",
"e2e": "yarn build packages/home-server && PORT=3123 yarn workspace @standardnotes/home-server start"
"e2e": "yarn build packages/home-server && PORT=3123 yarn workspace @standardnotes/home-server start",
"start": "yarn build packages/home-server && yarn workspace @standardnotes/home-server start"
},
"devDependencies": {
"@commitlint/cli": "^17.0.2",

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.
## [2.25.12](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.11...@standardnotes/analytics@2.25.12) (2023-08-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.11](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.10...@standardnotes/analytics@2.25.11) (2023-08-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.10](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.9...@standardnotes/analytics@2.25.10) (2023-08-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.8...@standardnotes/analytics@2.25.9) (2023-08-08)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.7...@standardnotes/analytics@2.25.8) (2023-08-03)
**Note:** Version bump only for package @standardnotes/analytics

View File

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

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.70.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.3...@standardnotes/api-gateway@1.70.4) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.2...@standardnotes/api-gateway@1.70.3) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.1...@standardnotes/api-gateway@1.70.2) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.0...@standardnotes/api-gateway@1.70.1) (2023-08-08)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.70.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.69.3...@standardnotes/api-gateway@1.70.0) (2023-08-07)
### Features
* **syncing-server:** limit shared vaults creation based on role ([#687](https://github.com/standardnotes/api-gateway/issues/687)) ([19b8921](https://github.com/standardnotes/api-gateway/commit/19b8921f286ff8f88c427e8ddd4512a8d61edb4f))
## [1.69.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.69.2...@standardnotes/api-gateway@1.69.3) (2023-08-03)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

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

View File

@@ -1,5 +1,4 @@
import { CrossServiceTokenData } from '@standardnotes/security'
import { RoleName } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { NextFunction, Request, Response } from 'express'
import { BaseMiddleware } from 'inversify-express-utils'
@@ -51,10 +50,6 @@ export abstract class AuthMiddleware extends BaseMiddleware {
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
response.locals.freeUser =
decodedToken.roles.length === 1 &&
decodedToken.roles.find((role) => role.name === RoleName.NAMES.CoreUser) !== undefined
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
await this.crossServiceTokenCache.set({
authorizationHeaderValue: authHeaderValue,

View File

@@ -1,5 +1,4 @@
import { CrossServiceTokenData } from '@standardnotes/security'
import { RoleName } from '@standardnotes/domain-core'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils'
@@ -60,9 +59,6 @@ export class WebSocketAuthMiddleware extends BaseMiddleware {
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
response.locals.freeUser =
decodedToken.roles.length === 1 &&
decodedToken.roles.find((role) => role.name === RoleName.NAMES.CoreUser) !== undefined
response.locals.user = decodedToken.user
response.locals.roles = decodedToken.roles
} catch (error) {

View File

@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.131.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.2...@standardnotes/auth-server@1.131.3) (2023-08-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.131.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.1...@standardnotes/auth-server@1.131.2) (2023-08-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.131.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.0...@standardnotes/auth-server@1.131.1) (2023-08-09)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.131.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.130.1...@standardnotes/auth-server@1.131.0) (2023-08-08)
### Features
* update storage quota used for user based on shared vault files ([#689](https://github.com/standardnotes/server/issues/689)) ([5311e74](https://github.com/standardnotes/server/commit/5311e7426617da6fc75593dd0fcbff589ca4fc22))
## [1.130.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.130.0...@standardnotes/auth-server@1.130.1) (2023-08-07)
### Bug Fixes
* **auth:** update user agent upon refreshing session token ([#685](https://github.com/standardnotes/server/issues/685)) ([bd5f492](https://github.com/standardnotes/server/commit/bd5f492a733f783c64fa4bc5840b4a9f5c913d3d))
# [1.130.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.129.0...@standardnotes/auth-server@1.130.0) (2023-08-07)
### Features
* **auth:** invalidate other sessions for user if the email or password are changed ([#684](https://github.com/standardnotes/server/issues/684)) ([f39d3ac](https://github.com/standardnotes/server/commit/f39d3aca5b7bb9e5f9c1c24cbe2359f30dea835c))
# [1.129.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.128.1...@standardnotes/auth-server@1.129.0) (2023-08-03)
### Features

View File

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

View File

@@ -38,7 +38,7 @@ import { GetUserKeyParams } from '../Domain/UseCase/GetUserKeyParams/GetUserKeyP
import { UpdateUser } from '../Domain/UseCase/UpdateUser'
import { RedisEphemeralSessionRepository } from '../Infra/Redis/RedisEphemeralSessionRepository'
import { GetActiveSessionsForUser } from '../Domain/UseCase/GetActiveSessionsForUser'
import { DeletePreviousSessionsForUser } from '../Domain/UseCase/DeletePreviousSessionsForUser'
import { DeleteOtherSessionsForUser } from '../Domain/UseCase/DeleteOtherSessionsForUser'
import { DeleteSessionForUser } from '../Domain/UseCase/DeleteSessionForUser'
import { Register } from '../Domain/UseCase/Register'
import { LockRepository } from '../Infra/Redis/LockRepository'
@@ -253,6 +253,9 @@ import { BaseSessionsController } from '../Infra/InversifyExpressUtils/Base/Base
import { Transform } from 'stream'
import { ActivatePremiumFeatures } from '../Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures'
import { PaymentsAccountDeletedEventHandler } from '../Domain/Handler/PaymentsAccountDeletedEventHandler'
import { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
export class ContainerConfigLoader {
async load(configuration?: {
@@ -827,9 +830,7 @@ export class ContainerConfigLoader {
container.bind<UpdateUser>(TYPES.Auth_UpdateUser).to(UpdateUser)
container.bind<Register>(TYPES.Auth_Register).to(Register)
container.bind<GetActiveSessionsForUser>(TYPES.Auth_GetActiveSessionsForUser).to(GetActiveSessionsForUser)
container
.bind<DeletePreviousSessionsForUser>(TYPES.Auth_DeletePreviousSessionsForUser)
.to(DeletePreviousSessionsForUser)
container.bind<DeleteOtherSessionsForUser>(TYPES.Auth_DeleteOtherSessionsForUser).to(DeleteOtherSessionsForUser)
container.bind<DeleteSessionForUser>(TYPES.Auth_DeleteSessionForUser).to(DeleteSessionForUser)
container.bind<ChangeCredentials>(TYPES.Auth_ChangeCredentials).to(ChangeCredentials)
container.bind<GetSettings>(TYPES.Auth_GetSettings).to(GetSettings)
@@ -884,6 +885,15 @@ export class ContainerConfigLoader {
container.bind<VerifyPredicate>(TYPES.Auth_VerifyPredicate).to(VerifyPredicate)
container.bind<CreateCrossServiceToken>(TYPES.Auth_CreateCrossServiceToken).to(CreateCrossServiceToken)
container.bind<ProcessUserRequest>(TYPES.Auth_ProcessUserRequest).to(ProcessUserRequest)
container
.bind<UpdateStorageQuotaUsedForUser>(TYPES.Auth_UpdateStorageQuotaUsedForUser)
.toConstantValue(
new UpdateStorageQuotaUsedForUser(
container.get(TYPES.Auth_UserRepository),
container.get(TYPES.Auth_UserSubscriptionService),
container.get(TYPES.Auth_SubscriptionSettingService),
),
)
// Controller
container
@@ -953,8 +963,38 @@ export class ContainerConfigLoader {
container
.bind<UserEmailChangedEventHandler>(TYPES.Auth_UserEmailChangedEventHandler)
.to(UserEmailChangedEventHandler)
container.bind<FileUploadedEventHandler>(TYPES.Auth_FileUploadedEventHandler).to(FileUploadedEventHandler)
container.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler).to(FileRemovedEventHandler)
container
.bind<FileUploadedEventHandler>(TYPES.Auth_FileUploadedEventHandler)
.toConstantValue(
new FileUploadedEventHandler(
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
container.get(TYPES.Auth_Logger),
),
)
container
.bind<SharedVaultFileUploadedEventHandler>(TYPES.Auth_SharedVaultFileUploadedEventHandler)
.toConstantValue(
new SharedVaultFileUploadedEventHandler(
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
container.get(TYPES.Auth_Logger),
),
)
container
.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler)
.toConstantValue(
new FileRemovedEventHandler(
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
container.get(TYPES.Auth_Logger),
),
)
container
.bind<SharedVaultFileRemovedEventHandler>(TYPES.Auth_SharedVaultFileRemovedEventHandler)
.toConstantValue(
new SharedVaultFileRemovedEventHandler(
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
container.get(TYPES.Auth_Logger),
),
)
container
.bind<ListedAccountCreatedEventHandler>(TYPES.Auth_ListedAccountCreatedEventHandler)
.to(ListedAccountCreatedEventHandler)
@@ -1001,7 +1041,9 @@ export class ContainerConfigLoader {
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.Auth_SubscriptionReassignedEventHandler)],
['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)],
['FILE_REMOVED', container.get(TYPES.Auth_FileRemovedEventHandler)],
['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)],
['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)],
['LISTED_ACCOUNT_DELETED', container.get(TYPES.Auth_ListedAccountDeletedEventHandler)],
[
@@ -1178,7 +1220,7 @@ export class ContainerConfigLoader {
.toConstantValue(
new BaseSessionController(
container.get(TYPES.Auth_DeleteSessionForUser),
container.get(TYPES.Auth_DeletePreviousSessionsForUser),
container.get(TYPES.Auth_DeleteOtherSessionsForUser),
container.get(TYPES.Auth_RefreshSessionToken),
container.get(TYPES.Auth_ControllerContainer),
),

View File

@@ -113,7 +113,7 @@ const TYPES = {
Auth_UpdateUser: Symbol.for('Auth_UpdateUser'),
Auth_Register: Symbol.for('Auth_Register'),
Auth_GetActiveSessionsForUser: Symbol.for('Auth_GetActiveSessionsForUser'),
Auth_DeletePreviousSessionsForUser: Symbol.for('Auth_DeletePreviousSessionsForUser'),
Auth_DeleteOtherSessionsForUser: Symbol.for('Auth_DeleteOtherSessionsForUser'),
Auth_DeleteSessionForUser: Symbol.for('Auth_DeleteSessionForUser'),
Auth_ChangeCredentials: Symbol.for('Auth_ChangePassword'),
Auth_GetSettings: Symbol.for('Auth_GetSettings'),
@@ -152,6 +152,7 @@ const TYPES = {
Auth_ActivatePremiumFeatures: Symbol.for('Auth_ActivatePremiumFeatures'),
Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'),
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
// Handlers
Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'),
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
@@ -165,7 +166,9 @@ const TYPES = {
Auth_ExtensionKeyGrantedEventHandler: Symbol.for('Auth_ExtensionKeyGrantedEventHandler'),
Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'),
Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'),
Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'),
Auth_ListedAccountDeletedEventHandler: Symbol.for('Auth_ListedAccountDeletedEventHandler'),
Auth_UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for(

View File

@@ -30,7 +30,7 @@ describe('AuthResponseFactory20161215', () => {
})
it('should create a 20161215 auth response', async () => {
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20161215',
userAgent: 'Google Chrome',
@@ -38,7 +38,7 @@ describe('AuthResponseFactory20161215', () => {
readonlyAccess: false,
})
expect(response).toEqual({
expect(result.response).toEqual({
user: { foo: 'bar' },
token: 'foobar',
})

View File

@@ -11,6 +11,7 @@ import { User } from '../User/User'
import { AuthResponse20161215 } from './AuthResponse20161215'
import { AuthResponse20200115 } from './AuthResponse20200115'
import { AuthResponseFactoryInterface } from './AuthResponseFactoryInterface'
import { Session } from '../Session/Session'
@injectable()
export class AuthResponseFactory20161215 implements AuthResponseFactoryInterface {
@@ -26,7 +27,7 @@ export class AuthResponseFactory20161215 implements AuthResponseFactoryInterface
userAgent: string
ephemeralSession: boolean
readonlyAccess: boolean
}): Promise<AuthResponse20161215 | AuthResponse20200115> {
}): Promise<{ response: AuthResponse20161215 | AuthResponse20200115; session?: Session }> {
this.logger.debug(`Creating JWT auth response for user ${dto.user.uuid}`)
const data: SessionTokenData = {
@@ -39,12 +40,14 @@ export class AuthResponseFactory20161215 implements AuthResponseFactoryInterface
this.logger.debug(`Created JWT token for user ${dto.user.uuid}: ${token}`)
return {
user: this.userProjector.projectSimple(dto.user) as {
uuid: string
email: string
protocolVersion: ProtocolVersion
response: {
user: this.userProjector.projectSimple(dto.user) as {
uuid: string
email: string
protocolVersion: ProtocolVersion
},
token,
},
token,
}
}
}

View File

@@ -29,7 +29,7 @@ describe('AuthResponseFactory20190520', () => {
})
it('should create a 20161215 auth response', async () => {
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20161215',
userAgent: 'Google Chrome',
@@ -37,7 +37,7 @@ describe('AuthResponseFactory20190520', () => {
readonlyAccess: false,
})
expect(response).toEqual({
expect(result.response).toEqual({
user: { foo: 'bar' },
token: 'foobar',
})

View File

@@ -11,6 +11,7 @@ import { User } from '../User/User'
import { AuthResponseFactory20200115 } from './AuthResponseFactory20200115'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { Session } from '../Session/Session'
describe('AuthResponseFactory20200115', () => {
let sessionService: SessionServiceInterface
@@ -48,8 +49,12 @@ describe('AuthResponseFactory20200115', () => {
}
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.createNewSessionForUser = jest.fn().mockReturnValue(sessionPayload)
sessionService.createNewEphemeralSessionForUser = jest.fn().mockReturnValue(sessionPayload)
sessionService.createNewSessionForUser = jest
.fn()
.mockReturnValue({ sessionHttpRepresentation: sessionPayload, session: {} as jest.Mocked<Session> })
sessionService.createNewEphemeralSessionForUser = jest
.fn()
.mockReturnValue({ sessionHttpRepresentation: sessionPayload, session: {} as jest.Mocked<Session> })
keyParamsFactory = {} as jest.Mocked<KeyParamsFactoryInterface>
keyParamsFactory.create = jest.fn().mockReturnValue({
@@ -76,7 +81,7 @@ describe('AuthResponseFactory20200115', () => {
it('should create a 20161215 auth response if user does not support sessions', async () => {
user.supportsSessions = jest.fn().mockReturnValue(false)
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20161215',
userAgent: 'Google Chrome',
@@ -84,7 +89,7 @@ describe('AuthResponseFactory20200115', () => {
readonlyAccess: false,
})
expect(response).toEqual({
expect(result.response).toEqual({
user: { foo: 'bar' },
token: expect.any(String),
})
@@ -93,7 +98,7 @@ describe('AuthResponseFactory20200115', () => {
it('should create a 20200115 auth response', async () => {
user.supportsSessions = jest.fn().mockReturnValue(true)
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20200115',
userAgent: 'Google Chrome',
@@ -101,7 +106,7 @@ describe('AuthResponseFactory20200115', () => {
readonlyAccess: false,
})
expect(response).toEqual({
expect(result.response).toEqual({
key_params: {
key1: 'value1',
key2: 'value2',
@@ -124,7 +129,7 @@ describe('AuthResponseFactory20200115', () => {
domainEventPublisher.publish = jest.fn().mockRejectedValue(new Error('test'))
user.supportsSessions = jest.fn().mockReturnValue(true)
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20200115',
userAgent: 'Google Chrome',
@@ -132,7 +137,7 @@ describe('AuthResponseFactory20200115', () => {
readonlyAccess: false,
})
expect(response).toEqual({
expect(result.response).toEqual({
key_params: {
key1: 'value1',
key2: 'value2',
@@ -153,7 +158,7 @@ describe('AuthResponseFactory20200115', () => {
it('should create a 20200115 auth response with an ephemeral session', async () => {
user.supportsSessions = jest.fn().mockReturnValue(true)
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20200115',
userAgent: 'Google Chrome',
@@ -161,7 +166,7 @@ describe('AuthResponseFactory20200115', () => {
readonlyAccess: false,
})
expect(response).toEqual({
expect(result.response).toEqual({
key_params: {
key1: 'value1',
key2: 'value2',
@@ -183,11 +188,14 @@ describe('AuthResponseFactory20200115', () => {
user.supportsSessions = jest.fn().mockReturnValue(true)
sessionService.createNewSessionForUser = jest.fn().mockReturnValue({
...sessionPayload,
readonly_access: true,
sessionHttpRepresentation: {
...sessionPayload,
readonly_access: true,
},
session: {} as jest.Mocked<Session>,
})
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20200115',
userAgent: 'Google Chrome',
@@ -195,7 +203,7 @@ describe('AuthResponseFactory20200115', () => {
readonlyAccess: true,
})
expect(response).toEqual({
expect(result.response).toEqual({
key_params: {
key1: 'value1',
key2: 'value2',

View File

@@ -19,6 +19,7 @@ import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterfac
import { AuthResponse20161215 } from './AuthResponse20161215'
import { AuthResponse20200115 } from './AuthResponse20200115'
import { Session } from '../Session/Session'
@injectable()
export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
@@ -40,21 +41,28 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
userAgent: string
ephemeralSession: boolean
readonlyAccess: boolean
}): Promise<AuthResponse20161215 | AuthResponse20200115> {
}): Promise<{ response: AuthResponse20161215 | AuthResponse20200115; session?: Session }> {
if (!dto.user.supportsSessions()) {
this.logger.debug(`User ${dto.user.uuid} does not support sessions. Falling back to JWT auth response`)
return super.createResponse(dto)
}
const sessionPayload = await this.createSession(dto)
const sessionCreationResult = await this.createSession(dto)
this.logger.debug('Created session payload for user %s: %O', dto.user.uuid, sessionPayload)
this.logger.debug(
'Created session payload for user %s: %O',
dto.user.uuid,
sessionCreationResult.sessionHttpRepresentation,
)
return {
session: sessionPayload,
key_params: this.keyParamsFactory.create(dto.user, true),
user: this.userProjector.projectSimple(dto.user) as SimpleUserProjection,
response: {
session: sessionCreationResult.sessionHttpRepresentation,
key_params: this.keyParamsFactory.create(dto.user, true),
user: this.userProjector.projectSimple(dto.user) as SimpleUserProjection,
},
session: sessionCreationResult.session,
}
}
@@ -64,12 +72,12 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
userAgent: string
ephemeralSession: boolean
readonlyAccess: boolean
}): Promise<SessionBody> {
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }> {
if (dto.ephemeralSession) {
return this.sessionService.createNewEphemeralSessionForUser(dto)
}
const session = this.sessionService.createNewSessionForUser(dto)
const sessionCreationResult = await this.sessionService.createNewSessionForUser(dto)
try {
await this.domainEventPublisher.publish(
@@ -79,6 +87,6 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
this.logger.error(`Failed to publish session created event: ${(error as Error).message}`)
}
return session
return sessionCreationResult
}
}

View File

@@ -1,3 +1,4 @@
import { Session } from '../Session/Session'
import { User } from '../User/User'
import { AuthResponse20161215 } from './AuthResponse20161215'
import { AuthResponse20200115 } from './AuthResponse20200115'
@@ -9,5 +10,5 @@ export interface AuthResponseFactoryInterface {
userAgent: string
ephemeralSession: boolean
readonlyAccess: boolean
}): Promise<AuthResponse20161215 | AuthResponse20200115>
}): Promise<{ response: AuthResponse20161215 | AuthResponse20200115; session?: Session }>
}

View File

@@ -40,7 +40,7 @@ describe('AuthenticationMethodResolver', () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.getSessionFromToken = jest.fn()
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session: undefined, isEphemeral: false })
sessionService.getRevokedSessionFromToken = jest.fn()
sessionService.markRevokedSessionAsReceived = jest.fn().mockReturnValue(revokedSession)
@@ -70,7 +70,7 @@ describe('AuthenticationMethodResolver', () => {
})
it('should resolve session authentication method', async () => {
sessionService.getSessionFromToken = jest.fn().mockReturnValue(session)
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false })
expect(await createResolver().resolve('test')).toEqual({
session,
@@ -80,7 +80,9 @@ describe('AuthenticationMethodResolver', () => {
})
it('should not resolve session authentication method with invalid user uuid on session', async () => {
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ userUuid: 'invalid' })
sessionService.getSessionFromToken = jest
.fn()
.mockReturnValue({ session: { userUuid: 'invalid' }, isEphemeral: false })
expect(await createResolver().resolve('test')).toBeUndefined
})

View File

@@ -43,7 +43,7 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
}
}
const session = await this.sessionService.getSessionFromToken(token)
const { session } = await this.sessionService.getSessionFromToken(token)
if (session) {
this.logger.debug('Token decoded successfully. Session found.')

View File

@@ -1,150 +0,0 @@
import 'reflect-metadata'
import { FileRemovedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { User } from '../User/User'
import { FileRemovedEventHandler } from './FileRemovedEventHandler'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
import { UserSubscription } from '../Subscription/UserSubscription'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
describe('FileRemovedEventHandler', () => {
let userSubscriptionService: UserSubscriptionServiceInterface
let logger: Logger
let regularUser: User
let sharedUser: User
let event: FileRemovedEvent
let subscriptionSettingService: SubscriptionSettingServiceInterface
let regularSubscription: UserSubscription
let sharedSubscription: UserSubscription
const createHandler = () => new FileRemovedEventHandler(userSubscriptionService, subscriptionSettingService, logger)
beforeEach(() => {
regularUser = {
uuid: '123',
} as jest.Mocked<User>
sharedUser = {
uuid: '234',
} as jest.Mocked<User>
regularSubscription = {
uuid: '1-2-3',
subscriptionType: UserSubscriptionType.Regular,
user: Promise.resolve(regularUser),
} as jest.Mocked<UserSubscription>
sharedSubscription = {
uuid: '2-3-4',
subscriptionType: UserSubscriptionType.Shared,
user: Promise.resolve(sharedUser),
} as jest.Mocked<UserSubscription>
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription: null })
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
subscriptionSettingService.createOrReplace = jest.fn()
event = {} as jest.Mocked<FileRemovedEvent>
event.createdAt = new Date(1)
event.payload = {
userUuid: '1-2-3',
fileByteSize: 123,
filePath: '1-2-3/2-3-4',
fileName: '2-3-4',
regularSubscriptionUuid: '4-5-6',
}
logger = {} as jest.Mocked<Logger>
logger.warn = jest.fn()
})
it('should do nothing a bytes used setting does not exist', async () => {
await createHandler().handle(event)
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
})
it('should not do anything if a user subscription is not found', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
value: 345,
})
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
await createHandler().handle(event)
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
})
it('should update a bytes used setting', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
value: 345,
})
await createHandler().handle(event)
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
props: {
name: 'FILE_UPLOAD_BYTES_USED',
sensitive: false,
unencryptedValue: '222',
serverEncryptionVersion: 0,
},
user: regularUser,
userSubscription: {
uuid: '1-2-3',
subscriptionType: 'regular',
user: Promise.resolve(regularUser),
},
})
})
it('should update a bytes used setting on both shared and regular subscription', async () => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription })
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
value: 345,
})
await createHandler().handle(event)
expect(subscriptionSettingService.createOrReplace).toHaveBeenNthCalledWith(1, {
props: {
name: 'FILE_UPLOAD_BYTES_USED',
sensitive: false,
unencryptedValue: '222',
serverEncryptionVersion: 0,
},
user: regularUser,
userSubscription: {
uuid: '1-2-3',
subscriptionType: 'regular',
user: Promise.resolve(regularUser),
},
})
expect(subscriptionSettingService.createOrReplace).toHaveBeenNthCalledWith(2, {
props: {
name: 'FILE_UPLOAD_BYTES_USED',
sensitive: false,
unencryptedValue: '222',
serverEncryptionVersion: 0,
},
user: sharedUser,
userSubscription: {
uuid: '2-3-4',
subscriptionType: 'shared',
user: Promise.resolve(sharedUser),
},
})
})
})

View File

@@ -1,63 +1,19 @@
import { DomainEventHandlerInterface, FileRemovedEvent } from '@standardnotes/domain-events'
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
import { UserSubscription } from '../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
@injectable()
export class FileRemovedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
@inject(TYPES.Auth_SubscriptionSettingService)
private subscriptionSettingService: SubscriptionSettingServiceInterface,
@inject(TYPES.Auth_Logger) private logger: Logger,
) {}
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
async handle(event: FileRemovedEvent): Promise<void> {
const { regularSubscription, sharedSubscription } =
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(event.payload.userUuid)
if (regularSubscription === null) {
this.logger.warn(`Could not find regular user subscription for user with uuid: ${event.payload.userUuid}`)
return
}
await this.updateUploadBytesUsedSetting(regularSubscription, event.payload.fileByteSize)
if (sharedSubscription !== null) {
await this.updateUploadBytesUsedSetting(sharedSubscription, event.payload.fileByteSize)
}
}
private async updateUploadBytesUsedSetting(subscription: UserSubscription, byteSize: number): Promise<void> {
const user = await subscription.user
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: user.uuid,
userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
userUuid: event.payload.userUuid,
bytesUsed: -event.payload.fileByteSize,
})
if (bytesUsedSetting === null) {
this.logger.warn(`Could not find bytes used setting for user with uuid: ${user.uuid}`)
return
if (result.isFailed()) {
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
}
const bytesUsed = bytesUsedSetting.value as string
await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription,
user,
props: {
name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesUsed - byteSize).toString(),
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
},
})
}
}

View File

@@ -1,82 +1,19 @@
import { DomainEventHandlerInterface, FileUploadedEvent } from '@standardnotes/domain-events'
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
import { UserSubscription } from '../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { User } from '../User/User'
import { Uuid } from '@standardnotes/domain-core'
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
@injectable()
export class FileUploadedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
@inject(TYPES.Auth_SubscriptionSettingService)
private subscriptionSettingService: SubscriptionSettingServiceInterface,
@inject(TYPES.Auth_Logger) private logger: Logger,
) {}
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
async handle(event: FileUploadedEvent): Promise<void> {
const userUuidOrError = Uuid.create(event.payload.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.warn(userUuidOrError.getError())
return
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
return
}
const { regularSubscription, sharedSubscription } =
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(userUuid.value)
if (regularSubscription === null) {
this.logger.warn(`Could not find regular user subscription for user with uuid: ${userUuid.value}`)
return
}
await this.updateUploadBytesUsedSetting(regularSubscription, user, event.payload.fileByteSize)
if (sharedSubscription !== null) {
await this.updateUploadBytesUsedSetting(sharedSubscription, user, event.payload.fileByteSize)
}
}
private async updateUploadBytesUsedSetting(
subscription: UserSubscription,
user: User,
byteSize: number,
): Promise<void> {
let bytesUsed = '0'
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: (await subscription.user).uuid,
userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
userUuid: event.payload.userUuid,
bytesUsed: event.payload.fileByteSize,
})
if (bytesUsedSetting !== null) {
bytesUsed = bytesUsedSetting.value as string
}
await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription,
user,
props: {
name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesUsed + byteSize).toString(),
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
},
})
if (result.isFailed()) {
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
}
}
}

View File

@@ -0,0 +1,19 @@
import { DomainEventHandlerInterface, SharedVaultFileRemovedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
export class SharedVaultFileRemovedEventHandler implements DomainEventHandlerInterface {
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
async handle(event: SharedVaultFileRemovedEvent): Promise<void> {
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
userUuid: event.payload.vaultOwnerUuid,
bytesUsed: -event.payload.fileByteSize,
})
if (result.isFailed()) {
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
}
}
}

View File

@@ -0,0 +1,19 @@
import { DomainEventHandlerInterface, SharedVaultFileUploadedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
export class SharedVaultFileUploadedEventHandler implements DomainEventHandlerInterface {
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
async handle(event: SharedVaultFileUploadedEvent): Promise<void> {
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
userUuid: event.payload.vaultOwnerUuid,
bytesUsed: event.payload.fileByteSize,
})
if (result.isFailed()) {
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
}
}
}

View File

@@ -4,13 +4,6 @@ export interface EphemeralSessionRepositoryInterface {
findOneByUuid(uuid: string): Promise<EphemeralSession | null>
findOneByUuidAndUserUuid(uuid: string, userUuid: string): Promise<EphemeralSession | null>
findAllByUserUuid(userUuid: string): Promise<Array<EphemeralSession>>
updateTokensAndExpirationDates(
uuid: string,
hashedAccessToken: string,
hashedRefreshToken: string,
accessExpiration: Date,
refreshExpiration: Date,
): Promise<void>
deleteOne(uuid: string, userUuid: string): Promise<void>
save(ephemeralSession: EphemeralSession): Promise<void>
}

View File

@@ -1,3 +1,5 @@
import { Uuid } from '@standardnotes/domain-core'
import { Session } from './Session'
export interface SessionRepositoryInterface {
@@ -5,10 +7,8 @@ export interface SessionRepositoryInterface {
findOneByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Session | null>
findAllByRefreshExpirationAndUserUuid(userUuid: string): Promise<Array<Session>>
findAllByUserUuid(userUuid: string): Promise<Array<Session>>
deleteAllByUserUuid(userUuid: string, currentSessionUuid: string): Promise<void>
deleteAllByUserUuidExceptOne(dto: { userUuid: Uuid; currentSessionUuid: Uuid }): Promise<void>
deleteOneByUuid(uuid: string): Promise<void>
updateHashedTokens(uuid: string, hashedAccessToken: string, hashedRefreshToken: string): Promise<void>
updatedTokenExpirationDates(uuid: string, accessExpiration: Date, refreshExpiration: Date): Promise<void>
save(session: Session): Promise<Session>
remove(session: Session): Promise<Session>
clearUserAgentByUserUuid(userUuid: string): Promise<void>

View File

@@ -24,8 +24,8 @@ describe('SessionService', () => {
let sessionRepository: SessionRepositoryInterface
let ephemeralSessionRepository: EphemeralSessionRepositoryInterface
let revokedSessionRepository: RevokedSessionRepositoryInterface
let session: Session
let ephemeralSession: EphemeralSession
let existingSession: Session
let existingEphemeralSession: EphemeralSession
let revokedSession: RevokedSession
let settingService: SettingServiceInterface
let deviceDetector: UAParser
@@ -54,14 +54,14 @@ describe('SessionService', () => {
)
beforeEach(() => {
session = {} as jest.Mocked<Session>
session.uuid = '2e1e43'
session.userUuid = '1-2-3'
session.userAgent = 'Chrome'
session.apiVersion = ApiVersion.v20200115
session.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
session.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
session.readonlyAccess = false
existingSession = {} as jest.Mocked<Session>
existingSession.uuid = '2e1e43'
existingSession.userUuid = '1-2-3'
existingSession.userAgent = 'Chrome'
existingSession.apiVersion = ApiVersion.v20200115
existingSession.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
existingSession.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
existingSession.readonlyAccess = false
revokedSession = {} as jest.Mocked<RevokedSession>
revokedSession.uuid = '2e1e43'
@@ -69,9 +69,7 @@ describe('SessionService', () => {
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
sessionRepository.deleteOneByUuid = jest.fn()
sessionRepository.save = jest.fn().mockReturnValue(session)
sessionRepository.updateHashedTokens = jest.fn()
sessionRepository.updatedTokenExpirationDates = jest.fn()
sessionRepository.save = jest.fn().mockReturnValue(existingSession)
settingService = {} as jest.Mocked<SettingServiceInterface>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
@@ -79,17 +77,18 @@ describe('SessionService', () => {
ephemeralSessionRepository = {} as jest.Mocked<EphemeralSessionRepositoryInterface>
ephemeralSessionRepository.save = jest.fn()
ephemeralSessionRepository.findOneByUuid = jest.fn()
ephemeralSessionRepository.updateTokensAndExpirationDates = jest.fn()
ephemeralSessionRepository.deleteOne = jest.fn()
revokedSessionRepository = {} as jest.Mocked<RevokedSessionRepositoryInterface>
revokedSessionRepository.save = jest.fn()
ephemeralSession = {} as jest.Mocked<EphemeralSession>
ephemeralSession.uuid = '2-3-4'
ephemeralSession.userAgent = 'Mozilla Firefox'
ephemeralSession.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
ephemeralSession.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
existingEphemeralSession = {} as jest.Mocked<EphemeralSession>
existingEphemeralSession.uuid = '2-3-4'
existingEphemeralSession.userUuid = '1-2-3'
existingEphemeralSession.userAgent = 'Mozilla Firefox'
existingEphemeralSession.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
existingEphemeralSession.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
existingEphemeralSession.readonlyAccess = false
timer = {} as jest.Mocked<TimerInterface>
timer.convertStringDateToMilliseconds = jest.fn().mockReturnValue(123)
@@ -138,7 +137,7 @@ describe('SessionService', () => {
})
it('should refresh access and refresh tokens for a session', async () => {
expect(await createService().refreshTokens(session)).toEqual({
expect(await createService().refreshTokens({ session: existingSession, isEphemeral: false })).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_token: expect.any(String),
@@ -146,15 +145,28 @@ describe('SessionService', () => {
readonly_access: false,
})
expect(sessionRepository.updateHashedTokens).toHaveBeenCalled()
expect(sessionRepository.updatedTokenExpirationDates).toHaveBeenCalled()
expect(sessionRepository.save).toHaveBeenCalled()
expect(ephemeralSessionRepository.save).not.toHaveBeenCalled()
})
it('should refresh access and refresh tokens for an ephemeral session', async () => {
expect(await createService().refreshTokens({ session: existingEphemeralSession, isEphemeral: true })).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_token: expect.any(String),
refresh_expiration: 123,
readonly_access: false,
})
expect(sessionRepository.save).not.toHaveBeenCalled()
expect(ephemeralSessionRepository.save).toHaveBeenCalled()
})
it('should create new session for a user', async () => {
const user = {} as jest.Mocked<User>
user.uuid = '123'
const sessionPayload = await createService().createNewSessionForUser({
const result = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -176,7 +188,7 @@ describe('SessionService', () => {
readonlyAccess: false,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -190,7 +202,7 @@ describe('SessionService', () => {
user.email = 'demo@standardnotes.com'
user.uuid = '123'
const sessionPayload = await createService().createNewSessionForUser({
const result = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -212,7 +224,7 @@ describe('SessionService', () => {
readonlyAccess: true,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -229,7 +241,7 @@ describe('SessionService', () => {
value: LogSessionUserAgentOption.Disabled,
} as jest.Mocked<Setting>)
const sessionPayload = await createService().createNewSessionForUser({
const result = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -250,7 +262,7 @@ describe('SessionService', () => {
readonlyAccess: false,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -305,7 +317,7 @@ describe('SessionService', () => {
user.uuid = '123'
user.email = 'test@test.te'
const sessionPayload = await createService().createNewSessionForUser({
const result = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -317,7 +329,7 @@ describe('SessionService', () => {
username: 'test@test.te',
subscriptionPlanName: null,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -333,7 +345,7 @@ describe('SessionService', () => {
user.uuid = '123'
user.email = 'test@test.te'
const sessionPayload = await createService().createNewSessionForUser({
const result = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -345,7 +357,7 @@ describe('SessionService', () => {
username: 'test@test.te',
subscriptionPlanName: null,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -361,7 +373,7 @@ describe('SessionService', () => {
user.uuid = '123'
user.email = 'test@test.te'
const sessionPayload = await createService().createNewSessionForUser({
const result = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -373,7 +385,7 @@ describe('SessionService', () => {
username: 'test@test.te',
subscriptionPlanName: null,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -386,7 +398,7 @@ describe('SessionService', () => {
const user = {} as jest.Mocked<User>
user.uuid = '123'
const sessionPayload = await createService().createNewEphemeralSessionForUser({
const result = await createService().createNewEphemeralSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -408,7 +420,7 @@ describe('SessionService', () => {
readonlyAccess: false,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -420,7 +432,7 @@ describe('SessionService', () => {
it('should delete a session by token', async () => {
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
if (uuid === '2') {
return session
return existingSession
}
return null
@@ -429,13 +441,28 @@ describe('SessionService', () => {
await createService().deleteSessionByToken('1:2:3')
expect(sessionRepository.deleteOneByUuid).toHaveBeenCalledWith('2e1e43')
expect(ephemeralSessionRepository.deleteOne).toHaveBeenCalledWith('2e1e43', '1-2-3')
expect(ephemeralSessionRepository.deleteOne).not.toHaveBeenCalled()
})
it('should delete an ephemeral session by token', async () => {
ephemeralSessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
if (uuid === '2') {
return existingEphemeralSession
}
return null
})
await createService().deleteSessionByToken('1:2:3')
expect(sessionRepository.deleteOneByUuid).not.toHaveBeenCalled()
expect(ephemeralSessionRepository.deleteOne).toHaveBeenCalledWith('2-3-4', '1-2-3')
})
it('should not delete a session by token if session is not found', async () => {
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
if (uuid === '2') {
return session
return existingSession
}
return null
@@ -448,13 +475,13 @@ describe('SessionService', () => {
})
it('should determine if a refresh token is valid', async () => {
expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2:3')).toBeTruthy()
expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2:4')).toBeFalsy()
expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2')).toBeFalsy()
expect(createService().isRefreshTokenMatchingHashedSessionToken(existingSession, '1:2:3')).toBeTruthy()
expect(createService().isRefreshTokenMatchingHashedSessionToken(existingSession, '1:2:4')).toBeFalsy()
expect(createService().isRefreshTokenMatchingHashedSessionToken(existingSession, '1:2')).toBeFalsy()
})
it('should return device info based on user agent', () => {
expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0 on Mac 10.13')
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0 on Mac 10.13')
})
it('should return device info based on undefined user agent', () => {
@@ -463,7 +490,7 @@ describe('SessionService', () => {
browser: { name: undefined, version: undefined },
os: { name: undefined, version: undefined },
})
expect(createService().getDeviceInfo(session)).toEqual('Unknown Client on Unknown OS')
expect(createService().getDeviceInfo(existingSession)).toEqual('Unknown Client on Unknown OS')
})
it('should return a shorter info based on lack of client in user agent', () => {
@@ -473,7 +500,7 @@ describe('SessionService', () => {
os: { name: 'iOS', version: '10.3' },
})
expect(createService().getDeviceInfo(session)).toEqual('iOS 10.3')
expect(createService().getDeviceInfo(existingSession)).toEqual('iOS 10.3')
})
it('should return a shorter info based on lack of os in user agent', () => {
@@ -483,13 +510,13 @@ describe('SessionService', () => {
os: { name: '', version: '' },
})
expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0')
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0')
})
it('should return unknown client and os if user agent is cleaned out', () => {
session.userAgent = null
existingSession.userAgent = null
expect(createService().getDeviceInfo(session)).toEqual('Unknown Client on Unknown OS')
expect(createService().getDeviceInfo(existingSession)).toEqual('Unknown Client on Unknown OS')
})
it('should return a shorter info based on partial os in user agent', () => {
@@ -499,7 +526,7 @@ describe('SessionService', () => {
os: { name: 'Windows', version: '' },
})
expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0 on Windows')
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0 on Windows')
deviceDetector.getResult = jest.fn().mockReturnValue({
ua: 'dummy-data',
@@ -507,7 +534,7 @@ describe('SessionService', () => {
os: { name: '', version: '7' },
})
expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0 on 7')
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0 on 7')
})
it('should return a shorter info based on partial client in user agent', () => {
@@ -517,7 +544,7 @@ describe('SessionService', () => {
os: { name: 'Windows', version: '7' },
})
expect(createService().getDeviceInfo(session)).toEqual('69.0 on Windows 7')
expect(createService().getDeviceInfo(existingSession)).toEqual('69.0 on Windows 7')
deviceDetector.getResult = jest.fn().mockReturnValue({
ua: 'dummy-data',
@@ -525,7 +552,7 @@ describe('SessionService', () => {
os: { name: 'Windows', version: '7' },
})
expect(createService().getDeviceInfo(session)).toEqual('Chrome on Windows 7')
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome on Windows 7')
})
it('should return a shorter info based on iOS agent', () => {
@@ -538,7 +565,7 @@ describe('SessionService', () => {
cpu: { architecture: undefined },
})
expect(createService().getDeviceInfo(session)).toEqual('iOS')
expect(createService().getDeviceInfo(existingSession)).toEqual('iOS')
})
it('should return a shorter info based on partial client and partial os in user agent', () => {
@@ -548,7 +575,7 @@ describe('SessionService', () => {
os: { name: 'Windows', version: '' },
})
expect(createService().getDeviceInfo(session)).toEqual('69.0 on Windows')
expect(createService().getDeviceInfo(existingSession)).toEqual('69.0 on Windows')
deviceDetector.getResult = jest.fn().mockReturnValue({
ua: 'dummy-data',
@@ -556,7 +583,7 @@ describe('SessionService', () => {
os: { name: '', version: '7' },
})
expect(createService().getDeviceInfo(session)).toEqual('Chrome on 7')
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome on 7')
})
it('should return only Android os for okHttp client', () => {
@@ -569,7 +596,7 @@ describe('SessionService', () => {
cpu: { architecture: undefined },
})
expect(createService().getDeviceInfo(session)).toEqual('Android')
expect(createService().getDeviceInfo(existingSession)).toEqual('Android')
})
it('should detect the StandardNotes app in user agent', () => {
@@ -582,7 +609,7 @@ describe('SessionService', () => {
cpu: { architecture: undefined },
})
expect(createService().getDeviceInfo(session)).toEqual('Standard Notes Desktop 3.5.18 on Mac OS 10.16.0')
expect(createService().getDeviceInfo(existingSession)).toEqual('Standard Notes Desktop 3.5.18 on Mac OS 10.16.0')
})
it('should return unknown device info as fallback', () => {
@@ -590,70 +617,72 @@ describe('SessionService', () => {
throw new Error('something bad happened')
})
expect(createService().getDeviceInfo(session)).toEqual('Unknown Client on Unknown OS')
expect(createService().getDeviceInfo(existingSession)).toEqual('Unknown Client on Unknown OS')
})
it('should retrieve a session from a session token', async () => {
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
if (uuid === '2') {
return session
return existingSession
}
return null
})
const result = await createService().getSessionFromToken('1:2:3')
const { session, isEphemeral } = await createService().getSessionFromToken('1:2:3')
expect(result).toEqual(session)
expect(session).toEqual(session)
expect(isEphemeral).toBeFalsy()
})
it('should retrieve an ephemeral session from a session token', async () => {
ephemeralSessionRepository.findOneByUuid = jest.fn().mockReturnValue(ephemeralSession)
ephemeralSessionRepository.findOneByUuid = jest.fn().mockReturnValue(existingEphemeralSession)
sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
const result = await createService().getSessionFromToken('1:2:3')
const { session, isEphemeral } = await createService().getSessionFromToken('1:2:3')
expect(result).toEqual(ephemeralSession)
expect(session).toEqual(existingEphemeralSession)
expect(isEphemeral).toBeTruthy()
})
it('should not retrieve a session from a session token that has access token missing', async () => {
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
if (uuid === '2') {
return session
return existingSession
}
return null
})
const result = await createService().getSessionFromToken('1:2')
const { session } = await createService().getSessionFromToken('1:2')
expect(result).toBeUndefined()
expect(session).toBeUndefined()
})
it('should not retrieve a session that is missing', async () => {
sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
const result = await createService().getSessionFromToken('1:2:3')
const { session } = await createService().getSessionFromToken('1:2:3')
expect(result).toBeUndefined()
expect(session).toBeUndefined()
})
it('should not retrieve a session from a session token that has invalid access token', async () => {
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
if (uuid === '2') {
return session
return existingSession
}
return null
})
const result = await createService().getSessionFromToken('1:2:4')
const { session } = await createService().getSessionFromToken('1:2:4')
expect(result).toBeUndefined()
expect(session).toBeUndefined()
})
it('should revoked a session', async () => {
await createService().createRevokedSession(session)
await createService().createRevokedSession(existingSession)
expect(revokedSessionRepository.save).toHaveBeenCalledWith({
uuid: '2e1e43',

View File

@@ -49,7 +49,7 @@ export class SessionService implements SessionServiceInterface {
apiVersion: string
userAgent: string
readonlyAccess: boolean
}): Promise<SessionBody> {
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }> {
const session = await this.createSession({
ephemeral: false,
...dto,
@@ -73,7 +73,10 @@ export class SessionService implements SessionServiceInterface {
this.logger.error(`Could not trace session while creating cross service token.: ${(error as Error).message}`)
}
return sessionPayload
return {
sessionHttpRepresentation: sessionPayload,
session,
}
}
async createNewEphemeralSessionForUser(dto: {
@@ -81,7 +84,7 @@ export class SessionService implements SessionServiceInterface {
apiVersion: string
userAgent: string
readonlyAccess: boolean
}): Promise<SessionBody> {
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }> {
const ephemeralSession = await this.createSession({
ephemeral: true,
...dto,
@@ -91,27 +94,20 @@ export class SessionService implements SessionServiceInterface {
await this.ephemeralSessionRepository.save(ephemeralSession)
return sessionPayload
return {
sessionHttpRepresentation: sessionPayload,
session: ephemeralSession,
}
}
async refreshTokens(session: Session): Promise<SessionBody> {
const sessionPayload = await this.createTokens(session)
async refreshTokens(dto: { session: Session; isEphemeral: boolean }): Promise<SessionBody> {
const sessionPayload = await this.createTokens(dto.session)
await this.sessionRepository.updateHashedTokens(session.uuid, session.hashedAccessToken, session.hashedRefreshToken)
await this.sessionRepository.updatedTokenExpirationDates(
session.uuid,
session.accessExpiration,
session.refreshExpiration,
)
await this.ephemeralSessionRepository.updateTokensAndExpirationDates(
session.uuid,
session.hashedAccessToken,
session.hashedRefreshToken,
session.accessExpiration,
session.refreshExpiration,
)
if (dto.isEphemeral) {
await this.ephemeralSessionRepository.save(dto.session)
} else {
await this.sessionRepository.save(dto.session)
}
return sessionPayload
}
@@ -190,25 +186,25 @@ export class SessionService implements SessionServiceInterface {
return `${browserInfo} on ${osInfo}`
}
async getSessionFromToken(token: string): Promise<Session | undefined> {
async getSessionFromToken(token: string): Promise<{ session: Session | undefined; isEphemeral: boolean }> {
const tokenParts = token.split(':')
const sessionUuid = tokenParts[1]
const accessToken = tokenParts[2]
if (!accessToken) {
return undefined
return { session: undefined, isEphemeral: false }
}
const session = await this.getSession(sessionUuid)
const { session, isEphemeral } = await this.getSession(sessionUuid)
if (!session) {
return undefined
return { session: undefined, isEphemeral: false }
}
const hashedAccessToken = crypto.createHash('sha256').update(accessToken).digest('hex')
if (crypto.timingSafeEqual(Buffer.from(session.hashedAccessToken), Buffer.from(hashedAccessToken))) {
return session
return { session, isEphemeral }
}
return undefined
return { session: undefined, isEphemeral: false }
}
async getRevokedSessionFromToken(token: string): Promise<RevokedSession | null> {
@@ -229,11 +225,14 @@ export class SessionService implements SessionServiceInterface {
}
async deleteSessionByToken(token: string): Promise<string | null> {
const session = await this.getSessionFromToken(token)
const { session, isEphemeral } = await this.getSessionFromToken(token)
if (session) {
await this.sessionRepository.deleteOneByUuid(session.uuid)
await this.ephemeralSessionRepository.deleteOne(session.uuid, session.userUuid)
if (isEphemeral) {
await this.ephemeralSessionRepository.deleteOne(session.uuid, session.userUuid)
} else {
await this.sessionRepository.deleteOneByUuid(session.uuid)
}
return session.userUuid
}
@@ -278,14 +277,19 @@ export class SessionService implements SessionServiceInterface {
return session
}
private async getSession(uuid: string): Promise<Session | null> {
private async getSession(uuid: string): Promise<{
session: Session | null
isEphemeral: boolean
}> {
let session = await this.ephemeralSessionRepository.findOneByUuid(uuid)
let isEphemeral = true
if (!session) {
session = await this.sessionRepository.findOneByUuid(uuid)
isEphemeral = false
}
return session
return { session, isEphemeral }
}
private async createTokens(session: Session): Promise<SessionBody> {

View File

@@ -9,15 +9,15 @@ export interface SessionServiceInterface {
apiVersion: string
userAgent: string
readonlyAccess: boolean
}): Promise<SessionBody>
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }>
createNewEphemeralSessionForUser(dto: {
user: User
apiVersion: string
userAgent: string
readonlyAccess: boolean
}): Promise<SessionBody>
refreshTokens(session: Session): Promise<SessionBody>
getSessionFromToken(token: string): Promise<Session | undefined>
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }>
refreshTokens(dto: { session: Session; isEphemeral: boolean }): Promise<SessionBody>
getSessionFromToken(token: string): Promise<{ session: Session | undefined; isEphemeral: boolean }>
getRevokedSessionFromToken(token: string): Promise<RevokedSession | null>
markRevokedSessionAsReceived(revokedSession: RevokedSession): Promise<RevokedSession>
deleteSessionByToken(token: string): Promise<string | null>

View File

@@ -11,7 +11,10 @@ import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { ChangeCredentials } from './ChangeCredentials'
import { Username } from '@standardnotes/domain-core'
import { Result, Username } from '@standardnotes/domain-core'
import { DeleteOtherSessionsForUser } from '../DeleteOtherSessionsForUser'
import { ApiVersion } from '../../Api/ApiVersion'
import { Session } from '../../Session/Session'
describe('ChangeCredentials', () => {
let userRepository: UserRepositoryInterface
@@ -21,13 +24,23 @@ describe('ChangeCredentials', () => {
let domainEventFactory: DomainEventFactoryInterface
let timer: TimerInterface
let user: User
let deleteOtherSessionsForUser: DeleteOtherSessionsForUser
const createUseCase = () =>
new ChangeCredentials(userRepository, authResponseFactoryResolver, domainEventPublisher, domainEventFactory, timer)
new ChangeCredentials(
userRepository,
authResponseFactoryResolver,
domainEventPublisher,
domainEventFactory,
timer,
deleteOtherSessionsForUser,
)
beforeEach(() => {
authResponseFactory = {} as jest.Mocked<AuthResponseFactoryInterface>
authResponseFactory.createResponse = jest.fn().mockReturnValue({ foo: 'bar' })
authResponseFactory.createResponse = jest
.fn()
.mockReturnValue({ response: { foo: 'bar' }, session: { uuid: '1-2-3' } as jest.Mocked<Session> })
authResponseFactoryResolver = {} as jest.Mocked<AuthResponseFactoryResolverInterface>
authResponseFactoryResolver.resolveAuthResponseFactoryVersion = jest.fn().mockReturnValue(authResponseFactory)
@@ -49,27 +62,25 @@ describe('ChangeCredentials', () => {
timer = {} as jest.Mocked<TimerInterface>
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
deleteOtherSessionsForUser = {} as jest.Mocked<DeleteOtherSessionsForUser>
deleteOtherSessionsForUser.execute = jest.fn().mockReturnValue(Result.ok())
})
it('should change password', async () => {
expect(
await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
}),
).toEqual({
success: true,
authResponse: {
foo: 'bar',
},
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeFalsy()
expect(userRepository.save).toHaveBeenCalledWith({
encryptedPassword: expect.any(String),
pwNonce: 'asdzxc',
@@ -81,29 +92,24 @@ describe('ChangeCredentials', () => {
})
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
expect(deleteOtherSessionsForUser.execute).toHaveBeenCalled()
})
it('should change email', async () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValueOnce(user).mockReturnValueOnce(null)
expect(
await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: 'new@test.te',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
}),
).toEqual({
success: true,
authResponse: {
foo: 'bar',
},
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: 'new@test.te',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeFalsy()
expect(userRepository.save).toHaveBeenCalledWith({
encryptedPassword: expect.any(String),
@@ -116,6 +122,7 @@ describe('ChangeCredentials', () => {
})
expect(domainEventFactory.createUserEmailChangedEvent).toHaveBeenCalledWith('1-2-3', 'test@test.te', 'new@test.te')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(deleteOtherSessionsForUser.execute).toHaveBeenCalled()
})
it('should not change email if already taken', async () => {
@@ -124,22 +131,19 @@ describe('ChangeCredentials', () => {
.mockReturnValueOnce(user)
.mockReturnValueOnce({} as jest.Mocked<User>)
expect(
await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: 'new@test.te',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
}),
).toEqual({
success: false,
errorMessage: 'The email you entered is already taken. Please try again.',
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: 'new@test.te',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('The email you entered is already taken. Please try again.')
expect(userRepository.save).not.toHaveBeenCalled()
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
@@ -147,22 +151,19 @@ describe('ChangeCredentials', () => {
})
it('should not change email if the new email is invalid', async () => {
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: 'Username cannot be empty',
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: '',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Username cannot be empty')
expect(userRepository.save).not.toHaveBeenCalled()
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
@@ -172,63 +173,52 @@ describe('ChangeCredentials', () => {
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.',
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: '',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('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({
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'test123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
}),
).toEqual({
success: false,
errorMessage: 'The current password you entered is incorrect. Please try again.',
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'test123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('The current password you entered is incorrect. Please try again.')
expect(userRepository.save).not.toHaveBeenCalled()
})
it('should update protocol version while changing password', async () => {
expect(
await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
protocolVersion: '004',
}),
).toEqual({
success: true,
authResponse: {
foo: 'bar',
},
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
protocolVersion: '004',
})
expect(result.isFailed()).toBeFalsy()
expect(userRepository.save).toHaveBeenCalledWith({
encryptedPassword: expect.any(String),
@@ -239,4 +229,63 @@ describe('ChangeCredentials', () => {
updatedAt: new Date(1),
})
})
it('should not delete other sessions for user if neither passoword nor email are changed', async () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValueOnce(user)
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'qweqwe123123',
newEmail: undefined,
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeFalsy()
expect(userRepository.save).toHaveBeenCalledWith({
encryptedPassword: expect.any(String),
email: 'test@test.te',
uuid: '1-2-3',
pwNonce: 'asdzxc',
kpCreated: '123',
kpOrigination: 'password-change',
updatedAt: new Date(1),
})
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(deleteOtherSessionsForUser.execute).not.toHaveBeenCalled()
})
it('should not delete other sessions for user if the caller does not support sessions', async () => {
authResponseFactory.createResponse = jest.fn().mockReturnValue({ response: { foo: 'bar' } })
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeFalsy()
expect(userRepository.save).toHaveBeenCalledWith({
encryptedPassword: expect.any(String),
pwNonce: 'asdzxc',
kpCreated: '123',
email: 'test@test.te',
uuid: '1-2-3',
kpOrigination: 'password-change',
updatedAt: new Date(1),
})
expect(deleteOtherSessionsForUser.execute).not.toHaveBeenCalled()
})
})

View File

@@ -1,20 +1,22 @@
import * as bcrypt from 'bcryptjs'
import { inject, injectable } from 'inversify'
import { DomainEventPublisherInterface, UserEmailChangedEvent } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { Result, UseCaseInterface, Username } from '@standardnotes/domain-core'
import TYPES from '../../../Bootstrap/Types'
import { AuthResponseFactoryResolverInterface } from '../../Auth/AuthResponseFactoryResolverInterface'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { ChangeCredentialsDTO } from './ChangeCredentialsDTO'
import { ChangeCredentialsResponse } from './ChangeCredentialsResponse'
import { UseCaseInterface } from '../UseCaseInterface'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { DomainEventPublisherInterface, UserEmailChangedEvent } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { Username } from '@standardnotes/domain-core'
import { DeleteOtherSessionsForUser } from '../DeleteOtherSessionsForUser'
import { AuthResponse20161215 } from '../../Auth/AuthResponse20161215'
import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
import { Session } from '../../Session/Session'
@injectable()
export class ChangeCredentials implements UseCaseInterface {
export class ChangeCredentials implements UseCaseInterface<AuthResponse20161215 | AuthResponse20200115> {
constructor(
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_AuthResponseFactoryResolver)
@@ -22,22 +24,18 @@ export class ChangeCredentials implements UseCaseInterface {
@inject(TYPES.Auth_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.Auth_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
@inject(TYPES.Auth_DeleteOtherSessionsForUser)
private deleteOtherSessionsForUserUseCase: DeleteOtherSessionsForUser,
) {}
async execute(dto: ChangeCredentialsDTO): Promise<ChangeCredentialsResponse> {
async execute(dto: ChangeCredentialsDTO): Promise<Result<AuthResponse20161215 | AuthResponse20200115>> {
const user = await this.userRepository.findOneByUsernameOrEmail(dto.username)
if (!user) {
return {
success: false,
errorMessage: 'User not found.',
}
return Result.fail('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.',
}
return Result.fail('The current password you entered is incorrect. Please try again.')
}
user.encryptedPassword = await bcrypt.hash(dto.newPassword, User.PASSWORD_HASH_COST)
@@ -46,19 +44,13 @@ export class ChangeCredentials implements UseCaseInterface {
if (dto.newEmail !== undefined) {
const newUsernameOrError = Username.create(dto.newEmail)
if (newUsernameOrError.isFailed()) {
return {
success: false,
errorMessage: newUsernameOrError.getError(),
}
return Result.fail(newUsernameOrError.getError())
}
const newUsername = newUsernameOrError.getValue()
const existingUser = await this.userRepository.findOneByUsernameOrEmail(newUsername)
if (existingUser !== null) {
return {
success: false,
errorMessage: 'The email you entered is already taken. Please try again.',
}
return Result.fail('The email you entered is already taken. Please try again.')
}
userEmailChangedEvent = this.domainEventFactory.createUserEmailChangedEvent(
@@ -90,15 +82,35 @@ export class ChangeCredentials implements UseCaseInterface {
const authResponseFactory = this.authResponseFactoryResolver.resolveAuthResponseFactoryVersion(dto.apiVersion)
return {
success: true,
authResponse: await authResponseFactory.createResponse({
user: updatedUser,
apiVersion: dto.apiVersion,
userAgent: dto.updatedWithUserAgent,
ephemeralSession: false,
readonlyAccess: false,
}),
const authResponse = await authResponseFactory.createResponse({
user: updatedUser,
apiVersion: dto.apiVersion,
userAgent: dto.updatedWithUserAgent,
ephemeralSession: false,
readonlyAccess: false,
})
if (authResponse.session) {
await this.deleteOtherSessionsForUserIfNeeded(user.uuid, authResponse.session, dto)
}
return Result.ok(authResponse.response)
}
private async deleteOtherSessionsForUserIfNeeded(
userUuid: string,
session: Session,
dto: ChangeCredentialsDTO,
): Promise<void> {
const passwordHasChanged = dto.newPassword !== dto.currentPassword
const userEmailChanged = dto.newEmail !== undefined
if (passwordHasChanged || userEmailChanged) {
await this.deleteOtherSessionsForUserUseCase.execute({
userUuid,
currentSessionUuid: session.uuid,
markAsRevoked: false,
})
}
}
}

View File

@@ -1,8 +0,0 @@
import { AuthResponse20161215 } from '../../Auth/AuthResponse20161215'
import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
export type ChangeCredentialsResponse = {
success: boolean
authResponse?: AuthResponse20161215 | AuthResponse20200115
errorMessage?: string
}

View File

@@ -0,0 +1,82 @@
import 'reflect-metadata'
import { Session } from '../Session/Session'
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { DeleteOtherSessionsForUser } from './DeleteOtherSessionsForUser'
describe('DeleteOtherSessionsForUser', () => {
let sessionRepository: SessionRepositoryInterface
let sessionService: SessionServiceInterface
let session: Session
let currentSession: Session
const createUseCase = () => new DeleteOtherSessionsForUser(sessionRepository, sessionService)
beforeEach(() => {
session = {} as jest.Mocked<Session>
session.uuid = '00000000-0000-0000-0000-000000000000'
currentSession = {} as jest.Mocked<Session>
currentSession.uuid = '00000000-0000-0000-0000-000000000001'
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
sessionRepository.deleteAllByUserUuidExceptOne = jest.fn()
sessionRepository.findAllByUserUuid = jest.fn().mockReturnValue([session, currentSession])
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.createRevokedSession = jest.fn()
})
it('should delete all sessions except current for a given user', async () => {
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
currentSessionUuid: '00000000-0000-0000-0000-000000000001',
markAsRevoked: true,
})
expect(result.isFailed()).toBeFalsy()
expect(sessionRepository.deleteAllByUserUuidExceptOne).toHaveBeenCalled()
expect(sessionService.createRevokedSession).toHaveBeenCalledWith(session)
expect(sessionService.createRevokedSession).not.toHaveBeenCalledWith(currentSession)
})
it('should delete all sessions except current for a given user without marking as revoked', async () => {
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
currentSessionUuid: '00000000-0000-0000-0000-000000000001',
markAsRevoked: false,
})
expect(result.isFailed()).toBeFalsy()
expect(sessionRepository.deleteAllByUserUuidExceptOne).toHaveBeenCalled()
expect(sessionService.createRevokedSession).not.toHaveBeenCalled()
})
it('should not delete any sessions if the user uuid is invalid', async () => {
const result = await createUseCase().execute({
userUuid: 'invalid',
currentSessionUuid: '00000000-0000-0000-0000-000000000001',
markAsRevoked: true,
})
expect(result.isFailed()).toBeTruthy()
expect(sessionRepository.deleteAllByUserUuidExceptOne).not.toHaveBeenCalled()
expect(sessionService.createRevokedSession).not.toHaveBeenCalled()
})
it('should not delete any sessions if the current session uuid is invalid', async () => {
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
currentSessionUuid: 'invalid',
markAsRevoked: true,
})
expect(result.isFailed()).toBeTruthy()
expect(sessionRepository.deleteAllByUserUuidExceptOne).not.toHaveBeenCalled()
expect(sessionService.createRevokedSession).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,46 @@
import { inject, injectable } from 'inversify'
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import TYPES from '../../Bootstrap/Types'
import { Session } from '../Session/Session'
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { DeleteOtherSessionsForUserDTO } from './DeleteOtherSessionsForUserDTO'
@injectable()
export class DeleteOtherSessionsForUser implements UseCaseInterface<void> {
constructor(
@inject(TYPES.Auth_SessionRepository) private sessionRepository: SessionRepositoryInterface,
@inject(TYPES.Auth_SessionService) private sessionService: SessionServiceInterface,
) {}
async execute(dto: DeleteOtherSessionsForUserDTO): Promise<Result<void>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const currentSessionUuidOrError = Uuid.create(dto.currentSessionUuid)
if (currentSessionUuidOrError.isFailed()) {
return Result.fail(currentSessionUuidOrError.getError())
}
const currentSessionUuid = currentSessionUuidOrError.getValue()
const sessions = await this.sessionRepository.findAllByUserUuid(dto.userUuid)
if (dto.markAsRevoked) {
await Promise.all(
sessions.map(async (session: Session) => {
if (session.uuid !== currentSessionUuid.value) {
await this.sessionService.createRevokedSession(session)
}
}),
)
}
await this.sessionRepository.deleteAllByUserUuidExceptOne({ userUuid, currentSessionUuid })
return Result.ok()
}
}

View File

@@ -0,0 +1,5 @@
export type DeleteOtherSessionsForUserDTO = {
userUuid: string
currentSessionUuid: string
markAsRevoked: boolean
}

View File

@@ -1,39 +0,0 @@
import 'reflect-metadata'
import { Session } from '../Session/Session'
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { DeletePreviousSessionsForUser } from './DeletePreviousSessionsForUser'
describe('DeletePreviousSessionsForUser', () => {
let sessionRepository: SessionRepositoryInterface
let sessionService: SessionServiceInterface
let session: Session
let currentSession: Session
const createUseCase = () => new DeletePreviousSessionsForUser(sessionRepository, sessionService)
beforeEach(() => {
session = {} as jest.Mocked<Session>
session.uuid = '1-2-3'
currentSession = {} as jest.Mocked<Session>
currentSession.uuid = '2-3-4'
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
sessionRepository.deleteAllByUserUuid = jest.fn()
sessionRepository.findAllByUserUuid = jest.fn().mockReturnValue([session, currentSession])
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.createRevokedSession = jest.fn()
})
it('should delete all sessions except current for a given user', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', currentSessionUuid: '2-3-4' })).toEqual({ success: true })
expect(sessionRepository.deleteAllByUserUuid).toHaveBeenCalledWith('1-2-3', '2-3-4')
expect(sessionService.createRevokedSession).toHaveBeenCalledWith(session)
expect(sessionService.createRevokedSession).not.toHaveBeenCalledWith(currentSession)
})
})

View File

@@ -1,32 +0,0 @@
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { Session } from '../Session/Session'
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { DeletePreviousSessionsForUserDTO } from './DeletePreviousSessionsForUserDTO'
import { DeletePreviousSessionsForUserResponse } from './DeletePreviousSessionsForUserResponse'
import { UseCaseInterface } from './UseCaseInterface'
@injectable()
export class DeletePreviousSessionsForUser implements UseCaseInterface {
constructor(
@inject(TYPES.Auth_SessionRepository) private sessionRepository: SessionRepositoryInterface,
@inject(TYPES.Auth_SessionService) private sessionService: SessionServiceInterface,
) {}
async execute(dto: DeletePreviousSessionsForUserDTO): Promise<DeletePreviousSessionsForUserResponse> {
const sessions = await this.sessionRepository.findAllByUserUuid(dto.userUuid)
await Promise.all(
sessions.map(async (session: Session) => {
if (session.uuid !== dto.currentSessionUuid) {
await this.sessionService.createRevokedSession(session)
}
}),
)
await this.sessionRepository.deleteAllByUserUuid(dto.userUuid, dto.currentSessionUuid)
return { success: true }
}
}

View File

@@ -1,4 +0,0 @@
export type DeletePreviousSessionsForUserDTO = {
userUuid: string
currentSessionUuid: string
}

View File

@@ -1,3 +0,0 @@
export type DeletePreviousSessionsForUserResponse = {
success: boolean
}

View File

@@ -26,7 +26,7 @@ describe('RefreshSessionToken', () => {
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.isRefreshTokenMatchingHashedSessionToken = jest.fn().mockReturnValue(true)
sessionService.getSessionFromToken = jest.fn().mockReturnValue(session)
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false })
sessionService.refreshTokens = jest.fn().mockReturnValue({
access_token: 'token1',
refresh_token: 'token2',
@@ -51,9 +51,10 @@ describe('RefreshSessionToken', () => {
const result = await createUseCase().execute({
accessToken: '123',
refreshToken: '234',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
})
expect(sessionService.refreshTokens).toHaveBeenCalledWith(session)
expect(sessionService.refreshTokens).toHaveBeenCalledWith({ session, isEphemeral: false })
expect(result).toEqual({
success: true,
@@ -74,9 +75,10 @@ describe('RefreshSessionToken', () => {
const result = await createUseCase().execute({
accessToken: '123',
refreshToken: '234',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
})
expect(sessionService.refreshTokens).toHaveBeenCalledWith(session)
expect(sessionService.refreshTokens).toHaveBeenCalledWith({ session, isEphemeral: false })
expect(result).toEqual({
success: true,
@@ -90,11 +92,12 @@ describe('RefreshSessionToken', () => {
})
it('should not refresh a session token if session is not found', async () => {
sessionService.getSessionFromToken = jest.fn().mockReturnValue(null)
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session: undefined, isEphemeral: false })
const result = await createUseCase().execute({
accessToken: '123',
refreshToken: '234',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
})
expect(result).toEqual({
@@ -110,6 +113,7 @@ describe('RefreshSessionToken', () => {
const result = await createUseCase().execute({
accessToken: '123',
refreshToken: '234',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
})
expect(result).toEqual({
@@ -125,6 +129,7 @@ describe('RefreshSessionToken', () => {
const result = await createUseCase().execute({
accessToken: '123',
refreshToken: '234',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
})
expect(result).toEqual({

View File

@@ -21,7 +21,7 @@ export class RefreshSessionToken {
) {}
async execute(dto: RefreshSessionTokenDTO): Promise<RefreshSessionTokenResponse> {
const session = await this.sessionService.getSessionFromToken(dto.accessToken)
const { session, isEphemeral } = await this.sessionService.getSessionFromToken(dto.accessToken)
if (!session) {
return {
success: false,
@@ -46,7 +46,9 @@ export class RefreshSessionToken {
}
}
const sessionPayload = await this.sessionService.refreshTokens(session)
session.userAgent = dto.userAgent
const sessionPayload = await this.sessionService.refreshTokens({ session, isEphemeral })
try {
await this.domainEventPublisher.publish(

View File

@@ -1,4 +1,5 @@
export type RefreshSessionTokenDTO = {
accessToken: string
refreshToken: string
userAgent: string
}

View File

@@ -10,6 +10,7 @@ import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { Register } from './Register'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
import { Session } from '../Session/Session'
describe('Register', () => {
let userRepository: UserRepositoryInterface
@@ -32,7 +33,9 @@ describe('Register', () => {
roleRepository.findOneByName = jest.fn().mockReturnValue(null)
authResponseFactory = {} as jest.Mocked<AuthResponseFactory20200115>
authResponseFactory.createResponse = jest.fn().mockReturnValue({ foo: 'bar' })
authResponseFactory.createResponse = jest
.fn()
.mockReturnValue({ response: { foo: 'bar' }, session: {} as jest.Mocked<Session> })
crypter = {} as jest.Mocked<CrypterInterface>
crypter.generateEncryptedUserServerKey = jest.fn().mockReturnValue('test')

View File

@@ -83,15 +83,17 @@ export class Register implements UseCaseInterface {
await this.settingService.applyDefaultSettingsUponRegistration(user)
const result = await this.authResponseFactory20200115.createResponse({
user,
apiVersion,
userAgent: dto.updatedWithUserAgent,
ephemeralSession,
readonlyAccess: false,
})
return {
success: true,
authResponse: (await this.authResponseFactory20200115.createResponse({
user,
apiVersion,
userAgent: dto.updatedWithUserAgent,
ephemeralSession,
readonlyAccess: false,
})) as AuthResponse20200115,
authResponse: result.response as AuthResponse20200115,
}
}
}

View File

@@ -13,6 +13,7 @@ import { SignIn } from './SignIn'
import { PKCERepositoryInterface } from '../User/PKCERepositoryInterface'
import { CrypterInterface } from '../Encryption/CrypterInterface'
import { ProtocolVersion } from '@standardnotes/common'
import { Session } from '../Session/Session'
describe('SignIn', () => {
let user: User
@@ -50,7 +51,9 @@ describe('SignIn', () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
authResponseFactory = {} as jest.Mocked<AuthResponseFactoryInterface>
authResponseFactory.createResponse = jest.fn().mockReturnValue({ foo: 'bar' })
authResponseFactory.createResponse = jest
.fn()
.mockReturnValue({ response: { foo: 'bar' }, session: {} as jest.Mocked<Session> })
authResponseFactoryResolver = {} as jest.Mocked<AuthResponseFactoryResolverInterface>
authResponseFactoryResolver.resolveAuthResponseFactoryVersion = jest.fn().mockReturnValue(authResponseFactory)

View File

@@ -95,15 +95,17 @@ export class SignIn implements UseCaseInterface {
await this.sendSignInEmailNotification(user, dto.userAgent)
const result = await authResponseFactory.createResponse({
user,
apiVersion: dto.apiVersion,
userAgent: dto.userAgent,
ephemeralSession: dto.ephemeralSession,
readonlyAccess: false,
})
return {
success: true,
authResponse: await authResponseFactory.createResponse({
user,
apiVersion: dto.apiVersion,
userAgent: dto.userAgent,
ephemeralSession: dto.ephemeralSession,
readonlyAccess: false,
}),
authResponse: result.response,
}
}

View File

@@ -124,7 +124,7 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
await this.clearLoginAttempts.execute({ email: username.value })
return Result.ok(authResponse as AuthResponse20200115)
return Result.ok(authResponse.response as AuthResponse20200115)
}
private async validateCodeVerifier(codeVerifier: string): Promise<boolean> {

View File

@@ -1,28 +1,22 @@
import 'reflect-metadata'
import { UpdateStorageQuotaUsedForUser } from './UpdateStorageQuotaUsedForUser'
import { FileUploadedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { FileUploadedEventHandler } from './FileUploadedEventHandler'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
import { UserSubscription } from '../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
describe('FileUploadedEventHandler', () => {
describe('UpdateStorageQuotaUsedForUser', () => {
let userRepository: UserRepositoryInterface
let userSubscriptionService: UserSubscriptionServiceInterface
let logger: Logger
let user: User
let event: FileUploadedEvent
let subscriptionSettingService: SubscriptionSettingServiceInterface
let regularSubscription: UserSubscription
let sharedSubscription: UserSubscription
const createHandler = () =>
new FileUploadedEventHandler(userRepository, userSubscriptionService, subscriptionSettingService, logger)
const createUseCase = () =>
new UpdateStorageQuotaUsedForUser(userRepository, userSubscriptionService, subscriptionSettingService)
beforeEach(() => {
user = {
@@ -52,23 +46,15 @@ describe('FileUploadedEventHandler', () => {
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
subscriptionSettingService.createOrReplace = jest.fn()
event = {} as jest.Mocked<FileUploadedEvent>
event.createdAt = new Date(1)
event.payload = {
userUuid: '00000000-0000-0000-0000-000000000000',
fileByteSize: 123,
filePath: '00000000-0000-0000-0000-000000000000/2-3-4',
fileName: '2-3-4',
}
logger = {} as jest.Mocked<Logger>
logger.warn = jest.fn()
})
it('should create a bytes used setting if one does not exist', async () => {
await createHandler().handle(event)
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
bytesUsed: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
props: {
name: 'FILE_UPLOAD_BYTES_USED',
@@ -86,9 +72,11 @@ describe('FileUploadedEventHandler', () => {
})
it('should not do anything if a user uuid is invalid', async () => {
event.payload.userUuid = 'invalid'
await createHandler().handle(event)
const result = await createUseCase().execute({
userUuid: 'invalid',
bytesUsed: 123,
})
expect(result.isFailed()).toBeTruthy()
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
})
@@ -96,7 +84,11 @@ describe('FileUploadedEventHandler', () => {
it('should not do anything if a user is not found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
await createHandler().handle(event)
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
bytesUsed: 123,
})
expect(result.isFailed()).toBeTruthy()
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
})
@@ -109,16 +101,24 @@ describe('FileUploadedEventHandler', () => {
.fn()
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
await createHandler().handle(event)
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
bytesUsed: 123,
})
expect(result.isFailed()).toBeTruthy()
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
})
it('should update a bytes used setting if one does exist', async () => {
it('should add bytes used setting if one does exist', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
value: 345,
})
await createHandler().handle(event)
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
bytesUsed: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
props: {
@@ -136,6 +136,32 @@ describe('FileUploadedEventHandler', () => {
})
})
it('should subtract bytes used setting if one does exist', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
value: 345,
})
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
bytesUsed: -123,
})
expect(result.isFailed()).toBeFalsy()
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
props: {
name: 'FILE_UPLOAD_BYTES_USED',
sensitive: false,
unencryptedValue: '222',
serverEncryptionVersion: 0,
},
user,
userSubscription: {
uuid: '00000000-0000-0000-0000-000000000000',
subscriptionType: 'regular',
user: Promise.resolve(user),
},
})
})
it('should update a bytes used setting on both regular and shared subscription', async () => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
@@ -144,7 +170,11 @@ describe('FileUploadedEventHandler', () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
value: 345,
})
await createHandler().handle(event)
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
bytesUsed: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
props: {

View File

@@ -0,0 +1,72 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SettingName } from '@standardnotes/settings'
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UpdateStorageQuotaUsedForUserDTO } from './UpdateStorageQuotaUsedForUserDTO'
import { User } from '../../User/User'
export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
constructor(
private userRepository: UserRepositoryInterface,
private userSubscriptionService: UserSubscriptionServiceInterface,
private subscriptionSettingService: SubscriptionSettingServiceInterface,
) {}
async execute(dto: UpdateStorageQuotaUsedForUserDTO): Promise<Result<void>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
return Result.fail(`Could not find user with uuid: ${userUuid.value}`)
}
const { regularSubscription, sharedSubscription } =
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(userUuid.value)
if (regularSubscription === null) {
return Result.fail(`Could not find regular user subscription for user with uuid: ${userUuid.value}`)
}
await this.updateUploadBytesUsedSetting(regularSubscription, user, dto.bytesUsed)
if (sharedSubscription !== null) {
await this.updateUploadBytesUsedSetting(sharedSubscription, user, dto.bytesUsed)
}
return Result.ok()
}
private async updateUploadBytesUsedSetting(
subscription: UserSubscription,
user: User,
bytesUsed: number,
): Promise<void> {
let bytesAlreadyUsed = '0'
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: (await subscription.user).uuid,
userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
})
if (bytesUsedSetting !== null) {
bytesAlreadyUsed = bytesUsedSetting.value as string
}
await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription,
user,
props: {
name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesAlreadyUsed + bytesUsed).toString(),
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
},
})
}
}

View File

@@ -0,0 +1,4 @@
export interface UpdateStorageQuotaUsedForUserDTO {
userUuid: string
bytesUsed: number
}

View File

@@ -8,6 +8,7 @@ import { AuthResponseFactoryInterface } from '../Auth/AuthResponseFactoryInterfa
import { AuthResponseFactoryResolverInterface } from '../Auth/AuthResponseFactoryResolverInterface'
import { UpdateUser } from './UpdateUser'
import { Session } from '../Session/Session'
describe('UpdateUser', () => {
let userRepository: UserRepositoryInterface
@@ -24,7 +25,9 @@ describe('UpdateUser', () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(undefined)
authResponseFactory = {} as jest.Mocked<AuthResponseFactoryInterface>
authResponseFactory.createResponse = jest.fn().mockReturnValue({ foo: 'bar' })
authResponseFactory.createResponse = jest
.fn()
.mockReturnValue({ response: { foo: 'bar' }, session: {} as jest.Mocked<Session> })
authResponseFactoryResolver = {} as jest.Mocked<AuthResponseFactoryResolverInterface>
authResponseFactoryResolver.resolveAuthResponseFactoryVersion = jest.fn().mockReturnValue(authResponseFactory)

View File

@@ -23,15 +23,17 @@ export class UpdateUser implements UseCaseInterface {
const authResponseFactory = this.authResponseFactoryResolver.resolveAuthResponseFactoryVersion(dto.apiVersion)
const result = await authResponseFactory.createResponse({
user: updatedUser,
apiVersion: dto.apiVersion,
userAgent: dto.updatedWithUserAgent,
ephemeralSession: false,
readonlyAccess: false,
})
return {
success: true,
authResponse: await authResponseFactory.createResponse({
user: updatedUser,
apiVersion: dto.apiVersion,
userAgent: dto.updatedWithUserAgent,
ephemeralSession: false,
readonlyAccess: false,
}),
authResponse: result.response,
}
}
}

View File

@@ -4,32 +4,33 @@ import * as express from 'express'
import { AnnotatedSessionController } from './AnnotatedSessionController'
import { results } from 'inversify-express-utils'
import { DeletePreviousSessionsForUser } from '../../Domain/UseCase/DeletePreviousSessionsForUser'
import { DeleteOtherSessionsForUser } from '../../Domain/UseCase/DeleteOtherSessionsForUser'
import { DeleteSessionForUser } from '../../Domain/UseCase/DeleteSessionForUser'
import { RefreshSessionToken } from '../../Domain/UseCase/RefreshSessionToken'
describe('AnnotatedSessionController', () => {
let deleteSessionForUser: DeleteSessionForUser
let deletePreviousSessionsForUser: DeletePreviousSessionsForUser
let deleteOtherSessionsForUser: DeleteOtherSessionsForUser
let refreshSessionToken: RefreshSessionToken
let request: express.Request
let response: express.Response
const createController = () =>
new AnnotatedSessionController(deleteSessionForUser, deletePreviousSessionsForUser, refreshSessionToken)
new AnnotatedSessionController(deleteSessionForUser, deleteOtherSessionsForUser, refreshSessionToken)
beforeEach(() => {
deleteSessionForUser = {} as jest.Mocked<DeleteSessionForUser>
deleteSessionForUser.execute = jest.fn().mockReturnValue({ success: true })
deletePreviousSessionsForUser = {} as jest.Mocked<DeletePreviousSessionsForUser>
deletePreviousSessionsForUser.execute = jest.fn()
deleteOtherSessionsForUser = {} as jest.Mocked<DeleteOtherSessionsForUser>
deleteOtherSessionsForUser.execute = jest.fn()
refreshSessionToken = {} as jest.Mocked<RefreshSessionToken>
refreshSessionToken.execute = jest.fn()
request = {
body: {},
headers: {},
} as jest.Mocked<express.Request>
response = {
@@ -70,6 +71,7 @@ describe('AnnotatedSessionController', () => {
it('should return bad request upon failed tokens refreshing', async () => {
request.body.access_token = '123'
request.body.refresh_token = '234'
request.headers['user-agent'] = 'Google Chrome'
refreshSessionToken.execute = jest.fn().mockReturnValue({
success: false,
@@ -196,9 +198,10 @@ describe('AnnotatedSessionController', () => {
const httpResult = <results.JsonResult>await createController().deleteAllSessions(request, response)
const result = await httpResult.executeAsync()
expect(deletePreviousSessionsForUser.execute).toHaveBeenCalledWith({
expect(deleteOtherSessionsForUser.execute).toHaveBeenCalledWith({
userUuid: '123',
currentSessionUuid: '234',
markAsRevoked: true,
})
expect(result.statusCode).toEqual(204)
@@ -218,7 +221,7 @@ describe('AnnotatedSessionController', () => {
const httpResponse = <results.JsonResult>await createController().deleteAllSessions(request, response)
const result = await httpResponse.executeAsync()
expect(deletePreviousSessionsForUser.execute).not.toHaveBeenCalled()
expect(deleteOtherSessionsForUser.execute).not.toHaveBeenCalled()
expect(result.statusCode).toEqual(401)
})

View File

@@ -8,7 +8,7 @@ import {
results,
} from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { DeletePreviousSessionsForUser } from '../../Domain/UseCase/DeletePreviousSessionsForUser'
import { DeleteOtherSessionsForUser } from '../../Domain/UseCase/DeleteOtherSessionsForUser'
import { DeleteSessionForUser } from '../../Domain/UseCase/DeleteSessionForUser'
import { RefreshSessionToken } from '../../Domain/UseCase/RefreshSessionToken'
import { BaseSessionController } from './Base/BaseSessionController'
@@ -17,11 +17,11 @@ import { BaseSessionController } from './Base/BaseSessionController'
export class AnnotatedSessionController extends BaseSessionController {
constructor(
@inject(TYPES.Auth_DeleteSessionForUser) override deleteSessionForUser: DeleteSessionForUser,
@inject(TYPES.Auth_DeletePreviousSessionsForUser)
override deletePreviousSessionsForUser: DeletePreviousSessionsForUser,
@inject(TYPES.Auth_DeleteOtherSessionsForUser)
override deleteOtherSessionsForUser: DeleteOtherSessionsForUser,
@inject(TYPES.Auth_RefreshSessionToken) override refreshSessionToken: RefreshSessionToken,
) {
super(deleteSessionForUser, deletePreviousSessionsForUser, refreshSessionToken)
super(deleteSessionForUser, deleteOtherSessionsForUser, refreshSessionToken)
}
@httpDelete('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)

View File

@@ -332,7 +332,7 @@ describe('AnnotatedUsersController', () => {
request.headers['user-agent'] = 'Google Chrome'
response.locals.user = user
changeCredentials.execute = jest.fn().mockReturnValue({ success: true, authResponse: { foo: 'bar' } })
changeCredentials.execute = jest.fn().mockReturnValue(Result.ok({ foo: 'bar' }))
const httpResponse = <results.JsonResult>await createController().changeCredentials(request, response)
const result = await httpResponse.executeAsync()
@@ -346,6 +346,7 @@ describe('AnnotatedUsersController', () => {
kpOrigination: 'change-password',
pwNonce: 'asdzxc',
protocolVersion: '004',
newEmail: undefined,
username: Username.create('test@test.te').getValue(),
})
@@ -385,7 +386,7 @@ describe('AnnotatedUsersController', () => {
request.headers['user-agent'] = 'Google Chrome'
response.locals.user = user
changeCredentials.execute = jest.fn().mockReturnValue({ success: false, errorMessage: 'Something bad happened' })
changeCredentials.execute = jest.fn().mockReturnValue(Result.fail('Something bad happened'))
const httpResponse = <results.JsonResult>await createController().changeCredentials(request, response)
const result = await httpResponse.executeAsync()

View File

@@ -3,14 +3,14 @@ import { Request, Response } from 'express'
import { BaseHttpController, results } from 'inversify-express-utils'
import { ErrorTag } from '@standardnotes/responses'
import { DeletePreviousSessionsForUser } from '../../../Domain/UseCase/DeletePreviousSessionsForUser'
import { DeleteOtherSessionsForUser } from '../../../Domain/UseCase/DeleteOtherSessionsForUser'
import { DeleteSessionForUser } from '../../../Domain/UseCase/DeleteSessionForUser'
import { RefreshSessionToken } from '../../../Domain/UseCase/RefreshSessionToken'
export class BaseSessionController extends BaseHttpController {
constructor(
protected deleteSessionForUser: DeleteSessionForUser,
protected deletePreviousSessionsForUser: DeletePreviousSessionsForUser,
protected deleteOtherSessionsForUser: DeleteOtherSessionsForUser,
protected refreshSessionToken: RefreshSessionToken,
private controllerContainer?: ControllerContainerInterface,
) {
@@ -106,9 +106,10 @@ export class BaseSessionController extends BaseHttpController {
)
}
await this.deletePreviousSessionsForUser.execute({
await this.deleteOtherSessionsForUser.execute({
userUuid: response.locals.user.uuid,
currentSessionUuid: response.locals.session.uuid,
markAsRevoked: true,
})
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
@@ -131,6 +132,7 @@ export class BaseSessionController extends BaseHttpController {
const result = await this.refreshSessionToken.execute({
accessToken: request.body.access_token,
refreshToken: request.body.refresh_token,
userAgent: <string>request.headers['user-agent'],
})
if (!result.success) {

View File

@@ -228,13 +228,13 @@ export class BaseUsersController extends BaseHttpController {
protocolVersion: request.body.version,
})
if (!changeCredentialsResult.success) {
if (changeCredentialsResult.isFailed()) {
await this.increaseLoginAttempts.execute({ email: response.locals.user.email })
return this.json(
{
error: {
message: changeCredentialsResult.errorMessage,
message: changeCredentialsResult.getError(),
},
},
401,
@@ -245,6 +245,6 @@ export class BaseUsersController extends BaseHttpController {
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
return this.json(changeCredentialsResult.authResponse)
return this.json(changeCredentialsResult.getValue())
}
}

View File

@@ -29,26 +29,6 @@ export class TypeORMEphemeralSessionRepository implements EphemeralSessionReposi
}
}
async updateTokensAndExpirationDates(
uuid: string,
hashedAccessToken: string,
hashedRefreshToken: string,
accessExpiration: Date,
refreshExpiration: Date,
): Promise<void> {
const session = await this.findOneByUuid(uuid)
if (!session) {
return
}
session.hashedAccessToken = hashedAccessToken
session.hashedRefreshToken = hashedRefreshToken
session.accessExpiration = accessExpiration
session.refreshExpiration = refreshExpiration
await this.save(session)
}
async findAllByUserUuid(userUuid: string): Promise<Array<EphemeralSession>> {
const ephemeralSessionUuidsJSON = await this.cacheEntryRepository.findUnexpiredOneByKey(
`${this.USER_SESSIONS_PREFIX}:${userUuid}`,
@@ -94,6 +74,8 @@ export class TypeORMEphemeralSessionRepository implements EphemeralSessionReposi
async save(ephemeralSession: EphemeralSession): Promise<void> {
const ttl = this.ephemeralSessionAge
ephemeralSession.updatedAt = this.timer.getUTCDate()
const stringifiedSession = JSON.stringify(ephemeralSession)
await this.cacheEntryRepository.save(

View File

@@ -7,6 +7,7 @@ import TYPES from '../../Bootstrap/Types'
import { Session } from '../../Domain/Session/Session'
import { SessionRepositoryInterface } from '../../Domain/Session/SessionRepositoryInterface'
import { Uuid } from '@standardnotes/domain-core'
@injectable()
export class TypeORMSessionRepository implements SessionRepositoryInterface {
@@ -17,6 +18,8 @@ export class TypeORMSessionRepository implements SessionRepositoryInterface {
) {}
async save(session: Session): Promise<Session> {
session.updatedAt = this.timer.getUTCDate()
return this.ormRepository.save(session)
}
@@ -40,32 +43,6 @@ export class TypeORMSessionRepository implements SessionRepositoryInterface {
.execute()
}
async updateHashedTokens(uuid: string, hashedAccessToken: string, hashedRefreshToken: string): Promise<void> {
await this.ormRepository
.createQueryBuilder('session')
.update()
.set({
hashedAccessToken,
hashedRefreshToken,
updatedAt: this.timer.getUTCDate(),
})
.where('uuid = :uuid', { uuid })
.execute()
}
async updatedTokenExpirationDates(uuid: string, accessExpiration: Date, refreshExpiration: Date): Promise<void> {
await this.ormRepository
.createQueryBuilder('session')
.update()
.set({
accessExpiration,
refreshExpiration,
updatedAt: this.timer.getUTCDate(),
})
.where('uuid = :uuid', { uuid })
.execute()
}
async findAllByRefreshExpirationAndUserUuid(userUuid: string): Promise<Session[]> {
return this.ormRepository
.createQueryBuilder('session')
@@ -100,13 +77,13 @@ export class TypeORMSessionRepository implements SessionRepositoryInterface {
.getMany()
}
async deleteAllByUserUuid(userUuid: string, currentSessionUuid: string): Promise<void> {
async deleteAllByUserUuidExceptOne(dto: { userUuid: Uuid; currentSessionUuid: Uuid }): Promise<void> {
await this.ormRepository
.createQueryBuilder('session')
.delete()
.where('user_uuid = :user_uuid AND uuid != :current_session_uuid', {
user_uuid: userUuid,
current_session_uuid: currentSessionUuid,
user_uuid: dto.userUuid.value,
current_session_uuid: dto.currentSessionUuid.value,
})
.execute()
}

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.25.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.25.1...@standardnotes/domain-core@1.25.2) (2023-08-09)
### Reverts
* Revert "Revert "feat(syncing-server): notify shared vault users upon file uploads or removals (#692)"" ([1c3ff52](https://github.com/standardnotes/server/commit/1c3ff526b7c4885f71f019f6c01142f522a6f8ad)), closes [#692](https://github.com/standardnotes/server/issues/692)
## [1.25.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.25.0...@standardnotes/domain-core@1.25.1) (2023-08-09)
### Reverts
* Revert "feat(syncing-server): notify shared vault users upon file uploads or removals (#692)" ([d261c81](https://github.com/standardnotes/server/commit/d261c81cd0bdbb9001c8589224f007ed2d338903)), closes [#692](https://github.com/standardnotes/server/issues/692)
# [1.25.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.24.2...@standardnotes/domain-core@1.25.0) (2023-08-09)
### Features
* **syncing-server:** notify shared vault users upon file uploads or removals ([#692](https://github.com/standardnotes/server/issues/692)) ([46867c1](https://github.com/standardnotes/server/commit/46867c1a4dd310c1971ff37e1bdf380c10e478fd))
## [1.24.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.24.1...@standardnotes/domain-core@1.24.2) (2023-08-02)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-core",
"version": "1.24.2",
"version": "1.25.2",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -6,6 +6,8 @@ export class NotificationType extends ValueObject<NotificationTypeProps> {
static readonly TYPES = {
SharedVaultItemRemoved: 'shared_vault_item_removed',
RemovedFromSharedVault: 'removed_from_shared_vault',
SharedVaultFileUploaded: 'shared_vault_file_uploaded',
SharedVaultFileRemoved: 'shared_vault_file_removed',
}
get value(): string {

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.12.11](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.10...@standardnotes/domain-events-infra@1.12.11) (2023-08-08)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.10](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.9...@standardnotes/domain-events-infra@1.12.10) (2023-08-03)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

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

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.
# [2.115.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.114.0...@standardnotes/domain-events@2.115.0) (2023-08-08)
### Features
* update storage quota used for user based on shared vault files ([#689](https://github.com/standardnotes/server/issues/689)) ([5311e74](https://github.com/standardnotes/server/commit/5311e7426617da6fc75593dd0fcbff589ca4fc22))
# [2.114.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.113.1...@standardnotes/domain-events@2.114.0) (2023-08-03)
### Features

View File

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

View File

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

View File

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

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.19](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.18...@standardnotes/event-store@1.11.19) (2023-08-09)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.18](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.17...@standardnotes/event-store@1.11.18) (2023-08-09)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.17](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.16...@standardnotes/event-store@1.11.17) (2023-08-09)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.16](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.15...@standardnotes/event-store@1.11.16) (2023-08-08)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.15](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.14...@standardnotes/event-store@1.11.15) (2023-08-03)
**Note:** Version bump only for package @standardnotes/event-store

View File

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

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.20.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.2...@standardnotes/files-server@1.20.3) (2023-08-09)
**Note:** Version bump only for package @standardnotes/files-server
## [1.20.2](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.1...@standardnotes/files-server@1.20.2) (2023-08-09)
**Note:** Version bump only for package @standardnotes/files-server
## [1.20.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.0...@standardnotes/files-server@1.20.1) (2023-08-09)
**Note:** Version bump only for package @standardnotes/files-server
# [1.20.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.18...@standardnotes/files-server@1.20.0) (2023-08-08)
### Features
* update storage quota used for user based on shared vault files ([#689](https://github.com/standardnotes/files/issues/689)) ([5311e74](https://github.com/standardnotes/files/commit/5311e7426617da6fc75593dd0fcbff589ca4fc22))
## [1.19.18](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.17...@standardnotes/files-server@1.19.18) (2023-08-03)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -7,6 +7,6 @@ module.exports = {
transform: {
...tsjPreset.transform,
},
coveragePathIgnorePatterns: ['/Bootstrap/', 'HealthCheckController', '/Infra/FS'],
coveragePathIgnorePatterns: ['/Bootstrap/', 'HealthCheckController', '/Infra/FS', '/Domain/Event/'],
setupFilesAfterEnv: ['./test-setup.ts'],
}

View File

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

View File

@@ -72,6 +72,16 @@ export class ContainerConfigLoader {
await import('newrelic')
}
// env vars
container.bind(TYPES.Files_VALET_TOKEN_SECRET).toConstantValue(env.get('VALET_TOKEN_SECRET'))
container
.bind(TYPES.Files_MAX_CHUNK_BYTES)
.toConstantValue(env.get('MAX_CHUNK_BYTES', true) ? +env.get('MAX_CHUNK_BYTES', true) : 100000000)
container.bind(TYPES.Files_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
container
.bind(TYPES.Files_FILE_UPLOAD_PATH)
.toConstantValue(env.get('FILE_UPLOAD_PATH', true) ?? `${__dirname}/../../uploads`)
const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server'
const isConfiguredForInMemoryCache = env.get('CACHE_TYPE', true) === 'memory'
@@ -85,6 +95,12 @@ export class ContainerConfigLoader {
container.bind<TimerInterface>(TYPES.Files_Timer).toConstantValue(new Timer())
// services
container
.bind<TokenDecoderInterface<ValetTokenData>>(TYPES.Files_ValetTokenDecoder)
.toConstantValue(new TokenDecoder<ValetTokenData>(container.get(TYPES.Files_VALET_TOKEN_SECRET)))
container.bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory).to(DomainEventFactory)
if (isConfiguredForInMemoryCache) {
container
.bind<UploadRepositoryInterface>(TYPES.Files_UploadRepository)
@@ -157,16 +173,6 @@ export class ContainerConfigLoader {
)
}
// env vars
container.bind(TYPES.Files_VALET_TOKEN_SECRET).toConstantValue(env.get('VALET_TOKEN_SECRET'))
container
.bind(TYPES.Files_MAX_CHUNK_BYTES)
.toConstantValue(env.get('MAX_CHUNK_BYTES', true) ? +env.get('MAX_CHUNK_BYTES', true) : 100000000)
container.bind(TYPES.Files_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
container
.bind(TYPES.Files_FILE_UPLOAD_PATH)
.toConstantValue(env.get('FILE_UPLOAD_PATH', true) ?? `${__dirname}/../../uploads`)
if (!isConfiguredForHomeServer && (env.get('S3_AWS_REGION', true) || env.get('S3_ENDPOINT', true))) {
const s3Opts: S3ClientConfig = {
apiVersion: 'latest',
@@ -198,7 +204,16 @@ export class ContainerConfigLoader {
container.bind<UploadFileChunk>(TYPES.Files_UploadFileChunk).to(UploadFileChunk)
container.bind<StreamDownloadFile>(TYPES.Files_StreamDownloadFile).to(StreamDownloadFile)
container.bind<CreateUploadSession>(TYPES.Files_CreateUploadSession).to(CreateUploadSession)
container.bind<FinishUploadSession>(TYPES.Files_FinishUploadSession).to(FinishUploadSession)
container
.bind<FinishUploadSession>(TYPES.Files_FinishUploadSession)
.toConstantValue(
new FinishUploadSession(
container.get(TYPES.Files_FileUploader),
container.get(TYPES.Files_UploadRepository),
container.get(TYPES.Files_DomainEventPublisher),
container.get(TYPES.Files_DomainEventFactory),
),
)
container.bind<GetFileMetadata>(TYPES.Files_GetFileMetadata).to(GetFileMetadata)
container.bind<RemoveFile>(TYPES.Files_RemoveFile).to(RemoveFile)
container.bind<MoveFile>(TYPES.Files_MoveFile).to(MoveFile)
@@ -210,12 +225,6 @@ export class ContainerConfigLoader {
.bind<SharedVaultValetTokenAuthMiddleware>(TYPES.Files_SharedVaultValetTokenAuthMiddleware)
.to(SharedVaultValetTokenAuthMiddleware)
// services
container
.bind<TokenDecoderInterface<ValetTokenData>>(TYPES.Files_ValetTokenDecoder)
.toConstantValue(new TokenDecoder<ValetTokenData>(container.get(TYPES.Files_VALET_TOKEN_SECRET)))
container.bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory).to(DomainEventFactory)
// Handlers
container
.bind<AccountDeletionRequestedEventHandler>(TYPES.Files_AccountDeletionRequestedEventHandler)

View File

@@ -1,126 +0,0 @@
import 'reflect-metadata'
import { TimerInterface } from '@standardnotes/time'
import { DomainEventFactory } from './DomainEventFactory'
describe('DomainEventFactory', () => {
let timer: TimerInterface
const createFactory = () => new DomainEventFactory(timer)
beforeEach(() => {
timer = {} as jest.Mocked<TimerInterface>
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({
fileByteSize: 123,
fileName: '2-3-4',
filePath: '1-2-3/2-3-4',
userUuid: '1-2-3',
}),
).toEqual({
createdAt: new Date(1),
meta: {
correlation: {
userIdentifier: '1-2-3',
userIdentifierType: 'uuid',
},
origin: 'files',
},
payload: {
fileByteSize: 123,
fileName: '2-3-4',
filePath: '1-2-3/2-3-4',
userUuid: '1-2-3',
},
type: 'FILE_UPLOADED',
})
})
it('should create a FILE_REMOVED event', () => {
expect(
createFactory().createFileRemovedEvent({
fileByteSize: 123,
fileName: '2-3-4',
filePath: '1-2-3/2-3-4',
userUuid: '1-2-3',
regularSubscriptionUuid: '1-2-3',
}),
).toEqual({
createdAt: new Date(1),
meta: {
correlation: {
userIdentifier: '1-2-3',
userIdentifierType: 'uuid',
},
origin: 'files',
},
payload: {
fileByteSize: 123,
fileName: '2-3-4',
filePath: '1-2-3/2-3-4',
userUuid: '1-2-3',
regularSubscriptionUuid: '1-2-3',
},
type: 'FILE_REMOVED',
})
})
})

View File

@@ -58,6 +58,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
createSharedVaultFileUploadedEvent(payload: {
sharedVaultUuid: string
vaultOwnerUuid: string
filePath: string
fileName: string
fileByteSize: number
@@ -78,6 +79,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
createSharedVaultFileRemovedEvent(payload: {
sharedVaultUuid: string
vaultOwnerUuid: string
filePath: string
fileName: string
fileByteSize: number

View File

@@ -21,12 +21,14 @@ export interface DomainEventFactoryInterface {
}): FileRemovedEvent
createSharedVaultFileUploadedEvent(payload: {
sharedVaultUuid: string
vaultOwnerUuid: string
filePath: string
fileName: string
fileByteSize: number
}): SharedVaultFileUploadedEvent
createSharedVaultFileRemovedEvent(payload: {
sharedVaultUuid: string
vaultOwnerUuid: string
filePath: string
fileName: string
fileByteSize: number

View File

@@ -1,11 +1,9 @@
import 'reflect-metadata'
import {
DomainEventPublisherInterface,
FileUploadedEvent,
SharedVaultFileUploadedEvent,
} from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { FileUploaderInterface } from '../../Services/FileUploaderInterface'
import { UploadRepositoryInterface } from '../../Upload/UploadRepositoryInterface'
@@ -17,10 +15,9 @@ describe('FinishUploadSession', () => {
let uploadRepository: UploadRepositoryInterface
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let logger: Logger
const createUseCase = () =>
new FinishUploadSession(fileUploader, uploadRepository, domainEventPublisher, domainEventFactory, logger)
new FinishUploadSession(fileUploader, uploadRepository, domainEventPublisher, domainEventFactory)
beforeEach(() => {
fileUploader = {} as jest.Mocked<FileUploaderInterface>
@@ -38,11 +35,6 @@ describe('FinishUploadSession', () => {
domainEventFactory.createSharedVaultFileUploadedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<SharedVaultFileUploadedEvent>)
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
logger.error = jest.fn()
logger.warn = jest.fn()
})
it('should not finish an upload session if non existing', async () => {
@@ -50,8 +42,7 @@ describe('FinishUploadSession', () => {
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
ownerUuid: '1-2-3',
ownerType: 'user',
userUuid: '00000000-0000-0000-0000-000000000000',
uploadBytesLimit: 100,
uploadBytesUsed: 0,
})
@@ -60,24 +51,33 @@ describe('FinishUploadSession', () => {
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should not finish an upload session user uuid is invalid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
userUuid: 'invalid',
uploadBytesLimit: 100,
uploadBytesUsed: 0,
})
expect(result.isFailed()).toBeTruthy()
expect(fileUploader.finishUploadSession).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should indicate of an error in finishing session fails', async () => {
uploadRepository.retrieveUploadSessionId = jest.fn().mockImplementation(() => {
throw new Error('oops')
})
expect(
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
ownerUuid: '1-2-3',
ownerType: 'user',
uploadBytesLimit: 100,
uploadBytesUsed: 0,
}),
).toEqual({
success: false,
message: 'Could not finish upload session',
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
userUuid: '00000000-0000-0000-0000-000000000000',
uploadBytesLimit: 100,
uploadBytesUsed: 0,
})
expect(result.getError()).toEqual('Could not finish upload session')
expect(fileUploader.finishUploadSession).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
@@ -85,13 +85,12 @@ describe('FinishUploadSession', () => {
it('should finish an upload session', async () => {
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
ownerUuid: '1-2-3',
ownerType: 'user',
userUuid: '00000000-0000-0000-0000-000000000000',
uploadBytesLimit: 100,
uploadBytesUsed: 0,
})
expect(fileUploader.finishUploadSession).toHaveBeenCalledWith('123', '1-2-3/2-3-4', [
expect(fileUploader.finishUploadSession).toHaveBeenCalledWith('123', '00000000-0000-0000-0000-000000000000/2-3-4', [
{ tag: '123', chunkId: 1, chunkSize: 1 },
])
expect(domainEventPublisher.publish).toHaveBeenCalled()
@@ -100,18 +99,32 @@ describe('FinishUploadSession', () => {
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',
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
uploadBytesLimit: 100,
uploadBytesUsed: 0,
})
expect(fileUploader.finishUploadSession).toHaveBeenCalledWith('123', '1-2-3/2-3-4', [
expect(fileUploader.finishUploadSession).toHaveBeenCalledWith('123', '00000000-0000-0000-0000-000000000000/2-3-4', [
{ tag: '123', chunkId: 1, chunkSize: 1 },
])
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('should not finish an upload session for a vault shared file if shared vault uuid is invalid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: 'invalid',
uploadBytesLimit: 100,
uploadBytesUsed: 0,
})
expect(result.isFailed()).toBeTruthy()
expect(fileUploader.finishUploadSession).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should not finish an upload session if the file size exceeds storage quota', async () => {
uploadRepository.retrieveUploadChunkResults = jest.fn().mockReturnValue([
{ tag: '123', chunkId: 1, chunkSize: 60 },
@@ -119,18 +132,13 @@ describe('FinishUploadSession', () => {
{ tag: '345', chunkId: 3, chunkSize: 20 },
])
expect(
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
ownerUuid: '1-2-3',
ownerType: 'user',
uploadBytesLimit: 100,
uploadBytesUsed: 20,
}),
).toEqual({
success: false,
message: 'Could not finish upload session. You are out of space.',
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
userUuid: '00000000-0000-0000-0000-000000000000',
uploadBytesLimit: 100,
uploadBytesUsed: 20,
})
expect(result.getError()).toEqual('Could not finish upload session. You are out of space.')
expect(fileUploader.finishUploadSession).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
@@ -143,17 +151,13 @@ describe('FinishUploadSession', () => {
{ tag: '345', chunkId: 3, chunkSize: 20 },
])
expect(
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
ownerUuid: '1-2-3',
ownerType: 'user',
uploadBytesLimit: -1,
uploadBytesUsed: 20,
}),
).toEqual({
success: true,
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
userUuid: '00000000-0000-0000-0000-000000000000',
uploadBytesLimit: -1,
uploadBytesUsed: 20,
})
expect(result.isFailed()).toBeFalsy()
expect(fileUploader.finishUploadSession).toHaveBeenCalled()
expect(domainEventPublisher.publish).toHaveBeenCalled()

View File

@@ -1,39 +1,41 @@
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import TYPES from '../../../Bootstrap/Types'
import { UseCaseInterface } from '../UseCaseInterface'
import { FinishUploadSessionDTO } from './FinishUploadSessionDTO'
import { FinishUploadSessionResponse } from './FinishUploadSessionResponse'
import { FileUploaderInterface } from '../../Services/FileUploaderInterface'
import { UploadRepositoryInterface } from '../../Upload/UploadRepositoryInterface'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
@injectable()
export class FinishUploadSession implements UseCaseInterface {
export class FinishUploadSession implements UseCaseInterface<void> {
constructor(
@inject(TYPES.Files_FileUploader) private fileUploader: FileUploaderInterface,
@inject(TYPES.Files_UploadRepository) private uploadRepository: UploadRepositoryInterface,
@inject(TYPES.Files_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.Files_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.Files_Logger) private logger: Logger,
private fileUploader: FileUploaderInterface,
private uploadRepository: UploadRepositoryInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
) {}
async execute(dto: FinishUploadSessionDTO): Promise<FinishUploadSessionResponse> {
async execute(dto: FinishUploadSessionDTO): Promise<Result<void>> {
try {
this.logger.debug(`Finishing upload session for resource: ${dto.resourceRemoteIdentifier}`)
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const filePath = `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`
let sharedVaultUuid: Uuid | undefined
if (dto.sharedVaultUuid !== undefined) {
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail(sharedVaultUuidOrError.getError())
}
sharedVaultUuid = sharedVaultUuidOrError.getValue()
}
const filePath = `${sharedVaultUuid ? sharedVaultUuid.value : userUuid.value}/${dto.resourceRemoteIdentifier}`
const uploadId = await this.uploadRepository.retrieveUploadSessionId(filePath)
if (uploadId === undefined) {
this.logger.warn(`Could not find upload session for file path: ${filePath}`)
return {
success: false,
message: 'Could not finish upload session',
}
return Result.fail('Could not finish upload session')
}
const uploadChunkResults = await this.uploadRepository.retrieveUploadChunkResults(uploadId)
@@ -46,46 +48,35 @@ export class FinishUploadSession implements UseCaseInterface {
const userHasUnlimitedStorage = dto.uploadBytesLimit === -1
const remainingSpaceLeft = dto.uploadBytesLimit - dto.uploadBytesUsed
if (!userHasUnlimitedStorage && remainingSpaceLeft < totalFileSize) {
return {
success: false,
message: 'Could not finish upload session. You are out of space.',
}
return Result.fail('Could not finish upload session. You are out of space.')
}
await this.fileUploader.finishUploadSession(uploadId, filePath, uploadChunkResults)
if (dto.ownerType === 'user') {
if (sharedVaultUuid !== undefined) {
await this.domainEventPublisher.publish(
this.domainEventFactory.createFileUploadedEvent({
userUuid: dto.ownerUuid,
filePath: `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
this.domainEventFactory.createSharedVaultFileUploadedEvent({
sharedVaultUuid: sharedVaultUuid.value,
vaultOwnerUuid: userUuid.value,
filePath,
fileName: dto.resourceRemoteIdentifier,
fileByteSize: totalFileSize,
}),
)
} else {
await this.domainEventPublisher.publish(
this.domainEventFactory.createSharedVaultFileUploadedEvent({
sharedVaultUuid: dto.ownerUuid,
filePath: `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
this.domainEventFactory.createFileUploadedEvent({
userUuid: userUuid.value,
filePath,
fileName: dto.resourceRemoteIdentifier,
fileByteSize: totalFileSize,
}),
)
}
return {
success: true,
}
return Result.ok()
} catch (error) {
this.logger.error(
`Could not finish upload session for resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`,
)
return {
success: false,
message: 'Could not finish upload session',
}
return Result.fail('Could not finish upload session')
}
}
}

View File

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

View File

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

View File

@@ -80,6 +80,7 @@ describe('RemoveFile', () => {
vaultInput: {
resourceRemoteIdentifier: '2-3-4',
sharedVaultUuid: '1-2-3',
vaultOwnerUuid: '3-4-5',
},
})

View File

@@ -43,6 +43,7 @@ export class RemoveFile implements UseCaseInterface<boolean> {
await this.domainEventPublisher.publish(
this.domainEventFactory.createSharedVaultFileRemovedEvent({
sharedVaultUuid: dto.vaultInput.sharedVaultUuid,
vaultOwnerUuid: dto.vaultInput.vaultOwnerUuid,
filePath: `${dto.vaultInput.sharedVaultUuid}/${dto.vaultInput.resourceRemoteIdentifier}`,
fileName: dto.vaultInput.resourceRemoteIdentifier,
fileByteSize: removedFileSize,

View File

@@ -6,6 +6,7 @@ export interface RemoveFileDTO {
}
vaultInput?: {
sharedVaultUuid: string
vaultOwnerUuid: string
resourceRemoteIdentifier: string
}
}

View File

@@ -1,19 +1,20 @@
import 'reflect-metadata'
import { ValetTokenOperation } from '@standardnotes/security'
import { BadRequestErrorMessageResult } from 'inversify-express-utils/lib/results'
import { Result } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { Request, Response } from 'express'
import { Writable, Readable } from 'stream'
import { results } from 'inversify-express-utils'
import { CreateUploadSession } from '../../Domain/UseCase/CreateUploadSession/CreateUploadSession'
import { FinishUploadSession } from '../../Domain/UseCase/FinishUploadSession/FinishUploadSession'
import { StreamDownloadFile } from '../../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
import { UploadFileChunk } from '../../Domain/UseCase/UploadFileChunk/UploadFileChunk'
import { Request, Response } from 'express'
import { Writable, Readable } from 'stream'
import { AnnotatedFilesController } from './AnnotatedFilesController'
import { GetFileMetadata } from '../../Domain/UseCase/GetFileMetadata/GetFileMetadata'
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('AnnotatedFilesController', () => {
let uploadFileChunk: UploadFileChunk
@@ -26,6 +27,7 @@ describe('AnnotatedFilesController', () => {
let response: Response
let readStream: Readable
const maxChunkBytes = 100_000
let logger: Logger
const createController = () =>
new AnnotatedFilesController(
@@ -36,9 +38,13 @@ describe('AnnotatedFilesController', () => {
getFileMetadata,
removeFile,
maxChunkBytes,
logger,
)
beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
readStream = {} as jest.Mocked<Readable>
readStream.pipe = jest.fn().mockReturnValue(new Writable())
@@ -52,7 +58,7 @@ describe('AnnotatedFilesController', () => {
createUploadSession.execute = jest.fn().mockReturnValue({ success: true, uploadId: '123' })
finishUploadSession = {} as jest.Mocked<FinishUploadSession>
finishUploadSession.execute = jest.fn().mockReturnValue({ success: true })
finishUploadSession.execute = jest.fn().mockReturnValue(Result.ok())
getFileMetadata = {} as jest.Mocked<GetFileMetadata>
getFileMetadata.execute = jest.fn().mockReturnValue({ success: true, size: 555_555 })
@@ -233,8 +239,7 @@ describe('AnnotatedFilesController', () => {
expect(finishUploadSession.execute).toHaveBeenCalledWith({
resourceRemoteIdentifier: '2-3-4',
ownerType: 'user',
ownerUuid: '1-2-3',
userUuid: '1-2-3',
})
})
@@ -249,7 +254,7 @@ describe('AnnotatedFilesController', () => {
it('should return bad request if upload session could not be finished', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
finishUploadSession.execute = jest.fn().mockReturnValue({ success: false })
finishUploadSession.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const httpResponse = await createController().finishUpload(request, response)
const result = await httpResponse.executeAsync()

View File

@@ -2,6 +2,9 @@ import { BaseHttpController, controller, httpDelete, httpGet, httpPost, results
import { Request, Response } from 'express'
import { inject } from 'inversify'
import { Writable } from 'stream'
import { ValetTokenOperation } from '@standardnotes/security'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { UploadFileChunk } from '../../Domain/UseCase/UploadFileChunk/UploadFileChunk'
import { StreamDownloadFile } from '../../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
@@ -9,7 +12,6 @@ import { CreateUploadSession } from '../../Domain/UseCase/CreateUploadSession/Cr
import { FinishUploadSession } from '../../Domain/UseCase/FinishUploadSession/FinishUploadSession'
import { GetFileMetadata } from '../../Domain/UseCase/GetFileMetadata/GetFileMetadata'
import { RemoveFile } from '../../Domain/UseCase/RemoveFile/RemoveFile'
import { ValetTokenOperation } from '@standardnotes/security'
@controller('/v1/files', TYPES.Files_ValetTokenAuthMiddleware)
export class AnnotatedFilesController extends BaseHttpController {
@@ -21,6 +23,7 @@ export class AnnotatedFilesController extends BaseHttpController {
@inject(TYPES.Files_GetFileMetadata) private getFileMetadata: GetFileMetadata,
@inject(TYPES.Files_RemoveFile) private removeFile: RemoveFile,
@inject(TYPES.Files_MAX_CHUNK_BYTES) private maxChunkBytes: number,
@inject(TYPES.Files_Logger) private logger: Logger,
) {
super()
}
@@ -85,15 +88,16 @@ export class AnnotatedFilesController extends BaseHttpController {
}
const result = await this.finishUploadSession.execute({
ownerUuid: response.locals.userUuid,
ownerType: 'user',
userUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
uploadBytesLimit: response.locals.uploadBytesLimit,
uploadBytesUsed: response.locals.uploadBytesUsed,
})
if (!result.success) {
return this.badRequest(result.message)
if (result.isFailed()) {
this.logger.error(result.getError())
return this.badRequest(result.getError())
}
return this.json({ success: true, message: 'File uploaded successfully' })

View File

@@ -3,6 +3,7 @@ import { Request, Response } from 'express'
import { inject } from 'inversify'
import { Writable } from 'stream'
import { SharedVaultValetTokenData, ValetTokenOperation } from '@standardnotes/security'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { CreateUploadSession } from '../../Domain/UseCase/CreateUploadSession/CreateUploadSession'
@@ -24,6 +25,7 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
@inject(TYPES.Files_RemoveFile) private removeFile: RemoveFile,
@inject(TYPES.Files_MoveFile) private moveFile: MoveFile,
@inject(TYPES.Files_MAX_CHUNK_BYTES) private maxChunkBytes: number,
@inject(TYPES.Files_Logger) private logger: Logger,
) {
super()
}
@@ -120,15 +122,17 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
}
const result = await this.finishUploadSession.execute({
ownerUuid: locals.sharedVaultUuid,
ownerType: 'shared-vault',
userUuid: locals.vaultOwnerUuid,
sharedVaultUuid: locals.sharedVaultUuid,
resourceRemoteIdentifier: locals.remoteIdentifier,
uploadBytesLimit: locals.uploadBytesLimit,
uploadBytesUsed: locals.uploadBytesUsed,
})
if (!result.success) {
return this.badRequest(result.message)
if (result.isFailed()) {
this.logger.error(result.getError())
return this.badRequest(result.getError())
}
return this.json({ success: true, message: 'File uploaded successfully' })
@@ -147,6 +151,7 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
const result = await this.removeFile.execute({
vaultInput: {
sharedVaultUuid: locals.sharedVaultUuid,
vaultOwnerUuid: locals.vaultOwnerUuid,
resourceRemoteIdentifier: locals.remoteIdentifier,
},
})

View File

@@ -63,6 +63,7 @@ export class SharedVaultValetTokenAuthMiddleware extends BaseMiddleware {
const whitelistedData: SharedVaultValetTokenData = {
sharedVaultUuid: valetTokenData.sharedVaultUuid,
vaultOwnerUuid: valetTokenData.vaultOwnerUuid,
remoteIdentifier: valetTokenData.remoteIdentifier,
permittedOperation: valetTokenData.permittedOperation,
uploadBytesUsed: valetTokenData.uploadBytesUsed,

View File

@@ -3,6 +3,62 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.13.42](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.41...@standardnotes/home-server@1.13.42) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.41](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.40...@standardnotes/home-server@1.13.41) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.40](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.39...@standardnotes/home-server@1.13.40) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.39](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.38...@standardnotes/home-server@1.13.39) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.38](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.37...@standardnotes/home-server@1.13.38) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.37](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.36...@standardnotes/home-server@1.13.37) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.36](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.35...@standardnotes/home-server@1.13.36) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.35](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.34...@standardnotes/home-server@1.13.35) (2023-08-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.34](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.33...@standardnotes/home-server@1.13.34) (2023-08-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.33](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.32...@standardnotes/home-server@1.13.33) (2023-08-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.32](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.31...@standardnotes/home-server@1.13.32) (2023-08-07)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.31](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.30...@standardnotes/home-server@1.13.31) (2023-08-07)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.30](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.29...@standardnotes/home-server@1.13.30) (2023-08-07)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.29](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.28...@standardnotes/home-server@1.13.29) (2023-08-03)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.28](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.27...@standardnotes/home-server@1.13.28) (2023-08-03)
**Note:** Version bump only for package @standardnotes/home-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.13.28",
"version": "1.13.42",
"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.26.6](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.5...@standardnotes/revisions-server@1.26.6) (2023-08-09)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.5](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.4...@standardnotes/revisions-server@1.26.5) (2023-08-09)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.4](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.3...@standardnotes/revisions-server@1.26.4) (2023-08-09)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.2...@standardnotes/revisions-server@1.26.3) (2023-08-08)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.1...@standardnotes/revisions-server@1.26.2) (2023-08-03)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.26.2",
"version": "1.26.6",
"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.20.21](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.20...@standardnotes/scheduler-server@1.20.21) (2023-08-09)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.20](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.19...@standardnotes/scheduler-server@1.20.20) (2023-08-09)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.19](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.18...@standardnotes/scheduler-server@1.20.19) (2023-08-09)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.18](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.17...@standardnotes/scheduler-server@1.20.18) (2023-08-08)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.17](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.16...@standardnotes/scheduler-server@1.20.17) (2023-08-03)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.20.17",
"version": "1.20.21",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

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.9.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.8.1...@standardnotes/security@1.9.0) (2023-08-08)
### Features
* update storage quota used for user based on shared vault files ([#689](https://github.com/standardnotes/server/issues/689)) ([5311e74](https://github.com/standardnotes/server/commit/5311e7426617da6fc75593dd0fcbff589ca4fc22))
## [1.8.1](https://github.com/standardnotes/server/compare/@standardnotes/security@1.8.0...@standardnotes/security@1.8.1) (2023-06-30)
**Note:** Version bump only for package @standardnotes/security

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/security",
"version": "1.8.1",
"version": "1.9.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,7 @@ import { SharedVaultMoveType } from './SharedVaultMoveType'
export interface SharedVaultValetTokenData {
sharedVaultUuid: string
vaultOwnerUuid: string
permittedOperation: ValetTokenOperation
remoteIdentifier: string
unencryptedFileSize?: number

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.21.24](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.23...@standardnotes/settings@1.21.24) (2023-08-09)
**Note:** Version bump only for package @standardnotes/settings
## [1.21.23](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.22...@standardnotes/settings@1.21.23) (2023-08-09)
**Note:** Version bump only for package @standardnotes/settings
## [1.21.22](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.21...@standardnotes/settings@1.21.22) (2023-08-09)
**Note:** Version bump only for package @standardnotes/settings
## [1.21.21](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.20...@standardnotes/settings@1.21.21) (2023-08-02)
**Note:** Version bump only for package @standardnotes/settings

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/settings",
"version": "1.21.21",
"version": "1.21.24",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,81 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.78.5](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.78.4...@standardnotes/syncing-server@1.78.5) (2023-08-09)
### Reverts
* Revert "tmp: disable fetching shared vault items" ([0eb86c0](https://github.com/standardnotes/syncing-server-js/commit/0eb86c009678a468bf9a7d0079dac58eff48f4d7))
* Revert "Revert "feat(syncing-server): notify shared vault users upon file uploads or removals (#692)"" ([1c3ff52](https://github.com/standardnotes/syncing-server-js/commit/1c3ff526b7c4885f71f019f6c01142f522a6f8ad)), closes [#692](https://github.com/standardnotes/syncing-server-js/issues/692)
## [1.78.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.78.3...@standardnotes/syncing-server@1.78.4) (2023-08-09)
### Bug Fixes
* **syncing-server:** casting handlers ([d7965b2](https://github.com/standardnotes/syncing-server-js/commit/d7965b2748ad59b1bff0cd6c0bf691303d9a6a76))
### Reverts
* Revert "Revert "fix(syncing-server): update storage quota used in a shared vault (#691)"" ([cbcd2ec](https://github.com/standardnotes/syncing-server-js/commit/cbcd2ec87ac5b94e06608da0426d7c27e5e56146)), closes [#691](https://github.com/standardnotes/syncing-server-js/issues/691)
## [1.78.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.78.2...@standardnotes/syncing-server@1.78.3) (2023-08-09)
### Reverts
* Revert "fix(syncing-server): update storage quota used in a shared vault (#691)" ([66f9352](https://github.com/standardnotes/syncing-server-js/commit/66f9352a062f45b5c66e7aae9681a56ca3ec6084)), closes [#691](https://github.com/standardnotes/syncing-server-js/issues/691)
## [1.78.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.78.1...@standardnotes/syncing-server@1.78.2) (2023-08-09)
### Reverts
* Revert "feat(syncing-server): notify shared vault users upon file uploads or removals (#692)" ([d261c81](https://github.com/standardnotes/syncing-server-js/commit/d261c81cd0bdbb9001c8589224f007ed2d338903)), closes [#692](https://github.com/standardnotes/syncing-server-js/issues/692)
## [1.78.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.78.0...@standardnotes/syncing-server@1.78.1) (2023-08-09)
**Note:** Version bump only for package @standardnotes/syncing-server
# [1.78.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.77.2...@standardnotes/syncing-server@1.78.0) (2023-08-09)
### Features
* **syncing-server:** notify shared vault users upon file uploads or removals ([#692](https://github.com/standardnotes/syncing-server-js/issues/692)) ([46867c1](https://github.com/standardnotes/syncing-server-js/commit/46867c1a4dd310c1971ff37e1bdf380c10e478fd))
## [1.77.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.77.1...@standardnotes/syncing-server@1.77.2) (2023-08-09)
### Bug Fixes
* **syncing-server:** update storage quota used in a shared vault ([#691](https://github.com/standardnotes/syncing-server-js/issues/691)) ([3415cae](https://github.com/standardnotes/syncing-server-js/commit/3415cae093ecd3631b924e722d4bd1d5015dd37a))
## [1.77.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.77.0...@standardnotes/syncing-server@1.77.1) (2023-08-08)
### Bug Fixes
* **syncing-server:** inviting already existing members to shared vault ([#690](https://github.com/standardnotes/syncing-server-js/issues/690)) ([0a16ee6](https://github.com/standardnotes/syncing-server-js/commit/0a16ee64fecc8d61d4a77fcf8c2c239691616000))
# [1.77.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.76.1...@standardnotes/syncing-server@1.77.0) (2023-08-08)
### Features
* update storage quota used for user based on shared vault files ([#689](https://github.com/standardnotes/syncing-server-js/issues/689)) ([5311e74](https://github.com/standardnotes/syncing-server-js/commit/5311e7426617da6fc75593dd0fcbff589ca4fc22))
## [1.76.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.76.0...@standardnotes/syncing-server@1.76.1) (2023-08-08)
### Bug Fixes
* **syncing-server:** race condition when adding admin user to newly created shared vault ([#688](https://github.com/standardnotes/syncing-server-js/issues/688)) ([3bd1547](https://github.com/standardnotes/syncing-server-js/commit/3bd1547ce3f599306f3942ce0a46f98cebfd33a4))
# [1.76.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.75.4...@standardnotes/syncing-server@1.76.0) (2023-08-07)
### Features
* **syncing-server:** limit shared vaults creation based on role ([#687](https://github.com/standardnotes/syncing-server-js/issues/687)) ([19b8921](https://github.com/standardnotes/syncing-server-js/commit/19b8921f286ff8f88c427e8ddd4512a8d61edb4f))
## [1.75.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.75.3...@standardnotes/syncing-server@1.75.4) (2023-08-03)
### Bug Fixes
* **syncing-server:** skip retrieval of items with invalid uuids ([#683](https://github.com/standardnotes/syncing-server-js/issues/683)) ([0036d52](https://github.com/standardnotes/syncing-server-js/commit/0036d527bd31cd81eda59e918b5f897f24cfa340))
## [1.75.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.75.2...@standardnotes/syncing-server@1.75.3) (2023-08-03)
**Note:** Version bump only for package @standardnotes/syncing-server

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