Compare commits

..

20 Commits

Author SHA1 Message Date
standardci
0b38617acf chore(release): publish new version
- @standardnotes/api-gateway@1.19.4
 - @standardnotes/auth-server@1.28.2
 - @standardnotes/domain-events-infra@1.8.10
 - @standardnotes/domain-events@2.60.4
 - @standardnotes/event-store@1.3.15
 - @standardnotes/files-server@1.5.52
 - @standardnotes/scheduler-server@1.10.29
 - @standardnotes/security@1.3.2
 - @standardnotes/syncing-server@1.8.4
2022-09-16 08:55:36 +00:00
Karol Sójko
377d32c449 fix(files): add verifying permitted operation on valet token 2022-09-16 10:52:25 +02:00
standardci
cdfb0c2603 chore(release): publish new version
- @standardnotes/auth-server@1.28.1
2022-09-15 12:19:43 +00:00
Karol Sójko
d85152429c fix(auth): missing injectable annotation 2022-09-15 14:17:56 +02:00
standardci
422e596fc7 chore(release): publish new version
- @standardnotes/api-gateway@1.19.3
2022-09-15 10:39:57 +00:00
Karol Sójko
89334c9022 fix(api-gateway): add remaining subscription time to stats 2022-09-15 12:38:28 +02:00
standardci
f5a0e88ab9 chore(release): publish new version
- @standardnotes/analytics@1.29.0
 - @standardnotes/api-gateway@1.19.2
 - @standardnotes/auth-server@1.28.0
 - @standardnotes/syncing-server@1.8.3
2022-09-15 10:23:29 +00:00
Karol Sójko
a59ba08339 feat(auth): add remaining subscription time stats 2022-09-15 12:21:59 +02:00
standardci
2641056c51 chore(release): publish new version
- @standardnotes/auth-server@1.27.0
2022-09-15 10:14:51 +00:00
Karol Sójko
5d812befc4 feat(auth): implement subscription server interface on server side 2022-09-15 12:12:50 +02:00
standardci
1c592d6f96 chore(release): publish new version
- @standardnotes/auth-server@1.26.1
2022-09-15 08:44:32 +00:00
Karol Sójko
531f13fe1f fix(auth): disallow duplicating subscription invites 2022-09-15 10:43:07 +02:00
standardci
4757cc8dae chore(release): publish new version
- @standardnotes/syncing-server@1.8.2
2022-09-15 08:27:49 +00:00
Karol Sójko
ecdfe9ecc0 fix(syncing-server): files count stats 2022-09-15 10:25:55 +02:00
standardci
d19cb08e9c chore(release): publish new version
- @standardnotes/auth-server@1.26.0
2022-09-13 13:48:14 +00:00
Karol Sójko
f45320e5ed feat(auth): add subscription sharing permission 2022-09-13 15:46:11 +02:00
standardci
93ded34de9 chore(release): publish new version
- @standardnotes/auth-server@1.25.13
2022-09-12 18:08:27 +00:00
Karol Sójko
dd13e2eaf7 fix(auth): add debug logs for canceling shared subscription invitations 2022-09-12 20:06:36 +02:00
standardci
1405c6f260 chore(release): publish new version
- @standardnotes/auth-server@1.25.12
2022-09-12 12:26:19 +00:00
Karol Sójko
0dab31f993 fix(auth): allow canceling shared subscription invitation before it was accepted 2022-09-12 14:24:52 +02:00
62 changed files with 775 additions and 286 deletions

109
.pnp.cjs generated
View File

