mirror of
https://github.com/standardnotes/server
synced 2026-01-17 05:04:27 -05:00
Compare commits
20 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6d655c5f5 | ||
|
|
46867c1a4d | ||
|
|
d29903bab6 | ||
|
|
3415cae093 | ||
|
|
408fd5a0c6 | ||
|
|
0a16ee64fe | ||
|
|
22b00479b4 | ||
|
|
5311e74266 | ||
|
|
5be7db7788 | ||
|
|
3bd1547ce3 | ||
|
|
a1fe15f7a9 | ||
|
|
19b8921f28 | ||
|
|
6b7879ba15 | ||
|
|
bd5f492a73 | ||
|
|
67311cc002 | ||
|
|
f39d3aca5b | ||
|
|
8e47491e3c | ||
|
|
0036d527bd | ||
|
|
f565f1d950 | ||
|
|
8e35dfa4b7 |
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [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
|
||||
|
||||
## [2.25.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.6...@standardnotes/analytics@2.25.7) (2023-08-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.25.7",
|
||||
"version": "2.25.10",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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.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
|
||||
|
||||
## [1.69.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.69.1...@standardnotes/api-gateway@1.69.2) (2023-08-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.69.2",
|
||||
"version": "1.70.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
* **auth:** add handling payments account deleted events STA-1769 ([#682](https://github.com/standardnotes/server/issues/682)) ([8e35dfa](https://github.com/standardnotes/server/commit/8e35dfa4b77256f4c0a3294b296a5526fd1020ad))
|
||||
|
||||
## [1.128.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.128.0...@standardnotes/auth-server@1.128.1) (2023-08-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.128.1",
|
||||
"version": "1.131.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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'
|
||||
@@ -252,6 +252,10 @@ import { BaseWebSocketsController } from '../Infra/InversifyExpressUtils/Base/Ba
|
||||
import { BaseSessionsController } from '../Infra/InversifyExpressUtils/Base/BaseSessionsController'
|
||||
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?: {
|
||||
@@ -826,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)
|
||||
@@ -883,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
|
||||
@@ -952,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)
|
||||
@@ -978,6 +1019,14 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_SettingService),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<PaymentsAccountDeletedEventHandler>(TYPES.Auth_PaymentsAccountDeletedEventHandler)
|
||||
.toConstantValue(
|
||||
new PaymentsAccountDeletedEventHandler(
|
||||
container.get(TYPES.Auth_DeleteAccount),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)],
|
||||
@@ -992,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)],
|
||||
[
|
||||
@@ -1005,6 +1056,7 @@ export class ContainerConfigLoader {
|
||||
],
|
||||
['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.Auth_PredicateVerificationRequestedEventHandler)],
|
||||
['EMAIL_SUBSCRIPTION_UNSUBSCRIBED', container.get(TYPES.Auth_EmailSubscriptionUnsubscribedEventHandler)],
|
||||
['PAYMENTS_ACCOUNT_DELETED', container.get(TYPES.Auth_PaymentsAccountDeletedEventHandler)],
|
||||
])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
@@ -1168,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),
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
@@ -176,6 +179,7 @@ const TYPES = {
|
||||
),
|
||||
Auth_PredicateVerificationRequestedEventHandler: Symbol.for('Auth_PredicateVerificationRequestedEventHandler'),
|
||||
Auth_EmailSubscriptionUnsubscribedEventHandler: Symbol.for('Auth_EmailSubscriptionUnsubscribedEventHandler'),
|
||||
Auth_PaymentsAccountDeletedEventHandler: Symbol.for('Auth_PaymentsAccountDeletedEventHandler'),
|
||||
// Services
|
||||
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
|
||||
Auth_SessionService: Symbol.for('Auth_SessionService'),
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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.')
|
||||
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Logger } from 'winston'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { PaymentsAccountDeletedEvent } from '@standardnotes/domain-events'
|
||||
|
||||
import { DeleteAccount } from '../UseCase/DeleteAccount/DeleteAccount'
|
||||
import { PaymentsAccountDeletedEventHandler } from './PaymentsAccountDeletedEventHandler'
|
||||
|
||||
describe('PaymentsAccountDeletedEventHandler', () => {
|
||||
let deleteAccountUseCase: DeleteAccount
|
||||
let logger: Logger
|
||||
let event: PaymentsAccountDeletedEvent
|
||||
|
||||
const createHandler = () => new PaymentsAccountDeletedEventHandler(deleteAccountUseCase, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
deleteAccountUseCase = {} as jest.Mocked<DeleteAccount>
|
||||
deleteAccountUseCase.execute = jest.fn().mockResolvedValue(Result.ok('success'))
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
|
||||
event = {
|
||||
payload: {
|
||||
username: 'username',
|
||||
},
|
||||
} as jest.Mocked<PaymentsAccountDeletedEvent>
|
||||
})
|
||||
|
||||
it('should delete account', async () => {
|
||||
const handler = createHandler()
|
||||
|
||||
await handler.handle(event)
|
||||
|
||||
expect(deleteAccountUseCase.execute).toHaveBeenCalledWith({
|
||||
username: 'username',
|
||||
})
|
||||
})
|
||||
|
||||
it('should log error if delete account fails', async () => {
|
||||
const handler = createHandler()
|
||||
|
||||
deleteAccountUseCase.execute = jest.fn().mockResolvedValue(Result.fail('error'))
|
||||
|
||||
await handler.handle(event)
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith('Failed to delete account for user username: error')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import { DomainEventHandlerInterface, PaymentsAccountDeletedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { DeleteAccount } from '../UseCase/DeleteAccount/DeleteAccount'
|
||||
|
||||
export class PaymentsAccountDeletedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private deleteAccountUseCase: DeleteAccount, private logger: Logger) {}
|
||||
|
||||
async handle(event: PaymentsAccountDeletedEvent): Promise<void> {
|
||||
const result = await this.deleteAccountUseCase.execute({
|
||||
username: event.payload.username,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to delete account for user ${event.payload.username}: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { AuthResponse20161215 } from '../../Auth/AuthResponse20161215'
|
||||
import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
|
||||
|
||||
export type ChangeCredentialsResponse = {
|
||||
success: boolean
|
||||
authResponse?: AuthResponse20161215 | AuthResponse20200115
|
||||
errorMessage?: string
|
||||
}
|
||||
@@ -35,6 +35,7 @@ describe('DeleteAccount', () => {
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
@@ -53,65 +54,124 @@ describe('DeleteAccount', () => {
|
||||
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(1)
|
||||
})
|
||||
|
||||
it('should trigger account deletion - no subscription', async () => {
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
|
||||
describe('when user uuid is provided', () => {
|
||||
it('should trigger account deletion - no subscription', async () => {
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
|
||||
|
||||
expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })).toEqual({
|
||||
message: 'Successfully deleted user',
|
||||
responseCode: 200,
|
||||
success: true,
|
||||
const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: undefined,
|
||||
it('should trigger account deletion - subscription present', async () => {
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription, sharedSubscription: null })
|
||||
|
||||
const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not trigger account deletion if user is not found', async () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not trigger account deletion if user uuid is invalid', async () => {
|
||||
const result = await createUseCase().execute({ userUuid: 'invalid' })
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should trigger account deletion - subscription present', async () => {
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription, sharedSubscription: null })
|
||||
describe('when username is provided', () => {
|
||||
it('should trigger account deletion - no subscription', async () => {
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
|
||||
|
||||
expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })).toEqual({
|
||||
message: 'Successfully deleted user',
|
||||
responseCode: 200,
|
||||
success: true,
|
||||
const result = await createUseCase().execute({ username: 'test@test.te' })
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
it('should trigger account deletion - subscription present', async () => {
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription, sharedSubscription: null })
|
||||
|
||||
const result = await createUseCase().execute({ username: 'test@test.te' })
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not trigger account deletion if user is not found', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await createUseCase().execute({ username: 'test@test.te' })
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not trigger account deletion if username is invalid', async () => {
|
||||
const result = await createUseCase().execute({ username: '' })
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not trigger account deletion if user is not found', async () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
describe('when neither user uuid nor username is provided', () => {
|
||||
it('should not trigger account deletion', async () => {
|
||||
const result = await createUseCase().execute({})
|
||||
|
||||
expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })).toEqual({
|
||||
message: 'User not found',
|
||||
responseCode: 404,
|
||||
success: false,
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not trigger account deletion if user uuid is invalid', async () => {
|
||||
expect(await createUseCase().execute({ userUuid: '' })).toEqual({
|
||||
message: 'Given value is not a valid uuid: ',
|
||||
responseCode: 400,
|
||||
success: false,
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { Result, UseCaseInterface, Username, Uuid } from '@standardnotes/domain-core'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
@@ -7,13 +7,12 @@ import TYPES from '../../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
|
||||
import { DeleteAccountDTO } from './DeleteAccountDTO'
|
||||
import { DeleteAccountResponse } from './DeleteAccountResponse'
|
||||
import { User } from '../../User/User'
|
||||
|
||||
@injectable()
|
||||
export class DeleteAccount implements UseCaseInterface {
|
||||
export class DeleteAccount implements UseCaseInterface<string> {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||
@@ -22,25 +21,30 @@ export class DeleteAccount implements UseCaseInterface {
|
||||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: DeleteAccountDTO): Promise<DeleteAccountResponse> {
|
||||
const uuidOrError = Uuid.create(dto.userUuid)
|
||||
if (uuidOrError.isFailed()) {
|
||||
return {
|
||||
success: false,
|
||||
responseCode: 400,
|
||||
message: uuidOrError.getError(),
|
||||
async execute(dto: DeleteAccountDTO): Promise<Result<string>> {
|
||||
let user: User | null = null
|
||||
if (dto.userUuid !== undefined) {
|
||||
const uuidOrError = Uuid.create(dto.userUuid)
|
||||
if (uuidOrError.isFailed()) {
|
||||
return Result.fail(uuidOrError.getError())
|
||||
}
|
||||
}
|
||||
const uuid = uuidOrError.getValue()
|
||||
const uuid = uuidOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUuid(uuid)
|
||||
user = await this.userRepository.findOneByUuid(uuid)
|
||||
} else if (dto.username !== undefined) {
|
||||
const usernameOrError = Username.create(dto.username)
|
||||
if (usernameOrError.isFailed()) {
|
||||
return Result.fail(usernameOrError.getError())
|
||||
}
|
||||
const username = usernameOrError.getValue()
|
||||
|
||||
user = await this.userRepository.findOneByUsernameOrEmail(username)
|
||||
} else {
|
||||
return Result.fail('Either userUuid or username must be provided.')
|
||||
}
|
||||
|
||||
if (user === null) {
|
||||
return {
|
||||
success: false,
|
||||
responseCode: 404,
|
||||
message: 'User not found',
|
||||
}
|
||||
return Result.ok('User already deleted.')
|
||||
}
|
||||
|
||||
let regularSubscriptionUuid = undefined
|
||||
@@ -57,10 +61,6 @@ export class DeleteAccount implements UseCaseInterface {
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Successfully deleted user',
|
||||
responseCode: 200,
|
||||
}
|
||||
return Result.ok('Successfully deleted account.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export type DeleteAccountDTO = {
|
||||
userUuid: string
|
||||
userUuid?: string
|
||||
username?: string
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export type DeleteAccountResponse = {
|
||||
success: boolean
|
||||
responseCode: number
|
||||
message: string
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export type DeleteOtherSessionsForUserDTO = {
|
||||
userUuid: string
|
||||
currentSessionUuid: string
|
||||
markAsRevoked: boolean
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export type DeletePreviousSessionsForUserDTO = {
|
||||
userUuid: string
|
||||
currentSessionUuid: string
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type DeletePreviousSessionsForUserResponse = {
|
||||
success: boolean
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type RefreshSessionTokenDTO = {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
userAgent: string
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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: {
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface UpdateStorageQuotaUsedForUserDTO {
|
||||
userUuid: string
|
||||
bytesUsed: number
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as express from 'express'
|
||||
|
||||
import { AnnotatedUsersController } from './AnnotatedUsersController'
|
||||
import { results } from 'inversify-express-utils'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { Result, Username } from '@standardnotes/domain-core'
|
||||
import { DeleteAccount } from '../../Domain/UseCase/DeleteAccount/DeleteAccount'
|
||||
import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
|
||||
import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
|
||||
@@ -45,7 +45,7 @@ describe('AnnotatedUsersController', () => {
|
||||
updateUser.execute = jest.fn()
|
||||
|
||||
deleteAccount = {} as jest.Mocked<DeleteAccount>
|
||||
deleteAccount.execute = jest.fn().mockReturnValue({ success: true, message: 'A OK', responseCode: 200 })
|
||||
deleteAccount.execute = jest.fn().mockReturnValue(Result.ok('success'))
|
||||
|
||||
user = {} as jest.Mocked<User>
|
||||
user.uuid = '123'
|
||||
@@ -181,7 +181,22 @@ describe('AnnotatedUsersController', () => {
|
||||
expect(deleteAccount.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
expect(await result.content.readAsStringAsync()).toEqual('{"message":"A OK"}')
|
||||
})
|
||||
|
||||
it('should indicate failure when deleting user', async () => {
|
||||
request.params.userUuid = '1-2-3'
|
||||
response.locals.user = {
|
||||
uuid: '1-2-3',
|
||||
}
|
||||
|
||||
deleteAccount.execute = jest.fn().mockReturnValue(Result.fail('Something bad happened'))
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().deleteAccount(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(deleteAccount.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('should not delete user if user uuid is different than the one in the session', async () => {
|
||||
@@ -317,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()
|
||||
@@ -331,6 +346,7 @@ describe('AnnotatedUsersController', () => {
|
||||
kpOrigination: 'change-password',
|
||||
pwNonce: 'asdzxc',
|
||||
protocolVersion: '004',
|
||||
newEmail: undefined,
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
})
|
||||
|
||||
@@ -370,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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -119,7 +119,18 @@ export class BaseUsersController extends BaseHttpController {
|
||||
userUuid: request.params.userUuid,
|
||||
})
|
||||
|
||||
return this.json({ message: result.message }, result.responseCode)
|
||||
if (result.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: result.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json({ message: result.getValue() }, 200)
|
||||
}
|
||||
|
||||
async getSubscription(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
@@ -217,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,
|
||||
@@ -234,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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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.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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-core",
|
||||
"version": "1.24.2",
|
||||
"version": "1.25.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.12.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
|
||||
|
||||
## [1.12.9](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.8...@standardnotes/domain-events-infra@1.12.9) (2023-07-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.12.9",
|
||||
"version": "1.12.11",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
# [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
|
||||
|
||||
* **auth:** add handling payments account deleted events STA-1769 ([#682](https://github.com/standardnotes/server/issues/682)) ([8e35dfa](https://github.com/standardnotes/server/commit/8e35dfa4b77256f4c0a3294b296a5526fd1020ad))
|
||||
|
||||
## [2.113.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.113.0...@standardnotes/domain-events@2.113.1) (2023-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.113.1",
|
||||
"version": "2.115.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
|
||||
import { PaymentsAccountDeletedEventPayload } from './PaymentsAccountDeletedEventPayload'
|
||||
|
||||
export interface PaymentsAccountDeletedEvent extends DomainEventInterface {
|
||||
type: 'PAYMENTS_ACCOUNT_DELETED'
|
||||
payload: PaymentsAccountDeletedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface PaymentsAccountDeletedEventPayload {
|
||||
username: string
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface SharedVaultFileRemovedEventPayload {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
fileByteSize: number
|
||||
filePath: string
|
||||
fileName: string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface SharedVaultFileUploadedEventPayload {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
fileByteSize: number
|
||||
filePath: string
|
||||
fileName: string
|
||||
|
||||
@@ -44,6 +44,8 @@ export * from './Event/MuteEmailsSettingChangedEvent'
|
||||
export * from './Event/MuteEmailsSettingChangedEventPayload'
|
||||
export * from './Event/PaymentFailedEvent'
|
||||
export * from './Event/PaymentFailedEventPayload'
|
||||
export * from './Event/PaymentsAccountDeletedEvent'
|
||||
export * from './Event/PaymentsAccountDeletedEventPayload'
|
||||
export * from './Event/PaymentSuccessEvent'
|
||||
export * from './Event/PaymentSuccessEventPayload'
|
||||
export * from './Event/PredicateVerificationRequestedEvent'
|
||||
|
||||
@@ -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.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
|
||||
|
||||
## [1.11.14](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.13...@standardnotes/event-store@1.11.14) (2023-08-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.11.14",
|
||||
"version": "1.11.17",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.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
|
||||
|
||||
## [1.19.17](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.16...@standardnotes/files-server@1.19.17) (2023-08-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.19.17",
|
||||
"version": "1.20.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type FinishUploadSessionDTO = {
|
||||
ownerUuid: string
|
||||
ownerType: 'user' | 'shared-vault'
|
||||
userUuid: string
|
||||
sharedVaultUuid?: string
|
||||
resourceRemoteIdentifier: string
|
||||
uploadBytesUsed: number
|
||||
uploadBytesLimit: number
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export type FinishUploadSessionResponse =
|
||||
| {
|
||||
success: true
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
message: string
|
||||
}
|
||||
@@ -80,6 +80,7 @@ describe('RemoveFile', () => {
|
||||
vaultInput: {
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
sharedVaultUuid: '1-2-3',
|
||||
vaultOwnerUuid: '3-4-5',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface RemoveFileDTO {
|
||||
}
|
||||
vaultInput?: {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
## [1.13.27](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.26...@standardnotes/home-server@1.13.27) (2023-08-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.13.27",
|
||||
"version": "1.13.37",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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.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
|
||||
|
||||
## [1.26.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.0...@standardnotes/revisions-server@1.26.1) (2023-08-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user