@@ -2484,16 +2484,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/api", [\
["npm:1.1.19", {\
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.1.19-6a6d650ec9-cca168245a.zip/node_modules/@standardnotes/api/",\
["npm:1.7.2", {\
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.7.2-e68e7d4e63-bdfc414e6d.zip/node_modules/@standardnotes/api/",\
"packageDependencies": [\
["@standardnotes/api", "npm:1.1.19"],\
["@standardnotes/auth", "npm:3.19.4"],\
["@standardnotes/api", "npm:1.7.2"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/encryption", "npm:1.12.0"],\
["@standardnotes/responses", "npm:1.6.39"],\
["@standardnotes/services", "npm:1.15.0"],\
["@standardnotes/utils", "npm:1.6.12"]\
["@standardnotes/encryption", "npm:1.15.2"],\
["@standardnotes/models", "npm:1.18.2"],\
["@standardnotes/responses", "npm:1.10.1"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/utils", "npm:1.9.0"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
@@ -2562,7 +2563,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
["@sentry/node", "npm:7.5.0"],\
["@standardnotes/analytics", "workspace:packages/analytics"],\
["@standardnotes/api", "npm:1.1.19"],\
["@standardnotes/api", "npm:1.7.2"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
@@ -2687,16 +2688,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/encryption", [\
["npm:1.12.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.12.0-eb2342c675-1a28653b1e.zip/node_modules/@standardnotes/encryption/",\
["npm:1.15.2", {\
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.15.2-ef86a8281d-6e8336f1e7.zip/node_modules/@standardnotes/encryption/",\
"packageDependencies": [\
["@standardnotes/encryption", "npm:1.12.0"],\
["@standardnotes/encryption", "npm:1.15.2"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/models", "npm:1.14.0"],\
["@standardnotes/responses", "npm:1.6.39"],\
["@standardnotes/services", "npm:1.15.0"],\
["@standardnotes/sncrypto-common", "npm:1.9.0"],\
["@standardnotes/utils", "npm:1.6.12"],\
["@standardnotes/models", "npm:1.18.2"],\
["@standardnotes/responses", "npm:1.10.1"],\
["@standardnotes/sncrypto-common", "npm:1.11.1"],\
["@standardnotes/utils", "npm:1.9.0"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
@@ -2742,6 +2742,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.52.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.52.0-8c1adf7881-3e6014272f.zip/node_modules/@standardnotes/features/",\
"packageDependencies": [\
["@standardnotes/features", "npm:1.52.0"],\
["@standardnotes/auth", "npm:3.19.4"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/security", "workspace:packages/security"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/files-server", [\
@@ -2797,14 +2808,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/models", [\
["npm:1.14.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.14.0-6f064d99e7-bfb9d517b6.zip/node_modules/@standardnotes/models/",\
["npm:1.18.2", {\
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.18.2-56f35bb72d-88180a93e5.zip/node_modules/@standardnotes/models/",\
"packageDependencies": [\
["@standardnotes/models", "npm:1.14.0"],\
["@standardnotes/models", "npm:1.18.2"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.50.0"],\
["@standardnotes/responses", "npm:1.6.39"],\
["@standardnotes/utils", "npm:1.6.12"],\
["@standardnotes/features", "npm:1.52.0"],\
["@standardnotes/responses", "npm:1.10.1"],\
["@standardnotes/utils", "npm:1.9.0"],\
["lodash", "npm:4.17.21"],\
["reflect-metadata", "npm:0.1.13"]\
],\
@@ -2840,6 +2851,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/responses", [\
["npm:1.10.1", {\
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.10.1-9f82fff6c1-b84fb3f71c.zip/node_modules/@standardnotes/responses/",\
"packageDependencies": [\
["@standardnotes/responses", "npm:1.10.1"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.52.0"],\
["@standardnotes/security", "workspace:packages/security"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.6.39", {\
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.6.39-395f4c2d65-0ea1d4d5b8.zip/node_modules/@standardnotes/responses/",\
"packageDependencies": [\
@@ -2932,21 +2954,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "SOFT"\
}]\
]],\
["@standardnotes/services", [\
["npm:1.15.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-services-npm-1.15.0-acab3bc6a3-1028a5b4c1.zip/node_modules/@standardnotes/services/",\
"packageDependencies": [\
["@standardnotes/services", "npm:1.15.0"],\
["@standardnotes/auth", "npm:3.19.4"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/models", "npm:1.14.0"],\
["@standardnotes/responses", "npm:1.6.39"],\
["@standardnotes/utils", "npm:1.6.12"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/settings", [\
["workspace:packages/settings", {\
"packageLocation": "./packages/settings/",\
@@ -2960,6 +2967,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/sncrypto-common", [\
["npm:1.11.1", {\
"packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.11.1-58d12d6912-69d698abb7.zip/node_modules/@standardnotes/sncrypto-common/",\
"packageDependencies": [\
["@standardnotes/sncrypto-common", "npm:1.11.1"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.9.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.9.0-48773f745a-42252d7198.zip/node_modules/@standardnotes/sncrypto-common/",\
"packageDependencies": [\
@@ -3071,6 +3086,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["lodash", "npm:4.17.21"]\
],\
"linkType": "HARD"\
}],\
["npm:1.9.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.9.0-da939553f6-4591aff48d.zip/node_modules/@standardnotes/utils/",\
"packageDependencies": [\
["@standardnotes/utils", "npm:1.9.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["dompurify", "npm:2.4.0"],\
["lodash", "npm:4.17.21"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
]],\
["@szmarczak/http-timer", [\
@@ -5844,6 +5870,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["dompurify", "npm:2.3.8"]\
],\
"linkType": "HARD"\
}],\
["npm:2.4.0", {\
"packageLocation": "./.yarn/cache/dompurify-npm-2.4.0-0ffecf22ef-c93ea73cf8.zip/node_modules/dompurify/",\
"packageDependencies": [\
["dompurify", "npm:2.4.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["dot-prop", [\

Binary file not shown.

Binary file not shown.

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.29.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.28.0...@standardnotes/analytics@1.29.0) (2022-09-15)
### Features
* **auth:** add remaining subscription time stats ([a59ba08](https://github.com/standardnotes/server/commit/a59ba083397c75960af0e8a102b617bf5baa287f))
# [1.28.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.27.0...@standardnotes/analytics@1.28.0) (2022-09-09)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "1.28.0",
"version": "1.29.0",
"engines": {
"node": ">=14.0.0 <17.0.0"
},

View File

@@ -3,6 +3,7 @@ export enum StatisticsMeasure {
SubscriptionLength = 'subscription-length',
RegistrationLength = 'registration-length',
RegistrationToSubscriptionTime = 'registration-to-subscription-time',
SubscriptionCancelToExpireTime = 'subscription-cancel-to-expire-time',
Refunds = 'refunds',
NotesCountFreeUsers = 'notes-count-free-users',
NotesCountPaidUsers = 'notes-count-paid-users',

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.19.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.3...@standardnotes/api-gateway@1.19.4) (2022-09-16)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.19.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.2...@standardnotes/api-gateway@1.19.3) (2022-09-15)
### Bug Fixes
* **api-gateway:** add remaining subscription time to stats ([89334c9](https://github.com/standardnotes/api-gateway/commit/89334c90221045308d83fce9e97c146185d21389))
## [1.19.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.1...@standardnotes/api-gateway@1.19.2) (2022-09-15)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.19.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.0...@standardnotes/api-gateway@1.19.1) (2022-09-09)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -94,6 +94,7 @@ const requestReport = async (
StatisticsMeasure.RegistrationLength,
StatisticsMeasure.SubscriptionLength,
StatisticsMeasure.RegistrationToSubscriptionTime,
StatisticsMeasure.SubscriptionCancelToExpireTime,
StatisticsMeasure.NotesCountFreeUsers,
StatisticsMeasure.NotesCountPaidUsers,
StatisticsMeasure.FilesCount,

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.19.1",
"version": "1.19.4",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,54 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.28.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.1...@standardnotes/auth-server@1.28.2) (2022-09-16)
### Bug Fixes
* **files:** add verifying permitted operation on valet token ([377d32c](https://github.com/standardnotes/server/commit/377d32c4498305f0f59ff59e7357f0d2f10ce3a2))
## [1.28.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.0...@standardnotes/auth-server@1.28.1) (2022-09-15)
### Bug Fixes
* **auth:** missing injectable annotation ([d851524](https://github.com/standardnotes/server/commit/d85152429ca379d3d0314a9864cc46ebee541958))
# [1.28.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.27.0...@standardnotes/auth-server@1.28.0) (2022-09-15)
### Features
* **auth:** add remaining subscription time stats ([a59ba08](https://github.com/standardnotes/server/commit/a59ba083397c75960af0e8a102b617bf5baa287f))
# [1.27.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.26.1...@standardnotes/auth-server@1.27.0) (2022-09-15)
### Features
* **auth:** implement subscription server interface on server side ([5d812be](https://github.com/standardnotes/server/commit/5d812befc4733954919eef0d3718ae6f8eb81654))
## [1.26.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.26.0...@standardnotes/auth-server@1.26.1) (2022-09-15)
### Bug Fixes
* **auth:** disallow duplicating subscription invites ([531f13f](https://github.com/standardnotes/server/commit/531f13fe1f4bdfb8d27f5e3c07ec0b15d36ad413))
# [1.26.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.13...@standardnotes/auth-server@1.26.0) (2022-09-13)
### Features
* **auth:** add subscription sharing permission ([f45320e](https://github.com/standardnotes/server/commit/f45320e5ed8948a432029586c05284f4d640de5b))
## [1.25.13](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.12...@standardnotes/auth-server@1.25.13) (2022-09-12)
### Bug Fixes
* **auth:** add debug logs for canceling shared subscription invitations ([dd13e2e](https://github.com/standardnotes/server/commit/dd13e2eaf74de56a3c8c30c236c32c6dc0c560f2))
## [1.25.12](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.11...@standardnotes/auth-server@1.25.12) (2022-09-12)
### Bug Fixes
* **auth:** allow canceling shared subscription invitation before it was accepted ([0dab31f](https://github.com/standardnotes/server/commit/0dab31f9936bfd5081a87eef9701a268b8dec88c))
## [1.25.11](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.10...@standardnotes/auth-server@1.25.11) (2022-09-09)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -17,10 +17,10 @@ import '../src/Controller/SubscriptionTokensController'
import '../src/Controller/OfflineController'
import '../src/Controller/ValetTokenController'
import '../src/Controller/ListedController'
import '../src/Controller/SubscriptionInvitesController'
import '../src/Controller/SubscriptionSettingsController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController'
import * as cors from 'cors'
import { urlencoded, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'

View File

@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addSubscriptionSharingPermission1663073954000 implements MigrationInterface {
name = 'addSubscriptionSharingPermission1663073954000'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'INSERT INTO `permissions` (uuid, name) VALUES ("3aeaf12e-380f-4f21-97b9-d862d63874f6", "server:subscription-sharing")',
)
// Pro User Permissions
await queryRunner.query(
'INSERT INTO `role_permissions` (role_uuid, permission_uuid) VALUES \
("8047edbb-a10a-4ff8-8d53-c2cae600a8e8", "3aeaf12e-380f-4f21-97b9-d862d63874f6") \
',
)
}
public async down(): Promise<void> {
return
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.25.11",
"version": "1.28.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -34,7 +34,7 @@
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.3.0",
"@standardnotes/analytics": "workspace:*",
"@standardnotes/api": "^1.1.19",
"@standardnotes/api": "^1.7.2",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",

View File

@@ -200,6 +200,7 @@ import { MuteMarketingEmails } from '../Domain/UseCase/MuteMarketingEmails/MuteM
import { PaymentFailedEventHandler } from '../Domain/Handler/PaymentFailedEventHandler'
import { PaymentSuccessEventHandler } from '../Domain/Handler/PaymentSuccessEventHandler'
import { RefundProcessedEventHandler } from '../Domain/Handler/RefundProcessedEventHandler'
import { SubscriptionInvitesController } from '../Controller/SubscriptionInvitesController'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -262,6 +263,7 @@ export class ContainerConfigLoader {
// Controller
container.bind<AuthController>(TYPES.AuthController).to(AuthController)
container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
// Repositories
container.bind<SessionRepositoryInterface>(TYPES.SessionRepository).to(MySQLSessionRepository)

View File

@@ -5,6 +5,7 @@ const TYPES = {
SQS: Symbol.for('SQS'),
// Controller
AuthController: Symbol.for('AuthController'),
SubscriptionInvitesController: Symbol.for('SubscriptionInvitesController'),
// Repositories
UserRepository: Symbol.for('UserRepository'),
SessionRepository: Symbol.for('SessionRepository'),

View File

@@ -1,16 +1,13 @@
import 'reflect-metadata'
import * as express from 'express'
import { SubscriptionInvitesController } from './SubscriptionInvitesController'
import { results } from 'inversify-express-utils'
import { User } from '../Domain/User/User'
import { InviteToSharedSubscription } from '../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription'
import { AcceptSharedSubscriptionInvitation } from '../Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation'
import { DeclineSharedSubscriptionInvitation } from '../Domain/UseCase/DeclineSharedSubscriptionInvitation/DeclineSharedSubscriptionInvitation'
import { CancelSharedSubscriptionInvitation } from '../Domain/UseCase/CancelSharedSubscriptionInvitation/CancelSharedSubscriptionInvitation'
import { RoleName } from '@standardnotes/common'
import { ListSharedSubscriptionInvitations } from '../Domain/UseCase/ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
import { ApiVersion } from '@standardnotes/api'
describe('SubscriptionInvitesController', () => {
let inviteToSharedSubscription: InviteToSharedSubscription
@@ -19,8 +16,6 @@ describe('SubscriptionInvitesController', () => {
let cancelSharedSubscriptionInvitation: CancelSharedSubscriptionInvitation
let listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations
let request: express.Request
let response: express.Response
let user: User
const createController = () =>
@@ -51,25 +46,6 @@ describe('SubscriptionInvitesController', () => {
listSharedSubscriptionInvitations = {} as jest.Mocked<ListSharedSubscriptionInvitations>
listSharedSubscriptionInvitations.execute = jest.fn()
request = {
headers: {},
body: {},
params: {},
} as jest.Mocked<express.Request>
response = {
locals: {},
} as jest.Mocked<express.Response>
response.locals.user = {
email: 'test@test.te',
}
response.locals.roles = [
{
uuid: '1-2-3',
name: RoleName.CoreUser,
},
]
})
it('should get invitations to subscription sharing', async () => {
@@ -77,128 +53,127 @@ describe('SubscriptionInvitesController', () => {
invitations: [],
})
const httpResponse = <results.JsonResult>await createController().listInvites(request, response)
const result = await httpResponse.executeAsync()
const result = await createController().listInvites({ api: ApiVersion.v0, inviterEmail: 'test@test.te' })
expect(listSharedSubscriptionInvitations.execute).toHaveBeenCalledWith({
inviterEmail: 'test@test.te',
})
expect(result.statusCode).toEqual(200)
expect(result.status).toEqual(200)
})
it('should cancel invitation to subscription sharing', async () => {
request.params.inviteUuid = '1-2-3'
cancelSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
success: true,
})
const httpResponse = <results.JsonResult>await createController().cancelSubscriptionSharing(request, response)
const result = await httpResponse.executeAsync()
const result = await createController().cancelInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
inviterEmail: 'test@test.te',
})
expect(cancelSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
sharedSubscriptionInvitationUuid: '1-2-3',
inviterEmail: 'test@test.te',
})
expect(result.statusCode).toEqual(200)
expect(result.status).toEqual(200)
})
it('should not cancel invitation to subscription sharing if the workflow fails', async () => {
request.params.inviteUuid = '1-2-3'
cancelSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
success: false,
})
const httpResponse = <results.JsonResult>await createController().cancelSubscriptionSharing(request, response)
const result = await httpResponse.executeAsync()
const result = await createController().cancelInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(result.statusCode).toEqual(400)
expect(result.status).toEqual(400)
})
it('should decline invitation to subscription sharing', async () => {
request.params.inviteUuid = '1-2-3'
declineSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
success: true,
})
const httpResponse = <results.JsonResult>await createController().declineInvite(request)
const result = await httpResponse.executeAsync()
const result = await createController().declineInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(declineSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
sharedSubscriptionInvitationUuid: '1-2-3',
})
expect(result.statusCode).toEqual(200)
expect(result.status).toEqual(200)
})
it('should not decline invitation to subscription sharing if the workflow fails', async () => {
request.params.inviteUuid = '1-2-3'
declineSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
success: false,
})
const httpResponse = <results.JsonResult>await createController().declineInvite(request)
const result = await httpResponse.executeAsync()
const result = await createController().declineInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(declineSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
sharedSubscriptionInvitationUuid: '1-2-3',
})
expect(result.statusCode).toEqual(400)
expect(result.status).toEqual(400)
})
it('should accept invitation to subscription sharing', async () => {
request.params.inviteUuid = '1-2-3'
acceptSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
success: true,
})
const httpResponse = <results.JsonResult>await createController().acceptInvite(request)
const result = await httpResponse.executeAsync()
const result = await createController().acceptInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(acceptSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
sharedSubscriptionInvitationUuid: '1-2-3',
})
expect(result.statusCode).toEqual(200)
expect(result.status).toEqual(200)
})
it('should not accept invitation to subscription sharing if the workflow fails', async () => {
request.params.inviteUuid = '1-2-3'
acceptSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
success: false,
})
const httpResponse = <results.JsonResult>await createController().acceptInvite(request)
const result = await httpResponse.executeAsync()
const result = await createController().acceptInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(acceptSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
sharedSubscriptionInvitationUuid: '1-2-3',
})
expect(result.statusCode).toEqual(400)
expect(result.status).toEqual(400)
})
it('should invite to user subscription', async () => {
request.body.identifier = 'invitee@test.te'
response.locals.user = {
uuid: '1-2-3',
email: 'test@test.te',
}
inviteToSharedSubscription.execute = jest.fn().mockReturnValue({
success: true,
})
const httpResponse = <results.JsonResult>await createController().inviteToSubscriptionSharing(request, response)
const result = await httpResponse.executeAsync()
const result = await createController().invite({
api: ApiVersion.v0,
identifier: 'invitee@test.te',
inviterUuid: '1-2-3',
inviterEmail: 'test@test.te',
inviterRoles: ['CORE_USER'],
})
expect(inviteToSharedSubscription.execute).toHaveBeenCalledWith({
inviterEmail: 'test@test.te',
@@ -207,37 +182,36 @@ describe('SubscriptionInvitesController', () => {
inviterRoles: ['CORE_USER'],
})
expect(result.statusCode).toEqual(200)
expect(result.status).toEqual(200)
})
it('should not invite to user subscription if the identifier is missing in request', async () => {
response.locals.user = {
uuid: '1-2-3',
email: 'test@test.te',
}
const httpResponse = <results.JsonResult>await createController().inviteToSubscriptionSharing(request, response)
const result = await httpResponse.executeAsync()
const result = await createController().invite({
api: ApiVersion.v0,
identifier: '',
inviterUuid: '1-2-3',
inviterEmail: 'test@test.te',
inviterRoles: ['CORE_USER'],
})
expect(inviteToSharedSubscription.execute).not.toHaveBeenCalled()
expect(result.statusCode).toEqual(400)
expect(result.status).toEqual(400)
})
it('should not invite to user subscription if the workflow does not run', async () => {
request.body.identifier = 'invitee@test.te'
response.locals.user = {
uuid: '1-2-3',
email: 'test@test.te',
}
inviteToSharedSubscription.execute = jest.fn().mockReturnValue({
success: false,
})
const httpResponse = <results.JsonResult>await createController().inviteToSubscriptionSharing(request, response)
const result = await httpResponse.executeAsync()
const result = await createController().invite({
api: ApiVersion.v0,
identifier: 'invitee@test.te',
inviterUuid: '1-2-3',
inviterEmail: 'test@test.te',
inviterRoles: ['CORE_USER'],
})
expect(result.statusCode).toEqual(400)
expect(result.status).toEqual(400)
})
})

View File

@@ -1,15 +1,19 @@
import { Role } from '@standardnotes/security'
import { Request, Response } from 'express'
import { inject } from 'inversify'
import {
BaseHttpController,
controller,
httpDelete,
httpGet,
httpPost,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
results,
} from 'inversify-express-utils'
HttpStatusCode,
SubscriptionInviteAcceptRequestParams,
SubscriptionInviteAcceptResponse,
SubscriptionInviteCancelRequestParams,
SubscriptionInviteCancelResponse,
SubscriptionInviteDeclineRequestParams,
SubscriptionInviteDeclineResponse,
SubscriptionInviteListRequestParams,
SubscriptionInviteListResponse,
SubscriptionInviteRequestParams,
SubscriptionInviteResponse,
SubscriptionServerInterface,
} from '@standardnotes/api'
import { RoleName } from '@standardnotes/common'
import { inject, injectable } from 'inversify'
import TYPES from '../Bootstrap/Types'
import { AcceptSharedSubscriptionInvitation } from '../Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation'
@@ -18,8 +22,8 @@ import { DeclineSharedSubscriptionInvitation } from '../Domain/UseCase/DeclineSh
import { InviteToSharedSubscription } from '../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription'
import { ListSharedSubscriptionInvitations } from '../Domain/UseCase/ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
@controller('/subscription-invites')
export class SubscriptionInvitesController extends BaseHttpController {
@injectable()
export class SubscriptionInvitesController implements SubscriptionServerInterface {
constructor(
@inject(TYPES.InviteToSharedSubscription) private inviteToSharedSubscription: InviteToSharedSubscription,
@inject(TYPES.AcceptSharedSubscriptionInvitation)
@@ -30,75 +34,103 @@ export class SubscriptionInvitesController extends BaseHttpController {
private cancelSharedSubscriptionInvitation: CancelSharedSubscriptionInvitation,
@inject(TYPES.ListSharedSubscriptionInvitations)
private listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations,
) {
super()
}
) {}
@httpGet('/:inviteUuid/accept')
async acceptInvite(request: Request): Promise<results.JsonResult> {
async acceptInvite(params: SubscriptionInviteAcceptRequestParams): Promise<SubscriptionInviteAcceptResponse> {
const result = await this.acceptSharedSubscriptionInvitation.execute({
sharedSubscriptionInvitationUuid: request.params.inviteUuid,
sharedSubscriptionInvitationUuid: params.inviteUuid,
})
if (result.success) {
return this.json(result)
return {
status: HttpStatusCode.Success,
data: result,
}
}
return this.json(result, 400)
return {
status: HttpStatusCode.BadRequest,
data: result,
}
}
@httpGet('/:inviteUuid/decline')
async declineInvite(request: Request): Promise<results.JsonResult> {
async declineInvite(params: SubscriptionInviteDeclineRequestParams): Promise<SubscriptionInviteDeclineResponse> {
const result = await this.declineSharedSubscriptionInvitation.execute({
sharedSubscriptionInvitationUuid: request.params.inviteUuid,
sharedSubscriptionInvitationUuid: params.inviteUuid,
})
if (result.success) {
return this.json(result)
return {
status: HttpStatusCode.Success,
data: result,
}
}
return this.json(result, 400)
return {
status: HttpStatusCode.BadRequest,
data: result,
}
}
@httpPost('/', TYPES.ApiGatewayAuthMiddleware)
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
if (!request.body.identifier) {
return this.json({ error: { message: 'Missing invitee identifier' } }, 400)
async invite(params: SubscriptionInviteRequestParams): Promise<SubscriptionInviteResponse> {
if (!params.identifier) {
return {
status: HttpStatusCode.BadRequest,
data: {
error: {
message: 'Missing invitee identifier',
},
},
}
}
const result = await this.inviteToSharedSubscription.execute({
inviterEmail: response.locals.user.email,
inviterUuid: response.locals.user.uuid,
inviteeIdentifier: request.body.identifier,
inviterRoles: response.locals.roles.map((role: Role) => role.name),
inviterEmail: params.inviterEmail as string,
inviterUuid: params.inviterUuid as string,
inviteeIdentifier: params.identifier,
inviterRoles: params.inviterRoles as RoleName[],
})
if (result.success) {
return this.json(result)
return {
status: HttpStatusCode.Success,
data: result,
}
}
return this.json(result, 400)
return {
status: HttpStatusCode.BadRequest,
data: result,
}
}
@httpDelete('/:inviteUuid', TYPES.ApiGatewayAuthMiddleware)
async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
async cancelInvite(params: SubscriptionInviteCancelRequestParams): Promise<SubscriptionInviteCancelResponse> {
const result = await this.cancelSharedSubscriptionInvitation.execute({
sharedSubscriptionInvitationUuid: request.params.inviteUuid,
inviterEmail: response.locals.user.email,
sharedSubscriptionInvitationUuid: params.inviteUuid,
inviterEmail: params.inviterEmail as string,
})
if (result.success) {
return this.json(result)
return {
status: HttpStatusCode.Success,
data: result,
}
}
return this.json(result, 400)
return {
status: HttpStatusCode.BadRequest,
data: result,
}
}
@httpGet('/', TYPES.ApiGatewayAuthMiddleware)
async listInvites(_request: Request, response: Response): Promise<results.JsonResult> {
async listInvites(params: SubscriptionInviteListRequestParams): Promise<SubscriptionInviteListResponse> {
const result = await this.listSharedSubscriptionInvitations.execute({
inviterEmail: response.locals.user.email,
inviterEmail: params.inviterEmail as string,
})
return this.json(result)
return {
status: HttpStatusCode.Success,
data: result,
}
}
}

View File

@@ -12,6 +12,7 @@ import { CreateValetTokenPayload } from '@standardnotes/responses'
import TYPES from '../Bootstrap/Types'
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
import { ErrorTag } from '@standardnotes/common'
import { ValetTokenOperation } from '@standardnotes/security'
@controller('/valet-tokens', TYPES.ApiGatewayAuthMiddleware)
export class ValetTokenController extends BaseHttpController {
@@ -37,7 +38,7 @@ export class ValetTokenController extends BaseHttpController {
const createValetKeyResponse = await this.createValetKey.execute({
userUuid: response.locals.user.uuid,
operation: payload.operation,
operation: payload.operation as ValetTokenOperation,
resources: payload.resources,
})

View File

@@ -13,7 +13,6 @@ import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyti
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { User } from '../User/User'
import { UserSubscription } from '../Subscription/UserSubscription'
import { Logger } from 'winston'
describe('SubscriptionCancelledEventHandler', () => {
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
@@ -24,7 +23,6 @@ describe('SubscriptionCancelledEventHandler', () => {
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let timestamp: number
let logger: Logger
const createHandler = () =>
new SubscriptionCancelledEventHandler(
@@ -34,7 +32,6 @@ describe('SubscriptionCancelledEventHandler', () => {
getUserAnalyticsId,
analyticsStore,
statisticsStore,
logger,
)
beforeEach(() => {
@@ -75,9 +72,6 @@ describe('SubscriptionCancelledEventHandler', () => {
offline: false,
replaced: false,
}
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
})
it('should update subscription cancelled', async () => {

View File

@@ -14,7 +14,6 @@ import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/Offl
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { UserSubscription } from '../Subscription/UserSubscription'
import { Logger } from 'winston'
@injectable()
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
@@ -26,7 +25,6 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
async handle(event: SubscriptionCancelledEvent): Promise<void> {
if (event.payload.offline) {
@@ -50,14 +48,18 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
if (subscriptions.length !== 0) {
const lastSubscription = subscriptions.shift() as UserSubscription
const subscriptionLength = event.payload.timestamp - lastSubscription.createdAt
this.logger.info(
`Canceling subscription ${lastSubscription.uuid} - lasted for ${subscriptionLength} microseconds`,
)
await this.statisticsStore.incrementMeasure(StatisticsMeasure.SubscriptionLength, subscriptionLength, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
const remainingSubscriptionTime = lastSubscription.endsAt - event.payload.timestamp
await this.statisticsStore.incrementMeasure(
StatisticsMeasure.SubscriptionCancelToExpireTime,
remainingSubscriptionTime,
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
}
}
}

View File

@@ -7,5 +7,9 @@ export interface SharedSubscriptionInvitationRepositoryInterface {
findOneByUuidAndStatus(uuid: Uuid, status: InvitationStatus): Promise<SharedSubscriptionInvitation | null>
findOneByUuid(uuid: Uuid): Promise<SharedSubscriptionInvitation | null>
findByInviterEmail(inviterEmail: string): Promise<SharedSubscriptionInvitation[]>
findOneByInviteeAndInviterEmail(
inviteeEmail: string,
inviterEmail: string,
): Promise<SharedSubscriptionInvitation | null>
countByInviterEmailAndStatus(inviterEmail: Uuid, statuses: InvitationStatus[]): Promise<number>
}

View File

@@ -16,6 +16,7 @@ import { DomainEventPublisherInterface, SharedSubscriptionInvitationCanceledEven
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { InviterIdentifierType } from '../../SharedSubscription/InviterIdentifierType'
import { InviteeIdentifierType } from '../../SharedSubscription/InviteeIdentifierType'
import { Logger } from 'winston'
describe('CancelSharedSubscriptionInvitation', () => {
let sharedSubscriptionInvitationRepository: SharedSubscriptionInvitationRepositoryInterface
@@ -28,6 +29,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
let invitation: SharedSubscriptionInvitation
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let logger: Logger
const createUseCase = () =>
new CancelSharedSubscriptionInvitation(
@@ -38,6 +40,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
domainEventPublisher,
domainEventFactory,
timer,
logger,
)
beforeEach(() => {
@@ -60,6 +63,9 @@ describe('CancelSharedSubscriptionInvitation', () => {
inviteeIdentifierType: InviteeIdentifierType.Email,
} as jest.Mocked<SharedSubscriptionInvitation>
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
sharedSubscriptionInvitationRepository = {} as jest.Mocked<SharedSubscriptionInvitationRepositoryInterface>
sharedSubscriptionInvitationRepository.findOneByUuid = jest.fn().mockReturnValue(invitation)
sharedSubscriptionInvitationRepository.save = jest.fn()
@@ -126,7 +132,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
})
})
it('should cancel a shared subscription invitation without subscription removal is subscription is not found', async () => {
it('should cancel a shared subscription invitation without subscription removal if subscription is not found', async () => {
userSubscriptionRepository.findOneByUserUuidAndSubscriptionId = jest.fn().mockReturnValue(null)
expect(
@@ -175,7 +181,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
})
})
it('should not cancel a shared subscription invitation if invitee is not found', async () => {
it('should cancel a shared subscription invitation without subscription removal if invitee is not found', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({
@@ -183,20 +189,21 @@ describe('CancelSharedSubscriptionInvitation', () => {
inviterEmail: 'test@test.te',
}),
).toEqual({
success: false,
success: true,
})
})
it('should not cancel a shared subscription invitation if invitee is not found', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({
sharedSubscriptionInvitationUuid: '1-2-3',
inviterEmail: 'test@test.te',
}),
).toEqual({
success: false,
expect(sharedSubscriptionInvitationRepository.save).toHaveBeenCalledWith({
status: 'canceled',
subscriptionId: 3,
updatedAt: 1,
inviterIdentifier: 'test@test.te',
uuid: '1-2-3',
inviterIdentifierType: 'email',
inviteeIdentifier: 'invitee@test.te',
inviteeIdentifierType: 'email',
})
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.removeUserRole).not.toHaveBeenCalled()
})
it('should not cancel a shared subscription invitation if inviter subscription is not found', async () => {

View File

@@ -2,6 +2,7 @@ import { SubscriptionName } from '@standardnotes/common'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
@@ -29,6 +30,7 @@ export class CancelSharedSubscriptionInvitation implements UseCaseInterface {
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
async execute(dto: CancelSharedSubscriptionInvitationDTO): Promise<CancelSharedSubscriptionInvitationResponse> {
@@ -36,29 +38,34 @@ export class CancelSharedSubscriptionInvitation implements UseCaseInterface {
dto.sharedSubscriptionInvitationUuid,
)
if (sharedSubscriptionInvitation === null) {
this.logger.debug(
`Could not find a shared subscription invitation with uuid ${dto.sharedSubscriptionInvitationUuid}`,
)
return {
success: false,
}
}
if (dto.inviterEmail !== sharedSubscriptionInvitation.inviterIdentifier) {
this.logger.debug(
`Subscription belongs to a different inviter (${sharedSubscriptionInvitation.inviterIdentifier}). Modifier: ${dto.inviterEmail}`,
)
return {
success: false,
}
}
const invitee = await this.userRepository.findOneByEmail(sharedSubscriptionInvitation.inviteeIdentifier)
if (invitee === null) {
return {
success: false,
}
}
const inviterUserSubscriptions = await this.userSubscriptionRepository.findBySubscriptionIdAndType(
sharedSubscriptionInvitation.subscriptionId,
UserSubscriptionType.Regular,
)
if (inviterUserSubscriptions.length !== 1) {
if (inviterUserSubscriptions.length === 0) {
this.logger.debug(`Could not find a regular subscription with id ${sharedSubscriptionInvitation.subscriptionId}`)
return {
success: false,
}
@@ -70,20 +77,22 @@ export class CancelSharedSubscriptionInvitation implements UseCaseInterface {
await this.sharedSubscriptionInvitationRepository.save(sharedSubscriptionInvitation)
await this.removeSharedSubscription(sharedSubscriptionInvitation.subscriptionId, invitee)
if (invitee !== null) {
await this.removeSharedSubscription(sharedSubscriptionInvitation.subscriptionId, invitee)
await this.roleService.removeUserRole(invitee, inviterUserSubscription.planName as SubscriptionName)
await this.roleService.removeUserRole(invitee, inviterUserSubscription.planName as SubscriptionName)
await this.domainEventPublisher.publish(
this.domainEventFactory.createSharedSubscriptionInvitationCanceledEvent({
inviteeIdentifier: invitee.uuid,
inviteeIdentifierType: InviteeIdentifierType.Uuid,
inviterEmail: sharedSubscriptionInvitation.inviterIdentifier,
inviterSubscriptionId: sharedSubscriptionInvitation.subscriptionId,
inviterSubscriptionUuid: inviterUserSubscription.uuid,
sharedSubscriptionInvitationUuid: sharedSubscriptionInvitation.uuid,
}),
)
await this.domainEventPublisher.publish(
this.domainEventFactory.createSharedSubscriptionInvitationCanceledEvent({
inviteeIdentifier: invitee.uuid,
inviteeIdentifierType: InviteeIdentifierType.Uuid,
inviterEmail: sharedSubscriptionInvitation.inviterIdentifier,
inviterSubscriptionId: sharedSubscriptionInvitation.subscriptionId,
inviterSubscriptionUuid: inviterUserSubscription.uuid,
sharedSubscriptionInvitationUuid: sharedSubscriptionInvitation.uuid,
}),
)
}
return {
success: true,

View File

@@ -1,6 +1,6 @@
import 'reflect-metadata'
import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security'
import { TokenEncoderInterface, ValetTokenData, ValetTokenOperation } from '@standardnotes/security'
import { CreateValetToken } from './CreateValetToken'
import { TimerInterface } from '@standardnotes/time'
import { UserSubscription } from '../../Subscription/UserSubscription'
@@ -70,7 +70,7 @@ describe('CreateValetToken', () => {
it('should create a read valet token', async () => {
const response = await createUseCase().execute({
operation: 'read',
operation: ValetTokenOperation.Read,
userUuid: '1-2-3',
resources: [
{
@@ -92,7 +92,7 @@ describe('CreateValetToken', () => {
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
const response = await createUseCase().execute({
operation: 'read',
operation: ValetTokenOperation.Read,
userUuid: '1-2-3',
resources: [
{
@@ -117,7 +117,7 @@ describe('CreateValetToken', () => {
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(150)
const response = await createUseCase().execute({
operation: 'read',
operation: ValetTokenOperation.Read,
userUuid: '1-2-3',
resources: [
{
@@ -135,7 +135,7 @@ describe('CreateValetToken', () => {
it('should not create a write valet token if unencrypted file size has not been provided for a resource', async () => {
const response = await createUseCase().execute({
operation: 'write',
operation: ValetTokenOperation.Write,
resources: [
{
remoteIdentifier: '2-3-4',
@@ -152,7 +152,7 @@ describe('CreateValetToken', () => {
it('should create a write valet token', async () => {
const response = await createUseCase().execute({
operation: 'write',
operation: ValetTokenOperation.Write,
resources: [
{
remoteIdentifier: '2-3-4',
@@ -192,7 +192,7 @@ describe('CreateValetToken', () => {
.mockReturnValue({ regularSubscription, sharedSubscription })
const response = await createUseCase().execute({
operation: 'write',
operation: ValetTokenOperation.Write,
resources: [
{
remoteIdentifier: '2-3-4',
@@ -232,7 +232,7 @@ describe('CreateValetToken', () => {
.mockReturnValue({ regularSubscription: null, sharedSubscription })
const response = await createUseCase().execute({
operation: 'write',
operation: ValetTokenOperation.Write,
resources: [
{
remoteIdentifier: '2-3-4',
@@ -252,7 +252,7 @@ describe('CreateValetToken', () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
const response = await createUseCase().execute({
operation: 'write',
operation: ValetTokenOperation.Write,
userUuid: '1-2-3',
resources: [
{

View File

@@ -1,5 +1,10 @@
import { CreateValetTokenPayload } from '@standardnotes/responses'
import { ValetTokenOperation } from '@standardnotes/security'
export type CreateValetTokenDTO = CreateValetTokenPayload & {
export type CreateValetTokenDTO = {
operation: ValetTokenOperation
resources: Array<{
remoteIdentifier: string
unencryptedFileSize?: number
}>
userUuid: string
}

View File

@@ -10,6 +10,7 @@ import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubs
import { UserSubscription } from '../../Subscription/UserSubscription'
import { RoleName } from '@standardnotes/common'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
import { SharedSubscriptionInvitation } from '../../SharedSubscription/SharedSubscriptionInvitation'
describe('InviteToSharedSubscription', () => {
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
@@ -40,6 +41,7 @@ describe('InviteToSharedSubscription', () => {
sharedSubscriptionInvitationRepository = {} as jest.Mocked<SharedSubscriptionInvitationRepositoryInterface>
sharedSubscriptionInvitationRepository.save = jest.fn().mockImplementation((same) => ({ ...same, uuid: '1-2-3' }))
sharedSubscriptionInvitationRepository.countByInviterEmailAndStatus = jest.fn().mockReturnValue(2)
sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail = jest.fn().mockReturnValue(null)
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
@@ -181,4 +183,26 @@ describe('InviteToSharedSubscription', () => {
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should not create an invitation if it already exists', async () => {
sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail = jest
.fn()
.mockReturnValue({} as jest.Mocked<SharedSubscriptionInvitation>)
expect(
await createUseCase().execute({
inviteeIdentifier: 'invitee@test.te',
inviterUuid: '1-2-3',
inviterEmail: 'inviter@test.te',
inviterRoles: [RoleName.ProUser],
}),
).toEqual({
success: false,
})
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(domainEventFactory.createSharedSubscriptionInvitationCreatedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
})

View File

@@ -53,6 +53,16 @@ export class InviteToSharedSubscription implements UseCaseInterface {
}
}
const existingInvitation = await this.sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail(
dto.inviteeIdentifier,
dto.inviterEmail,
)
if (existingInvitation !== null) {
return {
success: false,
}
}
const sharedSubscriptionInvition = new SharedSubscriptionInvitation()
sharedSubscriptionInvition.inviterIdentifier = dto.inviterEmail
sharedSubscriptionInvition.inviterIdentifierType = InviterIdentifierType.Email

View File

@@ -0,0 +1,77 @@
import { ApiVersion } from '@standardnotes/api'
import { Role } from '@standardnotes/security'
import { Request, Response } from 'express'
import { inject } from 'inversify'
import {
BaseHttpController,
controller,
httpDelete,
httpGet,
httpPost,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
results,
} from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { SubscriptionInvitesController } from '../../Controller/SubscriptionInvitesController'
@controller('/subscription-invites')
export class InversifyExpressSubscriptionInvitesController extends BaseHttpController {
constructor(
@inject(TYPES.SubscriptionInvitesController) private subscriptionInvitesController: SubscriptionInvitesController,
) {
super()
}
@httpGet('/:inviteUuid/accept')
async acceptInvite(request: Request): Promise<results.JsonResult> {
const response = await this.subscriptionInvitesController.acceptInvite({
api: request.query.api as ApiVersion,
inviteUuid: request.params.inviteUuid,
})
return this.json(response.data, response.status)
}
@httpGet('/:inviteUuid/decline')
async declineInvite(request: Request): Promise<results.JsonResult> {
const response = await this.subscriptionInvitesController.declineInvite({
api: request.query.api as ApiVersion,
inviteUuid: request.params.inviteUuid,
})
return this.json(response.data, response.status)
}
@httpPost('/', TYPES.ApiGatewayAuthMiddleware)
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.subscriptionInvitesController.invite({
...request.body,
inviterEmail: response.locals.user.email,
inviterUuid: response.locals.user.uuid,
inviterRoles: response.locals.roles.map((role: Role) => role.name),
})
return this.json(result.data, result.status)
}
@httpDelete('/:inviteUuid', TYPES.ApiGatewayAuthMiddleware)
async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.subscriptionInvitesController.cancelInvite({
...request.body,
inviteUuid: request.params.inviteUuid,
inviterEmail: response.locals.user.email,
})
return this.json(result.data, result.status)
}
@httpGet('/', TYPES.ApiGatewayAuthMiddleware)
async listInvites(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.subscriptionInvitesController.listInvites({
...request.body,
inviterEmail: response.locals.user.email,
})
return this.json(result.data, result.status)
}
}

View File

@@ -70,6 +70,23 @@ describe('MySQLSharedSubscriptionInvitationRepository', () => {
expect(result).toEqual(invitation)
})
it('should find one invitation by invitee and inviter email', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(invitation)
const result = await createRepository().findOneByInviteeAndInviterEmail('invitee@test.te', 'inviter@test.te')
expect(queryBuilder.where).toHaveBeenCalledWith(
'invitation.inviter_identifier = :inviterEmail AND invitation.invitee_identifier = :inviteeEmail',
{
inviterEmail: 'inviter@test.te',
inviteeEmail: 'invitee@test.te',
},
)
expect(result).toEqual(invitation)
})
it('should find one invitation by uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(invitation)

View File

@@ -13,6 +13,19 @@ export class MySQLSharedSubscriptionInvitationRepository implements SharedSubscr
private ormRepository: Repository<SharedSubscriptionInvitation>,
) {}
async findOneByInviteeAndInviterEmail(
inviteeEmail: string,
inviterEmail: string,
): Promise<SharedSubscriptionInvitation | null> {
return this.ormRepository
.createQueryBuilder('invitation')
.where('invitation.inviter_identifier = :inviterEmail AND invitation.invitee_identifier = :inviteeEmail', {
inviterEmail,
inviteeEmail,
})
.getOne()
}
async save(sharedSubscriptionInvitation: SharedSubscriptionInvitation): Promise<SharedSubscriptionInvitation> {
return this.ormRepository.save(sharedSubscriptionInvitation)
}

View File

@@ -189,6 +189,7 @@ describe('MySQLUserSubscriptionRepository', () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([subscription])
const result = await createRepository().findBySubscriptionIdAndType(123, UserSubscriptionType.Regular)
@@ -200,6 +201,7 @@ describe('MySQLUserSubscriptionRepository', () => {
type: 'regular',
},
)
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('created_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual([subscription])
})

View File

@@ -44,6 +44,7 @@ export class MySQLUserSubscriptionRepository implements UserSubscriptionReposito
subscriptionId,
type,
})
.orderBy('created_at', 'DESC')
.getMany()
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.8.9",
"version": "1.8.10",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.60.4](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.3...@standardnotes/domain-events@2.60.4) (2022-09-16)
**Note:** Version bump only for package @standardnotes/domain-events
## [2.60.3](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.2...@standardnotes/domain-events@2.60.3) (2022-09-09)
**Note:** Version bump only for package @standardnotes/domain-events

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.60.3",
"version": "2.60.4",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.15](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.14...@standardnotes/event-store@1.3.15) (2022-09-16)
**Note:** Version bump only for package @standardnotes/event-store
## [1.3.14](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.13...@standardnotes/event-store@1.3.14) (2022-09-09)
**Note:** Version bump only for package @standardnotes/event-store

View File

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

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.5.52](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.51...@standardnotes/files-server@1.5.52) (2022-09-16)
### Bug Fixes
* **files:** add verifying permitted operation on valet token ([377d32c](https://github.com/standardnotes/files/commit/377d32c4498305f0f59ff59e7357f0d2f10ce3a2))
## [1.5.51](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.50...@standardnotes/files-server@1.5.51) (2022-09-09)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.5.51",
"version": "1.5.52",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -11,6 +11,8 @@ import { FilesController } from './FilesController'
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'
describe('FilesController', () => {
let uploadFileChunk: UploadFileChunk
@@ -75,6 +77,8 @@ describe('FilesController', () => {
})
it('should return a writable stream upon file download', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
request.headers['range'] = 'bytes=0-'
const result = (await createController().download(request, response)) as () => Writable
@@ -89,7 +93,19 @@ describe('FilesController', () => {
expect(result()).toBeInstanceOf(Writable)
})
it('should not allow download on invalid operation in the valet token', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
request.headers['range'] = 'bytes=0-'
const result = await createController().download(request, response)
expect(result).toBeInstanceOf(BadRequestErrorMessageResult)
})
it('should return proper byte range on consecutive calls', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
request.headers['range'] = 'bytes=0-'
;(await createController().download(request, response)) as () => Writable
@@ -112,6 +128,8 @@ describe('FilesController', () => {
})
it('should return a writable stream with custom chunk size', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
request.headers['x-chunk-size'] = '50000'
request.headers['range'] = 'bytes=0-'
@@ -128,6 +146,8 @@ describe('FilesController', () => {
})
it('should default to maximum chunk size if custom chunk size is too large', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
request.headers['x-chunk-size'] = '200000'
request.headers['range'] = 'bytes=0-'
@@ -144,12 +164,16 @@ describe('FilesController', () => {
})
it('should not return a writable stream if bytes range is not provided', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
const httpResponse = await createController().download(request, response)
expect(httpResponse).toBeInstanceOf(results.BadRequestErrorMessageResult)
})
it('should not return a writable stream if getting file metadata fails', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
request.headers['range'] = 'bytes=0-'
getFileMetadata.execute = jest.fn().mockReturnValue({ success: false, message: 'error' })
@@ -160,6 +184,8 @@ describe('FilesController', () => {
})
it('should not return a writable stream if creating download stream fails', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
request.headers['range'] = 'bytes=0-'
streamDownloadFile.execute = jest.fn().mockReturnValue({ success: false, message: 'error' })
@@ -170,6 +196,8 @@ describe('FilesController', () => {
})
it('should create an upload session', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
await createController().startUpload(request, response)
expect(createUploadSession.execute).toHaveBeenCalledWith({
@@ -178,7 +206,17 @@ describe('FilesController', () => {
})
})
it('should not create an upload session on invalid operation in the valet token', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
const result = await createController().startUpload(request, response)
expect(result).toBeInstanceOf(BadRequestErrorMessageResult)
})
it('should return bad request if upload session could not be created', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
createUploadSession.execute = jest.fn().mockReturnValue({ success: false })
const httpResponse = await createController().startUpload(request, response)
@@ -188,6 +226,8 @@ describe('FilesController', () => {
})
it('should finish an upload session', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
await createController().finishUpload(request, response)
expect(finishUploadSession.execute).toHaveBeenCalledWith({
@@ -196,7 +236,17 @@ describe('FilesController', () => {
})
})
it('should not finish an upload session on invalid operation in the valet token', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
const result = await createController().finishUpload(request, response)
expect(result).toBeInstanceOf(BadRequestErrorMessageResult)
})
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 })
const httpResponse = await createController().finishUpload(request, response)
@@ -206,6 +256,8 @@ describe('FilesController', () => {
})
it('should remove a file', async () => {
response.locals.permittedOperation = ValetTokenOperation.Delete
await createController().remove(request, response)
expect(removeFile.execute).toHaveBeenCalledWith({
@@ -215,6 +267,8 @@ describe('FilesController', () => {
})
it('should return bad request if file removal could not be completed', async () => {
response.locals.permittedOperation = ValetTokenOperation.Delete
removeFile.execute = jest.fn().mockReturnValue({ success: false })
const httpResponse = await createController().remove(request, response)
@@ -223,7 +277,18 @@ describe('FilesController', () => {
expect(result.statusCode).toEqual(400)
})
it('should return bad request if file removal is not permitted on valet token', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
const httpResponse = await createController().remove(request, response)
const result = await httpResponse.executeAsync()
expect(result.statusCode).toEqual(400)
})
it('should upload a chunk to an upload session', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
request.headers['x-chunk-id'] = '2'
request.body = Buffer.from([123])
@@ -238,6 +303,8 @@ describe('FilesController', () => {
})
it('should return bad request if chunk could not be uploaded', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
request.headers['x-chunk-id'] = '2'
request.body = Buffer.from([123])
uploadFileChunk.execute = jest.fn().mockReturnValue({ success: false })
@@ -248,7 +315,18 @@ describe('FilesController', () => {
expect(result.statusCode).toEqual(400)
})
it('should return bad request if valet token is not permitted', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
const httpResponse = await createController().uploadChunk(request, response)
const result = await httpResponse.executeAsync()
expect(result.statusCode).toEqual(400)
})
it('should return bad request if chunk id is missing', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
request.body = Buffer.from([123])
const httpResponse = await createController().uploadChunk(request, response)

View File

@@ -9,6 +9,7 @@ import { CreateUploadSession } from '../Domain/UseCase/CreateUploadSession/Creat
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.ValetTokenAuthMiddleware)
export class FilesController extends BaseHttpController {
@@ -29,6 +30,10 @@ export class FilesController extends BaseHttpController {
_request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
if (response.locals.permittedOperation !== ValetTokenOperation.Write) {
return this.badRequest('Not permitted for this operation')
}
const result = await this.createUploadSession.execute({
userUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
@@ -46,6 +51,10 @@ export class FilesController extends BaseHttpController {
request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
if (response.locals.permittedOperation !== ValetTokenOperation.Write) {
return this.badRequest('Not permitted for this operation')
}
const chunkId = +(request.headers['x-chunk-id'] as string)
if (!chunkId) {
return this.badRequest('Missing x-chunk-id header in request.')
@@ -70,6 +79,10 @@ export class FilesController extends BaseHttpController {
_request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
if (response.locals.permittedOperation !== ValetTokenOperation.Write) {
return this.badRequest('Not permitted for this operation')
}
const result = await this.finishUploadSession.execute({
userUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
@@ -89,6 +102,10 @@ export class FilesController extends BaseHttpController {
_request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
if (response.locals.permittedOperation !== ValetTokenOperation.Delete) {
return this.badRequest('Not permitted for this operation')
}
const result = await this.removeFile.execute({
userUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
@@ -107,6 +124,10 @@ export class FilesController extends BaseHttpController {
request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | (() => Writable)> {
if (response.locals.permittedOperation !== ValetTokenOperation.Read) {
return this.badRequest('Not permitted for this operation')
}
const range = request.headers['range']
if (!range) {
return this.badRequest('File download requires range header to be set.')

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.29](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.28...@standardnotes/scheduler-server@1.10.29) (2022-09-16)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.10.28](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.27...@standardnotes/scheduler-server@1.10.28) (2022-09-09)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.10.28",
"version": "1.10.29",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.2](https://github.com/standardnotes/server/compare/@standardnotes/security@1.3.1...@standardnotes/security@1.3.2) (2022-09-16)
### Bug Fixes
* **files:** add verifying permitted operation on valet token ([377d32c](https://github.com/standardnotes/server/commit/377d32c4498305f0f59ff59e7357f0d2f10ce3a2))
## [1.3.1](https://github.com/standardnotes/server/compare/@standardnotes/security@1.3.0...@standardnotes/security@1.3.1) (2022-09-09)
**Note:** Version bump only for package @standardnotes/security

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/security",
"version": "1.3.1",
"version": "1.3.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -1,10 +1,12 @@
import { Uuid } from '@standardnotes/common'
import { ValetTokenOperation } from './ValetTokenOperation'
export type ValetTokenData = {
userUuid: Uuid
sharedSubscriptionUuid: Uuid | undefined
regularSubscriptionUuid: Uuid
permittedOperation: 'read' | 'write' | 'delete'
permittedOperation: ValetTokenOperation
permittedResources: Array<{
remoteIdentifier: string
unencryptedFileSize?: number

View File

@@ -0,0 +1,5 @@
export enum ValetTokenOperation {
Read = 'read',
Write = 'write',
Delete = 'delete',
}

View File

@@ -11,3 +11,4 @@ export * from './Token/OfflineFeaturesTokenData'
export * from './Token/OfflineUserTokenData'
export * from './Token/SessionTokenData'
export * from './Token/ValetTokenData'
export * from './Token/ValetTokenOperation'

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.8.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.3...@standardnotes/syncing-server@1.8.4) (2022-09-16)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.8.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.2...@standardnotes/syncing-server@1.8.3) (2022-09-15)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.8.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.1...@standardnotes/syncing-server@1.8.2) (2022-09-15)
### Bug Fixes
* **syncing-server:** files count stats ([ecdfe9e](https://github.com/standardnotes/syncing-server-js/commit/ecdfe9ecc0bce882c1e3c6984f67b76862d76836))
## [1.8.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.0...@standardnotes/syncing-server@1.8.1) (2022-09-09)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.8.1",
"version": "1.8.4",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -112,10 +112,12 @@ export class CheckIntegrity implements UseCaseInterface {
[Period.Today, Period.ThisMonth],
)
await this.statisticsStore.incrementMeasure(StatisticsMeasure.FilesCount, counts.files, [
Period.Today,
Period.ThisMonth,
])
if (!freeUser) {
await this.statisticsStore.incrementMeasure(StatisticsMeasure.FilesCount, counts.files, [
Period.Today,
Period.ThisMonth,
])
}
}
}
}

132
yarn.lock
View File

@@ -1803,17 +1803,18 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/api@npm:^1.1.19":
version: 1.1.19
resolution: "@standardnotes/api@npm:1.1.19"
"@standardnotes/api@npm:^1.7.2":
version: 1.7.2
resolution: "@standardnotes/api@npm:1.7.2"
dependencies:
"@standardnotes/auth": ^3.19.4
"@standardnotes/common": ^1.23.1
"@standardnotes/encryption": ^1.8.23
"@standardnotes/responses": ^1.6.39
"@standardnotes/services": ^1.13.23
"@standardnotes/utils": ^1.6.12
checksum: cca168245a80d333ca6433799a7cbe4a233956cace92b9e9ec45b3f67e4e907ef4f08a9573008bdf2b11a09100dc0381cff820ee5bea384407c2107c494913ba
"@standardnotes/common": ^1.32.0
"@standardnotes/encryption": 1.15.2
"@standardnotes/models": 1.18.2
"@standardnotes/responses": 1.10.1
"@standardnotes/security": ^1.1.0
"@standardnotes/utils": 1.9.0
reflect-metadata: ^0.1.13
checksum: bdfc414e6d01620fd047979255a43eb447afbb69d1bb694015b162ad236431273cd234bba4129d13ba94791271aaff71895d726357491d6ab984c7d5a7a8a3f7
languageName: node
linkType: hard
@@ -1824,7 +1825,7 @@ __metadata:
"@newrelic/winston-enricher": ^4.0.0
"@sentry/node": ^7.3.0
"@standardnotes/analytics": "workspace:*"
"@standardnotes/api": ^1.1.19
"@standardnotes/api": ^1.7.2
"@standardnotes/common": "workspace:*"
"@standardnotes/domain-events": "workspace:*"
"@standardnotes/domain-events-infra": "workspace:*"
@@ -1885,7 +1886,7 @@ __metadata:
languageName: node
linkType: hard
"@standardnotes/common@^1.19.1, @standardnotes/common@^1.23.1, @standardnotes/common@workspace:*, @standardnotes/common@workspace:^, @standardnotes/common@workspace:packages/common":
"@standardnotes/common@^1.19.1, @standardnotes/common@^1.23.1, @standardnotes/common@^1.32.0, @standardnotes/common@workspace:*, @standardnotes/common@workspace:^, @standardnotes/common@workspace:packages/common":
version: 0.0.0-use.local
resolution: "@standardnotes/common@workspace:packages/common"
dependencies:
@@ -1950,18 +1951,17 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/encryption@npm:^1.8.23":
version: 1.12.0
resolution: "@standardnotes/encryption@npm:1.12.0"
"@standardnotes/encryption@npm:1.15.2":
version: 1.15.2
resolution: "@standardnotes/encryption@npm:1.15.2"
dependencies:
"@standardnotes/common": ^1.23.1
"@standardnotes/models": 1.14.0
"@standardnotes/responses": ^1.6.39
"@standardnotes/services": 1.15.0
"@standardnotes/sncrypto-common": ^1.9.0
"@standardnotes/utils": ^1.6.12
"@standardnotes/common": ^1.32.0
"@standardnotes/models": 1.18.2
"@standardnotes/responses": 1.10.1
"@standardnotes/sncrypto-common": 1.11.1
"@standardnotes/utils": 1.9.0
reflect-metadata: ^0.1.13
checksum: 1a28653b1e75c8c728fc7e68a64950eea2a291b339c7cd9f8672061ab9768ae7895fb75184b98e9046c296a96bb40d835dda7706ace973a948232f0f0655fcf7
checksum: 6e8336f1e7e961fbd42c4890458dca877da62dcc1987f7e9a7fb6ca230821276fce6a33652669bcc1752a80ffc55e4cf82b8631f7902d9714f4a07a7956092b0
languageName: node
linkType: hard
@@ -1993,7 +1993,19 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/features@npm:1.50.0, @standardnotes/features@npm:^1.36.3, @standardnotes/features@npm:^1.47.0":
"@standardnotes/features@npm:1.52.0":
version: 1.52.0
resolution: "@standardnotes/features@npm:1.52.0"
dependencies:
"@standardnotes/auth": ^3.19.4
"@standardnotes/common": ^1.32.0
"@standardnotes/security": ^1.2.0
reflect-metadata: ^0.1.13
checksum: 3e6014272f72ed33bc7de3cefb33a63a02866c01bfd4a54bc95426e2719f4997940de382cfd83982eaeafdbdf9afac558aecb9139117facfe9c7479089e2952d
languageName: node
linkType: hard
"@standardnotes/features@npm:^1.36.3, @standardnotes/features@npm:^1.47.0":
version: 1.50.0
resolution: "@standardnotes/features@npm:1.50.0"
dependencies:
@@ -2054,17 +2066,17 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/models@npm:1.14.0":
version: 1.14.0
resolution: "@standardnotes/models@npm:1.14.0"
"@standardnotes/models@npm:1.18.2":
version: 1.18.2
resolution: "@standardnotes/models@npm:1.18.2"
dependencies:
"@standardnotes/common": ^1.23.1
"@standardnotes/features": 1.50.0
"@standardnotes/responses": ^1.6.39
"@standardnotes/utils": ^1.6.12
"@standardnotes/common": ^1.32.0
"@standardnotes/features": 1.52.0
"@standardnotes/responses": 1.10.1
"@standardnotes/utils": 1.9.0
lodash: ^4.17.21
reflect-metadata: ^0.1.13
checksum: bfb9d517b6569d39e3f7bb700a644e430c7c88a2ce84c24d649efd8aac1fa94f222258fe08e1afa2614ffd73ac414911bbe39a597c1f6e9bfce6852a2c7ac776
checksum: 88180a93e5acdc349e1f96159c40610d7f52d49f0566386d9d6db8767d5ac4ba73af3131c8e433afa253557349e3f96238f6b2060e94df51ceedb5d378b3dd1f
languageName: node
linkType: hard
@@ -2093,6 +2105,18 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/responses@npm:1.10.1":
version: 1.10.1
resolution: "@standardnotes/responses@npm:1.10.1"
dependencies:
"@standardnotes/common": ^1.32.0
"@standardnotes/features": 1.52.0
"@standardnotes/security": ^1.1.0
reflect-metadata: ^0.1.13
checksum: b84fb3f71cc32286fc757280e01c2da7fd0576e96455bfd53c5e55f807875d7201a23e727a7c702277b90f1959837a9a0cbda94ca6a4f4ad6a4896e306ed851c
languageName: node
linkType: hard
"@standardnotes/responses@npm:^1.6.39":
version: 1.6.39
resolution: "@standardnotes/responses@npm:1.6.39"
@@ -2138,7 +2162,7 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/security@workspace:*, @standardnotes/security@workspace:packages/security":
"@standardnotes/security@^1.1.0, @standardnotes/security@^1.2.0, @standardnotes/security@workspace:*, @standardnotes/security@workspace:packages/security":
version: 0.0.0-use.local
resolution: "@standardnotes/security@workspace:packages/security"
dependencies:
@@ -2179,20 +2203,6 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/services@npm:1.15.0, @standardnotes/services@npm:^1.13.23":
version: 1.15.0
resolution: "@standardnotes/services@npm:1.15.0"
dependencies:
"@standardnotes/auth": ^3.19.4
"@standardnotes/common": ^1.23.1
"@standardnotes/models": 1.14.0
"@standardnotes/responses": ^1.6.39
"@standardnotes/utils": ^1.6.12
reflect-metadata: ^0.1.13
checksum: 1028a5b4c1372f13044115b3dea510a7e32479567161007472116f8a6168570735beeb32a5e795259f461bc983e75c4a4be72b8a927c60225c4057594ce139b2
languageName: node
linkType: hard
"@standardnotes/settings@workspace:*, @standardnotes/settings@workspace:packages/settings":
version: 0.0.0-use.local
resolution: "@standardnotes/settings@workspace:packages/settings"
@@ -2203,6 +2213,15 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/sncrypto-common@npm:1.11.1":
version: 1.11.1
resolution: "@standardnotes/sncrypto-common@npm:1.11.1"
dependencies:
reflect-metadata: ^0.1.13
checksum: 69d698abb7ffc2aecfffd9ccf3e023adca73e5b27cfa1106dfdf10a13d6455b9581c9bf854b333f00255317ec62c384c516b218f40a55ee84fd4f659b8aef16b
languageName: node
linkType: hard
"@standardnotes/sncrypto-common@npm:^1.9.0":
version: 1.9.0
resolution: "@standardnotes/sncrypto-common@npm:1.9.0"
@@ -2297,7 +2316,19 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/utils@npm:^1.4.6, @standardnotes/utils@npm:^1.6.12":
"@standardnotes/utils@npm:1.9.0":
version: 1.9.0
resolution: "@standardnotes/utils@npm:1.9.0"
dependencies:
"@standardnotes/common": ^1.32.0
dompurify: ^2.3.8
lodash: ^4.17.21
reflect-metadata: ^0.1.13
checksum: 4591aff48d074b30b911f96c63eaaf521ab49563507672fbd4d7fe460e51f88a45effb002d1c82cca3513d2199c0cdb720556b03ec3e0266f593317c8efa764a
languageName: node
linkType: hard
"@standardnotes/utils@npm:^1.4.6":
version: 1.6.12
resolution: "@standardnotes/utils@npm:1.6.12"
dependencies:
@@ -4471,6 +4502,13 @@ __metadata:
languageName: node
linkType: hard
"dompurify@npm:^2.3.8":
version: 2.4.0
resolution: "dompurify@npm:2.4.0"
checksum: c93ea73cf8e3ba044588450198563e56ce6902e36d0e16e3699df2fa59e82c4fdd11d4ad04ef5024569ce96a35b46f29d0bbea522516add33cd39a7f56a8a675
languageName: node
linkType: hard
"dot-prop@npm:^5.1.0":
version: 5.3.0
resolution: "dot-prop@npm:5.3.0"