mirror of
https://github.com/standardnotes/server
synced 2026-01-27 08:01:11 -05:00
Compare commits
23 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dea171115 | ||
|
|
aef9254713 | ||
|
|
31b7396006 | ||
|
|
be0a2649da | ||
|
|
bf8f91f83d | ||
|
|
effdfebc19 | ||
|
|
f4816e6c9a | ||
|
|
152a5cbd27 | ||
|
|
1488763115 | ||
|
|
bbb35d16fc | ||
|
|
ef07045ee9 | ||
|
|
3ba673b424 | ||
|
|
9c4032ebea | ||
|
|
05bb12c978 | ||
|
|
df957f07e3 | ||
|
|
b510284e01 | ||
|
|
205a1ed637 | ||
|
|
2073c735a5 | ||
|
|
34085ac6fb | ||
|
|
3d6559921b | ||
|
|
15a7f0e71a | ||
|
|
3e56243d6f | ||
|
|
032fcb938d |
24
.github/workflows/common-e2e.yml
vendored
24
.github/workflows/common-e2e.yml
vendored
@@ -19,7 +19,7 @@ on:
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
name: (Docker) E2E Test Suite
|
||||
name: (Self Hosting) E2E Test Suite
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -56,17 +56,8 @@ jobs:
|
||||
- name: Wait for server to start
|
||||
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
|
||||
|
||||
- name: Define if vault tests are enabled
|
||||
id: vaults
|
||||
run: |
|
||||
if [ "${{ matrix.secondary_db_enabled }}" = "true" ] && [ "${{ matrix.transition_mode_enabled }}" = "true" ]; then
|
||||
echo "vault-tests=enabled" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "vault-tests=disabled" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Run E2E Test Suite
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=${{ steps.vaults.outputs.vault-tests }}
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=enabled
|
||||
|
||||
- name: Show logs on failure
|
||||
if: ${{ failure() }}
|
||||
@@ -170,17 +161,8 @@ jobs:
|
||||
- name: Wait for server to start
|
||||
run: for i in {1..30}; do curl -s http://localhost:3123/healthcheck && break || sleep 1; done
|
||||
|
||||
- name: Define if vault tests are enabled
|
||||
id: vaults
|
||||
run: |
|
||||
if [ "${{ matrix.secondary_db_enabled }}" = "true" ] && [ "${{ matrix.transition_mode_enabled }}" = "true" ]; then
|
||||
echo "vault-tests=enabled" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "vault-tests=disabled" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Run E2E Test Suite
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=${{ steps.vaults.outputs.vault-tests }}
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=enabled
|
||||
|
||||
- name: Show logs on failure
|
||||
if: ${{ failure() }}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
# Setup environment variables
|
||||
|
||||
export MODE="self-hosted"
|
||||
|
||||
#########
|
||||
# PORTS #
|
||||
#########
|
||||
|
||||
@@ -147,10 +147,16 @@ LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $SYNCING_SERVER_
|
||||
echo "linking done:"
|
||||
echo "$LINKING_RESULT"
|
||||
|
||||
echo "linking topic $SYNCING_SERVER_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN"
|
||||
LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
|
||||
echo "linking topic $FILES_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN"
|
||||
LINKING_RESULT=$(link_queue_and_topic $FILES_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
|
||||
echo "linking done:"
|
||||
echo "$LINKING_RESULT"
|
||||
|
||||
echo "linking topic $SYNCING_SERVER_TOPIC_ARN to queue $AUTH_QUEUE_ARN"
|
||||
LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $AUTH_QUEUE_ARN)
|
||||
echo "linking done:"
|
||||
echo "$LINKING_RESULT"
|
||||
|
||||
echo "linking topic $AUTH_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN"
|
||||
LINKING_RESULT=$(link_queue_and_topic $AUTH_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
|
||||
echo "linking done:"
|
||||
|
||||
@@ -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.17](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.16...@standardnotes/analytics@2.25.17) (2023-08-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.15...@standardnotes/analytics@2.25.16) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.14...@standardnotes/analytics@2.25.15) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.13...@standardnotes/analytics@2.25.14) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.25.14",
|
||||
"version": "2.25.17",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
MODE=microservice # microservice | home-server
|
||||
MODE=microservice # microservice | home-server | self-hosted
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
VERSION=development
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.72.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.72.0...@standardnotes/api-gateway@1.72.1) (2023-08-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow self hosted to use new model of items ([#714](https://github.com/standardnotes/api-gateway/issues/714)) ([aef9254](https://github.com/standardnotes/api-gateway/commit/aef9254713560c00a90a3e84e3cd94417e8f30d2))
|
||||
|
||||
# [1.72.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.1...@standardnotes/api-gateway@1.72.0) (2023-08-24)
|
||||
|
||||
### Features
|
||||
|
||||
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/api-gateway/issues/707)) ([05bb12c](https://github.com/standardnotes/api-gateway/commit/05bb12c97899824f06e6d01d105dec75fc328440))
|
||||
|
||||
## [1.71.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.0...@standardnotes/api-gateway@1.71.1) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.71.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.5...@standardnotes/api-gateway@1.71.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/api-gateway/issues/704)) ([34085ac](https://github.com/standardnotes/api-gateway/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
|
||||
|
||||
## [1.70.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.4...@standardnotes/api-gateway@1.70.5) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.70.5",
|
||||
"version": "1.72.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -27,16 +27,23 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
}
|
||||
|
||||
const authHeaderValue = request.headers.authorization as string
|
||||
const sharedVaultOwnerContextHeaderValue = request.headers['x-shared-vault-owner-context'] as string | undefined
|
||||
const cacheKey = `${authHeaderValue}${
|
||||
sharedVaultOwnerContextHeaderValue ? `:${sharedVaultOwnerContextHeaderValue}` : ''
|
||||
}`
|
||||
|
||||
try {
|
||||
let crossServiceTokenFetchedFromCache = true
|
||||
let crossServiceToken = null
|
||||
if (this.crossServiceTokenCacheTTL) {
|
||||
crossServiceToken = await this.crossServiceTokenCache.get(authHeaderValue)
|
||||
crossServiceToken = await this.crossServiceTokenCache.get(cacheKey)
|
||||
}
|
||||
|
||||
if (crossServiceToken === null) {
|
||||
const authResponse = await this.serviceProxy.validateSession(authHeaderValue)
|
||||
if (this.crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken)) {
|
||||
const authResponse = await this.serviceProxy.validateSession({
|
||||
authorization: authHeaderValue,
|
||||
sharedVaultOwnerContext: sharedVaultOwnerContextHeaderValue,
|
||||
})
|
||||
|
||||
if (!this.handleSessionValidationResponse(authResponse, response, next)) {
|
||||
return
|
||||
@@ -48,12 +55,14 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
|
||||
response.locals.authToken = crossServiceToken
|
||||
|
||||
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
const decodedToken = <CrossServiceTokenData>(
|
||||
verify(response.locals.authToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
)
|
||||
|
||||
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
|
||||
await this.crossServiceTokenCache.set({
|
||||
authorizationHeaderValue: authHeaderValue,
|
||||
encodedCrossServiceToken: crossServiceToken,
|
||||
key: cacheKey,
|
||||
encodedCrossServiceToken: response.locals.authToken,
|
||||
expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
|
||||
userUuid: decodedToken.user.uuid,
|
||||
})
|
||||
@@ -62,6 +71,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
response.locals.user = decodedToken.user
|
||||
response.locals.session = decodedToken.session
|
||||
response.locals.roles = decodedToken.roles
|
||||
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
@@ -118,4 +128,14 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
|
||||
return Math.min(crossServiceTokenDefaultCacheExpiration, sessionAccessExpiration, sessionRefreshExpiration)
|
||||
}
|
||||
|
||||
private crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken: string | null) {
|
||||
if (crossServiceToken === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
|
||||
return decodedToken.ongoing_transition === true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,16 @@ export class ItemsController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
@httpPost('/transition')
|
||||
async transition(request: Request, response: Response): Promise<void> {
|
||||
await this.serviceProxy.callSyncingServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'items/transition'),
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/:uuid')
|
||||
async getItem(request: Request, response: Response): Promise<void> {
|
||||
await this.serviceProxy.callSyncingServer(
|
||||
|
||||
@@ -80,6 +80,15 @@ export class UsersController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/transition-status', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
|
||||
async getTransitionStatus(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier('GET', 'users/transition-status'),
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/:userId/params', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
|
||||
async getKeyParams(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
|
||||
@@ -12,29 +12,29 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
async set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
key: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void> {
|
||||
let userAuthHeaders = []
|
||||
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
|
||||
if (userAuthHeadersJSON) {
|
||||
userAuthHeaders = JSON.parse(userAuthHeadersJSON)
|
||||
let userKeys = []
|
||||
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
|
||||
if (userKeysJSON) {
|
||||
userKeys = JSON.parse(userKeysJSON)
|
||||
}
|
||||
userAuthHeaders.push(dto.authorizationHeaderValue)
|
||||
userKeys.push(dto.key)
|
||||
|
||||
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userAuthHeaders))
|
||||
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userKeys))
|
||||
this.crossServiceTokenTTLCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
|
||||
|
||||
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
|
||||
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
|
||||
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
|
||||
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
|
||||
}
|
||||
|
||||
async get(authorizationHeaderValue: string): Promise<string | null> {
|
||||
async get(key: string): Promise<string | null> {
|
||||
this.invalidateExpiredTokens()
|
||||
|
||||
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${key}`)
|
||||
if (!cachedToken) {
|
||||
return null
|
||||
}
|
||||
@@ -43,15 +43,15 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
|
||||
}
|
||||
|
||||
async invalidate(userUuid: string): Promise<void> {
|
||||
let userAuthorizationHeaderValues = []
|
||||
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
if (userAuthHeadersJSON) {
|
||||
userAuthorizationHeaderValues = JSON.parse(userAuthHeadersJSON)
|
||||
let userKeyValues = []
|
||||
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
if (userKeysJSON) {
|
||||
userKeyValues = JSON.parse(userKeysJSON)
|
||||
}
|
||||
|
||||
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
|
||||
this.crossServiceTokenCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
for (const key of userKeyValues) {
|
||||
this.crossServiceTokenCache.delete(`${this.PREFIX}:${key}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${key}`)
|
||||
}
|
||||
this.crossServiceTokenCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
@@ -12,32 +12,32 @@ export class RedisCrossServiceTokenCache implements CrossServiceTokenCacheInterf
|
||||
constructor(@inject(TYPES.ApiGateway_Redis) private redisClient: IORedis.Redis) {}
|
||||
|
||||
async set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
key: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void> {
|
||||
const pipeline = this.redisClient.pipeline()
|
||||
|
||||
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.authorizationHeaderValue)
|
||||
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.key)
|
||||
pipeline.expireat(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
|
||||
|
||||
pipeline.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
|
||||
pipeline.expireat(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
|
||||
pipeline.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
|
||||
pipeline.expireat(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
|
||||
|
||||
await pipeline.exec()
|
||||
}
|
||||
|
||||
async get(authorizationHeaderValue: string): Promise<string | null> {
|
||||
return this.redisClient.get(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
async get(key: string): Promise<string | null> {
|
||||
return this.redisClient.get(`${this.PREFIX}:${key}`)
|
||||
}
|
||||
|
||||
async invalidate(userUuid: string): Promise<void> {
|
||||
const userAuthorizationHeaderValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
const userKeyValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
const pipeline = this.redisClient.pipeline()
|
||||
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
|
||||
pipeline.del(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
for (const key of userKeyValues) {
|
||||
pipeline.del(`${this.PREFIX}:${key}`)
|
||||
}
|
||||
pipeline.del(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export interface CrossServiceTokenCacheInterface {
|
||||
set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
key: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void>
|
||||
get(authorizationHeaderValue: string): Promise<string | null>
|
||||
get(key: string): Promise<string | null>
|
||||
invalidate(userUuid: string): Promise<void>
|
||||
}
|
||||
|
||||
@@ -24,14 +24,16 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async validateSession(
|
||||
authorizationHeaderValue: string,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
async validateSession(headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
const authResponse = await this.httpClient.request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: authorizationHeaderValue,
|
||||
Authorization: headers.authorization,
|
||||
Accept: 'application/json',
|
||||
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
|
||||
},
|
||||
validateStatus: (status: number) => {
|
||||
return status >= 200 && status < 500
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface ServiceProxyInterface {
|
||||
endpointOrMethodIdentifier: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void>
|
||||
validateSession(authorizationHeaderValue: string): Promise<{
|
||||
validateSession(headers: { authorization: string; sharedVaultOwnerContext?: string }): Promise<{
|
||||
status: number
|
||||
data: unknown
|
||||
headers: {
|
||||
|
||||
@@ -6,9 +6,10 @@ import { ServiceProxyInterface } from '../Http/ServiceProxyInterface'
|
||||
export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
constructor(private serviceContainer: ServiceContainerInterface, private filesServerUrl: string) {}
|
||||
|
||||
async validateSession(
|
||||
authorizationHeaderValue: string,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
async validateSession(headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
const authService = this.serviceContainer.get(ServiceIdentifier.create(ServiceIdentifier.NAMES.Auth).getValue())
|
||||
if (!authService) {
|
||||
throw new Error('Auth service not found')
|
||||
@@ -17,7 +18,8 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
const serviceResponse = (await authService.handleRequest(
|
||||
{
|
||||
headers: {
|
||||
authorization: authorizationHeaderValue,
|
||||
authorization: headers.authorization,
|
||||
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
|
||||
},
|
||||
} as never,
|
||||
{} as never,
|
||||
|
||||
@@ -42,7 +42,8 @@ export class EndpointResolver implements EndpointResolverInterface {
|
||||
// Users Controller
|
||||
['[PATCH]:users/:userId', 'auth.users.update'],
|
||||
['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'],
|
||||
['[PUT]:auth/params', 'auth.users.getKeyParams'],
|
||||
['[GET]:users/params', 'auth.users.getKeyParams'],
|
||||
['[GET]:users/transition-status', 'auth.users.transition-status'],
|
||||
['[DELETE]:users/:userUuid', 'auth.users.delete'],
|
||||
['[POST]:listed', 'auth.users.createListedAccount'],
|
||||
['[POST]:auth', 'auth.users.register'],
|
||||
@@ -58,6 +59,7 @@ export class EndpointResolver implements EndpointResolverInterface {
|
||||
// Syncing Server
|
||||
['[POST]:items/sync', 'sync.items.sync'],
|
||||
['[POST]:items/check-integrity', 'sync.items.check_integrity'],
|
||||
['[POST]:items/transition', 'sync.items.transition'],
|
||||
['[GET]:items/:uuid', 'sync.items.get_item'],
|
||||
// Revisions Controller V2
|
||||
['[GET]:items/:itemUuid/revisions', 'revisions.revisions.getRevisions'],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
MODE=microservice # microservice | home-server
|
||||
MODE=microservice # microservice | home-server | self-hosted
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
VERSION=development
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.135.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.135.1...@standardnotes/auth-server@1.135.2) (2023-08-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow self hosted to use new model of items ([#714](https://github.com/standardnotes/server/issues/714)) ([aef9254](https://github.com/standardnotes/server/commit/aef9254713560c00a90a3e84e3cd94417e8f30d2))
|
||||
|
||||
## [1.135.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.135.0...@standardnotes/auth-server@1.135.1) (2023-08-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** account enumeration with pseudo u2f and mfa ([#709](https://github.com/standardnotes/server/issues/709)) ([bbb35d1](https://github.com/standardnotes/server/commit/bbb35d16fc4f6a57fe774a648fbda13ec64a8865))
|
||||
|
||||
# [1.135.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.134.0...@standardnotes/auth-server@1.135.0) (2023-08-24)
|
||||
|
||||
### Features
|
||||
|
||||
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/server/issues/707)) ([05bb12c](https://github.com/standardnotes/server/commit/05bb12c97899824f06e6d01d105dec75fc328440))
|
||||
|
||||
# [1.134.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.133.0...@standardnotes/auth-server@1.134.0) (2023-08-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
|
||||
|
||||
# [1.133.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.132.0...@standardnotes/auth-server@1.133.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/server/issues/704)) ([34085ac](https://github.com/standardnotes/server/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
|
||||
|
||||
# [1.132.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.5...@standardnotes/auth-server@1.132.0) (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.132.0",
|
||||
"version": "1.135.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -256,6 +256,13 @@ import { PaymentsAccountDeletedEventHandler } from '../Domain/Handler/PaymentsAc
|
||||
import { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
|
||||
import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
|
||||
import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler'
|
||||
import { TransitionStatusRepositoryInterface } from '../Domain/Transition/TransitionStatusRepositoryInterface'
|
||||
import { RedisTransitionStatusRepository } from '../Infra/Redis/RedisTransitionStatusRepository'
|
||||
import { InMemoryTransitionStatusRepository } from '../Infra/InMemory/InMemoryTransitionStatusRepository'
|
||||
import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler'
|
||||
import { UpdateTransitionStatus } from '../Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
|
||||
import { GetTransitionStatus } from '../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(configuration?: {
|
||||
@@ -609,6 +616,9 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository)
|
||||
.toConstantValue(new InMemoryTransitionStatusRepository())
|
||||
} else {
|
||||
container.bind<PKCERepositoryInterface>(TYPES.Auth_PKCERepository).to(RedisPKCERepository)
|
||||
container.bind<LockRepositoryInterface>(TYPES.Auth_LockRepository).to(LockRepository)
|
||||
@@ -621,6 +631,9 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<SubscriptionTokenRepositoryInterface>(TYPES.Auth_SubscriptionTokenRepository)
|
||||
.to(RedisSubscriptionTokenRepository)
|
||||
container
|
||||
.bind<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository)
|
||||
.toConstantValue(new RedisTransitionStatusRepository(container.get<Redis>(TYPES.Auth_Redis)))
|
||||
}
|
||||
|
||||
// Services
|
||||
@@ -897,6 +910,22 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_SubscriptionSettingService),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<UpdateTransitionStatus>(TYPES.Auth_UpdateTransitionStatus)
|
||||
.toConstantValue(
|
||||
new UpdateTransitionStatus(
|
||||
container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
|
||||
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GetTransitionStatus>(TYPES.Auth_GetTransitionStatus)
|
||||
.toConstantValue(
|
||||
new GetTransitionStatus(
|
||||
container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
),
|
||||
)
|
||||
|
||||
// Controller
|
||||
container
|
||||
@@ -982,6 +1011,14 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedVaultFileMovedEventHandler>(TYPES.Auth_SharedVaultFileMovedEventHandler)
|
||||
.toConstantValue(
|
||||
new SharedVaultFileMovedEventHandler(
|
||||
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler)
|
||||
.toConstantValue(
|
||||
@@ -1030,6 +1067,14 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<TransitionStatusUpdatedEventHandler>(TYPES.Auth_TransitionStatusUpdatedEventHandler)
|
||||
.toConstantValue(
|
||||
new TransitionStatusUpdatedEventHandler(
|
||||
container.get<UpdateTransitionStatus>(TYPES.Auth_UpdateTransitionStatus),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)],
|
||||
@@ -1045,6 +1090,7 @@ export class ContainerConfigLoader {
|
||||
['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
|
||||
['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
|
||||
['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)],
|
||||
['SHARED_VAULT_FILE_MOVED', container.get(TYPES.Auth_SharedVaultFileMovedEventHandler)],
|
||||
['FILE_REMOVED', container.get(TYPES.Auth_FileRemovedEventHandler)],
|
||||
['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)],
|
||||
['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)],
|
||||
@@ -1060,6 +1106,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)],
|
||||
['TRANSITION_STATUS_UPDATED', container.get(TYPES.Auth_TransitionStatusUpdatedEventHandler)],
|
||||
])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
@@ -1164,14 +1211,15 @@ export class ContainerConfigLoader {
|
||||
.bind<BaseUsersController>(TYPES.Auth_BaseUsersController)
|
||||
.toConstantValue(
|
||||
new BaseUsersController(
|
||||
container.get(TYPES.Auth_UpdateUser),
|
||||
container.get(TYPES.Auth_GetUserKeyParams),
|
||||
container.get(TYPES.Auth_DeleteAccount),
|
||||
container.get(TYPES.Auth_GetUserSubscription),
|
||||
container.get(TYPES.Auth_ClearLoginAttempts),
|
||||
container.get(TYPES.Auth_IncreaseLoginAttempts),
|
||||
container.get(TYPES.Auth_ChangeCredentials),
|
||||
container.get(TYPES.Auth_ControllerContainer),
|
||||
container.get<UpdateUser>(TYPES.Auth_UpdateUser),
|
||||
container.get<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams),
|
||||
container.get<DeleteAccount>(TYPES.Auth_DeleteAccount),
|
||||
container.get<GetUserSubscription>(TYPES.Auth_GetUserSubscription),
|
||||
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
|
||||
container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts),
|
||||
container.get<ChangeCredentials>(TYPES.Auth_ChangeCredentials),
|
||||
container.get<GetTransitionStatus>(TYPES.Auth_GetTransitionStatus),
|
||||
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
|
||||
),
|
||||
)
|
||||
container
|
||||
|
||||
@@ -27,6 +27,7 @@ export class Service implements AuthServiceInterface {
|
||||
async activatePremiumFeatures(dto: {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
uploadBytesLimit?: number
|
||||
endsAt?: Date
|
||||
}): Promise<Result<string>> {
|
||||
if (!this.container) {
|
||||
|
||||
@@ -35,6 +35,7 @@ const TYPES = {
|
||||
Auth_AuthenticatorRepository: Symbol.for('Auth_AuthenticatorRepository'),
|
||||
Auth_AuthenticatorChallengeRepository: Symbol.for('Auth_AuthenticatorChallengeRepository'),
|
||||
Auth_CacheEntryRepository: Symbol.for('Auth_CacheEntryRepository'),
|
||||
Auth_TransitionStatusRepository: Symbol.for('Auth_TransitionStatusRepository'),
|
||||
// ORM
|
||||
Auth_ORMOfflineSettingRepository: Symbol.for('Auth_ORMOfflineSettingRepository'),
|
||||
Auth_ORMOfflineUserSubscriptionRepository: Symbol.for('Auth_ORMOfflineUserSubscriptionRepository'),
|
||||
@@ -154,6 +155,8 @@ const TYPES = {
|
||||
Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'),
|
||||
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
|
||||
Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
|
||||
Auth_UpdateTransitionStatus: Symbol.for('Auth_UpdateTransitionStatus'),
|
||||
Auth_GetTransitionStatus: Symbol.for('Auth_GetTransitionStatus'),
|
||||
// Handlers
|
||||
Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'),
|
||||
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
|
||||
@@ -168,6 +171,7 @@ const TYPES = {
|
||||
Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
|
||||
Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
|
||||
Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
|
||||
Auth_SharedVaultFileMovedEventHandler: Symbol.for('Auth_SharedVaultFileMovedEventHandler'),
|
||||
Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'),
|
||||
Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'),
|
||||
Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'),
|
||||
@@ -181,6 +185,7 @@ const TYPES = {
|
||||
Auth_PredicateVerificationRequestedEventHandler: Symbol.for('Auth_PredicateVerificationRequestedEventHandler'),
|
||||
Auth_EmailSubscriptionUnsubscribedEventHandler: Symbol.for('Auth_EmailSubscriptionUnsubscribedEventHandler'),
|
||||
Auth_PaymentsAccountDeletedEventHandler: Symbol.for('Auth_PaymentsAccountDeletedEventHandler'),
|
||||
Auth_TransitionStatusUpdatedEventHandler: Symbol.for('Auth_TransitionStatusUpdatedEventHandler'),
|
||||
// Services
|
||||
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
|
||||
Auth_SessionService: Symbol.for('Auth_SessionService'),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { DomainEventHandlerInterface, SharedVaultFileMovedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
|
||||
export class SharedVaultFileMovedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
|
||||
|
||||
async handle(event: SharedVaultFileMovedEvent): Promise<void> {
|
||||
const subtractResult = await this.updateStorageQuotaUsedForUserUseCase.execute({
|
||||
userUuid: event.payload.from.ownerUuid,
|
||||
bytesUsed: -event.payload.fileByteSize,
|
||||
})
|
||||
|
||||
if (subtractResult.isFailed()) {
|
||||
this.logger.error(`Failed to update storage quota used for user: ${subtractResult.getError()}`)
|
||||
}
|
||||
|
||||
const addResult = await this.updateStorageQuotaUsedForUserUseCase.execute({
|
||||
userUuid: event.payload.to.ownerUuid,
|
||||
bytesUsed: event.payload.fileByteSize,
|
||||
})
|
||||
|
||||
if (addResult.isFailed()) {
|
||||
this.logger.error(`Failed to update storage quota used for user: ${addResult.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ describe('SubscriptionExpiredEventHandler', () => {
|
||||
offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.removeUserRole = jest.fn()
|
||||
roleService.removeUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
timestamp = dayjs.utc().valueOf()
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('SubscriptionExpiredEventHandler', () => {
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRole).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
|
||||
})
|
||||
|
||||
it('should update subscription ends at', async () => {
|
||||
@@ -108,7 +108,7 @@ describe('SubscriptionExpiredEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('SubscriptionExpiredEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,7 +48,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
||||
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
|
||||
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
|
||||
for (const userSubscription of userSubscriptions) {
|
||||
await this.roleService.removeUserRole(await userSubscription.user, subscriptionName)
|
||||
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
roleService.setOfflineUserRole = jest.fn()
|
||||
|
||||
subscriptionExpiresAt = timestamp + 365 * 1000
|
||||
@@ -106,7 +106,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
})
|
||||
|
||||
it('should update user default settings', async () => {
|
||||
@@ -162,7 +162,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -171,7 +171,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -70,7 +70,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
}
|
||||
|
||||
private async addUserRole(user: User, subscriptionName: string): Promise<void> {
|
||||
await this.roleService.addUserRole(user, subscriptionName)
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
|
||||
}
|
||||
|
||||
private async createSubscription(
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('SubscriptionReassignedEventHandler', () => {
|
||||
userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
subscriptionExpiresAt = timestamp + 365 * 1000
|
||||
|
||||
@@ -100,7 +100,7 @@ describe('SubscriptionReassignedEventHandler', () => {
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
})
|
||||
|
||||
it('should create subscription', async () => {
|
||||
@@ -146,7 +146,7 @@ describe('SubscriptionReassignedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -155,7 +155,7 @@ describe('SubscriptionReassignedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -67,7 +67,7 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
|
||||
}
|
||||
|
||||
private async addUserRole(user: User, subscriptionName: string): Promise<void> {
|
||||
await this.roleService.addUserRole(user, subscriptionName)
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
|
||||
}
|
||||
|
||||
private async createSubscription(
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('SubscriptionRefundedEventHandler', () => {
|
||||
offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.removeUserRole = jest.fn()
|
||||
roleService.removeUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
timestamp = dayjs.utc().valueOf()
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('SubscriptionRefundedEventHandler', () => {
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRole).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
|
||||
})
|
||||
|
||||
it('should update subscription ends at', async () => {
|
||||
@@ -109,7 +109,7 @@ describe('SubscriptionRefundedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -118,7 +118,7 @@ describe('SubscriptionRefundedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,7 +48,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
||||
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
|
||||
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
|
||||
for (const userSubscription of userSubscriptions) {
|
||||
await this.roleService.removeUserRole(await userSubscription.user, subscriptionName)
|
||||
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('SubscriptionRenewedEventHandler', () => {
|
||||
offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
roleService.setOfflineUserRole = jest.fn()
|
||||
|
||||
timestamp = dayjs.utc().valueOf()
|
||||
@@ -107,7 +107,7 @@ describe('SubscriptionRenewedEventHandler', () => {
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
})
|
||||
|
||||
it('should update the offline user role', async () => {
|
||||
@@ -123,7 +123,7 @@ describe('SubscriptionRenewedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('SubscriptionRenewedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('SubscriptionRenewedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -73,7 +73,7 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
||||
for (const userSubscription of userSubscriptions) {
|
||||
const user = await userSubscription.user
|
||||
|
||||
await this.roleService.addUserRole(user, subscriptionName)
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
|
||||
})
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
roleService.setOfflineUserRole = jest.fn()
|
||||
|
||||
subscriptionExpiresAt = timestamp + 365 * 1000
|
||||
@@ -121,7 +121,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
})
|
||||
|
||||
it('should update user default settings', async () => {
|
||||
@@ -243,7 +243,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -252,7 +252,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -93,7 +93,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
|
||||
event.payload.timestamp,
|
||||
)
|
||||
|
||||
await this.roleService.addUserRole(user, event.payload.subscriptionName)
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, event.payload.subscriptionName)
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { DomainEventHandlerInterface, TransitionStatusUpdatedEvent } from '@standardnotes/domain-events'
|
||||
import { UpdateTransitionStatus } from '../UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private updateTransitionStatusUseCase: UpdateTransitionStatus, private logger: Logger) {}
|
||||
|
||||
async handle(event: TransitionStatusUpdatedEvent): Promise<void> {
|
||||
const result = await this.updateTransitionStatusUseCase.execute({
|
||||
status: event.payload.status,
|
||||
userUuid: event.payload.userUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to update transition status for user ${event.payload.userUuid}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { RoleName, Uuid } from '@standardnotes/domain-core'
|
||||
import { Role } from '../Role/Role'
|
||||
|
||||
import { ClientServiceInterface } from '../Client/ClientServiceInterface'
|
||||
@@ -81,9 +81,44 @@ describe('RoleService', () => {
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
describe('adding roles', () => {
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([basicRole]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
userRepository.save = jest.fn().mockReturnValue(user)
|
||||
})
|
||||
|
||||
it('should add a role to a user', async () => {
|
||||
await createService().addRoleToUser(
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
)
|
||||
|
||||
user.roles = Promise.resolve([basicRole, proRole])
|
||||
expect(userRepository.save).toHaveBeenCalledWith(user)
|
||||
})
|
||||
|
||||
it('should not add a role to a user if the user could not be found', async () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createService().addRoleToUser(
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
)
|
||||
|
||||
expect(userRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('adding roles based on subscription', () => {
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
@@ -96,7 +131,7 @@ describe('RoleService', () => {
|
||||
})
|
||||
|
||||
it('should add role to user', async () => {
|
||||
await createService().addUserRole(user, SubscriptionName.ProPlan)
|
||||
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
|
||||
|
||||
expect(roleRepository.findOneByName).toHaveBeenCalledWith(RoleName.NAMES.ProUser)
|
||||
user.roles = Promise.resolve([basicRole, proRole])
|
||||
@@ -112,7 +147,7 @@ describe('RoleService', () => {
|
||||
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
await createService().addUserRole(user, SubscriptionName.ProPlan)
|
||||
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
|
||||
|
||||
expect(roleRepository.findOneByName).toHaveBeenCalledWith(RoleName.NAMES.ProUser)
|
||||
expect(userRepository.save).toHaveBeenCalledWith(user)
|
||||
@@ -120,7 +155,7 @@ describe('RoleService', () => {
|
||||
})
|
||||
|
||||
it('should send websockets event', async () => {
|
||||
await createService().addUserRole(user, SubscriptionName.ProPlan)
|
||||
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
|
||||
|
||||
expect(webSocketsClientService.sendUserRolesChangedEvent).toHaveBeenCalledWith(user)
|
||||
})
|
||||
@@ -128,14 +163,14 @@ describe('RoleService', () => {
|
||||
it('should not add role if no role name exists for subscription name', async () => {
|
||||
roleToSubscriptionMap.getRoleNameForSubscriptionName = jest.fn().mockReturnValue(undefined)
|
||||
|
||||
await createService().addUserRole(user, 'test' as SubscriptionName)
|
||||
await createService().addUserRoleBasedOnSubscription(user, 'test' as SubscriptionName)
|
||||
|
||||
expect(userRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not add role if no role exists for role name', async () => {
|
||||
roleRepository.findOneByName = jest.fn().mockReturnValue(null)
|
||||
await createService().addUserRole(user, SubscriptionName.ProPlan)
|
||||
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
|
||||
|
||||
expect(userRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -169,7 +204,7 @@ describe('RoleService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('removing roles', () => {
|
||||
describe('removing roles based on subscription', () => {
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
@@ -182,13 +217,13 @@ describe('RoleService', () => {
|
||||
})
|
||||
|
||||
it('should remove role from user', async () => {
|
||||
await createService().removeUserRole(user, SubscriptionName.ProPlan)
|
||||
await createService().removeUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
|
||||
|
||||
expect(userRepository.save).toHaveBeenCalledWith(user)
|
||||
})
|
||||
|
||||
it('should send websockets event', async () => {
|
||||
await createService().removeUserRole(user, SubscriptionName.ProPlan)
|
||||
await createService().removeUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
|
||||
|
||||
expect(webSocketsClientService.sendUserRolesChangedEvent).toHaveBeenCalledWith(user)
|
||||
})
|
||||
@@ -196,7 +231,7 @@ describe('RoleService', () => {
|
||||
it('should not remove role if role name does not exist for subscription name', async () => {
|
||||
roleToSubscriptionMap.getRoleNameForSubscriptionName = jest.fn().mockReturnValue(undefined)
|
||||
|
||||
await createService().removeUserRole(user, 'test' as SubscriptionName)
|
||||
await createService().removeUserRoleBasedOnSubscription(user, 'test' as SubscriptionName)
|
||||
|
||||
expect(userRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ import { RoleToSubscriptionMapInterface } from './RoleToSubscriptionMapInterface
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { Role } from './Role'
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { RoleName, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
@injectable()
|
||||
export class RoleService implements RoleServiceInterface {
|
||||
@@ -54,7 +54,18 @@ export class RoleService implements RoleServiceInterface {
|
||||
return false
|
||||
}
|
||||
|
||||
async addUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
|
||||
async addRoleToUser(userUuid: Uuid, roleName: RoleName): Promise<void> {
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
if (user === null) {
|
||||
this.logger.error(`Could not find user with uuid ${userUuid.value} to add role ${roleName.value}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.addToExistingRoles(user, roleName.value)
|
||||
}
|
||||
|
||||
async addUserRoleBasedOnSubscription(user: User, subscriptionName: SubscriptionName): Promise<void> {
|
||||
const roleName = this.roleToSubscriptionMap.getRoleNameForSubscriptionName(subscriptionName)
|
||||
|
||||
if (roleName === undefined) {
|
||||
@@ -62,25 +73,7 @@ export class RoleService implements RoleServiceInterface {
|
||||
return
|
||||
}
|
||||
|
||||
const role = await this.roleRepository.findOneByName(roleName)
|
||||
|
||||
if (role === null) {
|
||||
this.logger.warn(`Could not find role for role name: ${roleName}`)
|
||||
return
|
||||
}
|
||||
|
||||
const rolesMap = new Map<string, Role>()
|
||||
const currentRoles = await user.roles
|
||||
for (const currentRole of currentRoles) {
|
||||
rolesMap.set(currentRole.name, currentRole)
|
||||
}
|
||||
if (!rolesMap.has(role.name)) {
|
||||
rolesMap.set(role.name, role)
|
||||
}
|
||||
|
||||
user.roles = Promise.resolve([...rolesMap.values()])
|
||||
await this.userRepository.save(user)
|
||||
await this.webSocketsClientService.sendUserRolesChangedEvent(user)
|
||||
await this.addToExistingRoles(user, roleName)
|
||||
}
|
||||
|
||||
async setOfflineUserRole(offlineUserSubscription: OfflineUserSubscription): Promise<void> {
|
||||
@@ -107,7 +100,7 @@ export class RoleService implements RoleServiceInterface {
|
||||
await this.offlineUserSubscriptionRepository.save(offlineUserSubscription)
|
||||
}
|
||||
|
||||
async removeUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
|
||||
async removeUserRoleBasedOnSubscription(user: User, subscriptionName: SubscriptionName): Promise<void> {
|
||||
const roleName = this.roleToSubscriptionMap.getRoleNameForSubscriptionName(subscriptionName)
|
||||
|
||||
if (roleName === undefined) {
|
||||
@@ -120,4 +113,27 @@ export class RoleService implements RoleServiceInterface {
|
||||
await this.userRepository.save(user)
|
||||
await this.webSocketsClientService.sendUserRolesChangedEvent(user)
|
||||
}
|
||||
|
||||
private async addToExistingRoles(user: User, roleNameString: string): Promise<void> {
|
||||
const role = await this.roleRepository.findOneByName(roleNameString)
|
||||
|
||||
if (role === null) {
|
||||
this.logger.warn(`Could not find role for role name: ${roleNameString}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const rolesMap = new Map<string, Role>()
|
||||
const currentRoles = await user.roles
|
||||
for (const currentRole of currentRoles) {
|
||||
rolesMap.set(currentRole.name, currentRole)
|
||||
}
|
||||
if (!rolesMap.has(role.name)) {
|
||||
rolesMap.set(role.name, role)
|
||||
}
|
||||
|
||||
user.roles = Promise.resolve([...rolesMap.values()])
|
||||
await this.userRepository.save(user)
|
||||
await this.webSocketsClientService.sendUserRolesChangedEvent(user)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
import { RoleName, Uuid } from '@standardnotes/domain-core'
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { User } from '../User/User'
|
||||
|
||||
export interface RoleServiceInterface {
|
||||
addUserRole(user: User, subscriptionName: string): Promise<void>
|
||||
addRoleToUser(userUuid: Uuid, roleName: RoleName): Promise<void>
|
||||
addUserRoleBasedOnSubscription(user: User, subscriptionName: string): Promise<void>
|
||||
setOfflineUserRole(offlineUserSubscription: OfflineUserSubscription): Promise<void>
|
||||
removeUserRole(user: User, subscriptionName: string): Promise<void>
|
||||
removeUserRoleBasedOnSubscription(user: User, subscriptionName: string): Promise<void>
|
||||
userHasPermission(userUuid: string, permissionName: PermissionName): Promise<boolean>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface TransitionStatusRepositoryInterface {
|
||||
updateStatus(userUuid: string, status: 'STARTED' | 'FAILED'): Promise<void>
|
||||
removeStatus(userUuid: string): Promise<void>
|
||||
getStatus(userUuid: string): Promise<'STARTED' | 'FAILED' | null>
|
||||
}
|
||||
@@ -69,7 +69,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
userSubscriptionRepository.save = jest.fn().mockReturnValue(inviteeSubscription)
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
|
||||
@@ -103,7 +103,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
updatedAt: 1,
|
||||
user: Promise.resolve(invitee),
|
||||
})
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
inviteeSubscription,
|
||||
)
|
||||
@@ -143,7 +143,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
updatedAt: 3,
|
||||
user: Promise.resolve(invitee),
|
||||
})
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
inviteeSubscription,
|
||||
)
|
||||
@@ -162,7 +162,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -180,7 +180,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -202,7 +202,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -219,7 +219,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -244,7 +244,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -100,7 +100,7 @@ export class AcceptSharedSubscriptionInvitation implements UseCaseInterface {
|
||||
}
|
||||
|
||||
private async addUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
|
||||
await this.roleService.addUserRole(user, subscriptionName)
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
|
||||
}
|
||||
|
||||
private async createSharedSubscription(
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('ActivatePremiumFeatures', () => {
|
||||
userSubscriptionRepository.save = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
@@ -73,7 +73,7 @@ describe('ActivatePremiumFeatures', () => {
|
||||
expect(result.isFailed()).toBe(false)
|
||||
|
||||
expect(userSubscriptionRepository.save).toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should save a subscription with custom plan name and endsAt', async () => {
|
||||
|
||||
@@ -53,11 +53,11 @@ export class ActivatePremiumFeatures implements UseCaseInterface<string> {
|
||||
|
||||
await this.userSubscriptionRepository.save(subscription)
|
||||
|
||||
await this.roleService.addUserRole(user, subscriptionPlanName.value)
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionPlanName.value)
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
|
||||
subscription,
|
||||
new Map([[SettingName.NAMES.FileUploadBytesLimit, '-1']]),
|
||||
new Map([[SettingName.NAMES.FileUploadBytesLimit, `${dto.uploadBytesLimit ?? -1}`]]),
|
||||
)
|
||||
|
||||
return Result.ok('Premium features activated.')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface ActivatePremiumFeaturesDTO {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
uploadBytesLimit?: number
|
||||
endsAt?: Date
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
userSubscriptionRepository.save = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.removeUserRole = jest.fn()
|
||||
roleService.removeUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
|
||||
@@ -122,7 +122,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
endsAt: 1,
|
||||
user: Promise.resolve(invitee),
|
||||
})
|
||||
expect(roleService.removeUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
expect(domainEventFactory.createSharedSubscriptionInvitationCanceledEvent).toHaveBeenCalledWith({
|
||||
inviteeIdentifier: '123',
|
||||
@@ -156,7 +156,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
inviteeIdentifierType: 'email',
|
||||
})
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
})
|
||||
|
||||
it('should not cancel a shared subscription invitation if it is not found', async () => {
|
||||
@@ -204,7 +204,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
inviteeIdentifierType: 'email',
|
||||
})
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not cancel a shared subscription invitation if inviter subscription is not found', async () => {
|
||||
|
||||
@@ -90,7 +90,10 @@ export class CancelSharedSubscriptionInvitation implements UseCaseInterface {
|
||||
if (invitee !== null) {
|
||||
await this.removeSharedSubscription(sharedSubscriptionInvitation.subscriptionId, invitee)
|
||||
|
||||
await this.roleService.removeUserRole(invitee, inviterUserSubscription.planName as SubscriptionName)
|
||||
await this.roleService.removeUserRoleBasedOnSubscription(
|
||||
invitee,
|
||||
inviterUserSubscription.planName as SubscriptionName,
|
||||
)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createSharedSubscriptionInvitationCanceledEvent({
|
||||
|
||||
@@ -8,6 +8,9 @@ import { Role } from '../../Role/Role'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
|
||||
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
|
||||
import { GetSetting } from '../GetSetting/GetSetting'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
|
||||
|
||||
describe('CreateCrossServiceToken', () => {
|
||||
let userProjector: ProjectorInterface<User>
|
||||
@@ -15,6 +18,8 @@ describe('CreateCrossServiceToken', () => {
|
||||
let roleProjector: ProjectorInterface<Role>
|
||||
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
|
||||
let userRepository: UserRepositoryInterface
|
||||
let getSettingUseCase: GetSetting
|
||||
let transitionStatusRepository: TransitionStatusRepositoryInterface
|
||||
const jwtTTL = 60
|
||||
|
||||
let session: Session
|
||||
@@ -22,7 +27,16 @@ describe('CreateCrossServiceToken', () => {
|
||||
let role: Role
|
||||
|
||||
const createUseCase = () =>
|
||||
new CreateCrossServiceToken(userProjector, sessionProjector, roleProjector, tokenEncoder, userRepository, jwtTTL)
|
||||
new CreateCrossServiceToken(
|
||||
userProjector,
|
||||
sessionProjector,
|
||||
roleProjector,
|
||||
tokenEncoder,
|
||||
userRepository,
|
||||
jwtTTL,
|
||||
getSettingUseCase,
|
||||
transitionStatusRepository,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
@@ -50,6 +64,12 @@ describe('CreateCrossServiceToken', () => {
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
|
||||
getSettingUseCase = {} as jest.Mocked<GetSetting>
|
||||
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ setting: { value: '100' } }))
|
||||
|
||||
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
|
||||
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('TO-DO')
|
||||
})
|
||||
|
||||
it('should create a cross service token for user', async () => {
|
||||
@@ -73,6 +93,36 @@ describe('CreateCrossServiceToken', () => {
|
||||
email: 'test@test.te',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
ongoing_transition: false,
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should create a cross service token for user that has an ongoing transaction', async () => {
|
||||
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('STARTED')
|
||||
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
email: 'test@test.te',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
ongoing_transition: true,
|
||||
},
|
||||
60,
|
||||
)
|
||||
@@ -95,6 +145,7 @@ describe('CreateCrossServiceToken', () => {
|
||||
email: 'test@test.te',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
ongoing_transition: false,
|
||||
},
|
||||
60,
|
||||
)
|
||||
@@ -117,6 +168,7 @@ describe('CreateCrossServiceToken', () => {
|
||||
email: 'test@test.te',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
ongoing_transition: false,
|
||||
},
|
||||
60,
|
||||
)
|
||||
@@ -125,28 +177,75 @@ describe('CreateCrossServiceToken', () => {
|
||||
it('should throw an error if user does not exist', async () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should throw an error if user uuid is invalid', async () => {
|
||||
let caughtError = null
|
||||
try {
|
||||
await createUseCase().execute({
|
||||
userUuid: 'invalid',
|
||||
})
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: 'invalid',
|
||||
})
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('shared vault context', () => {
|
||||
it('should add shared vault context if shared vault owner uuid is provided', async () => {
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
shared_vault_owner_context: {
|
||||
upload_bytes_limit: 100,
|
||||
},
|
||||
user: {
|
||||
email: 'test@test.te',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
ongoing_transition: false,
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw an error if shared vault owner context is sensitive', async () => {
|
||||
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ sensitive: true }))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should throw an error if it fails to retrieve shared vault owner setting', async () => {
|
||||
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
|
||||
@@ -7,14 +8,14 @@ import { Role } from '../../Role/Role'
|
||||
import { Session } from '../../Session/Session'
|
||||
import { User } from '../../User/User'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
|
||||
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
|
||||
import { CreateCrossServiceTokenResponse } from './CreateCrossServiceTokenResponse'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { GetSetting } from '../GetSetting/GetSetting'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
export class CreateCrossServiceToken implements UseCaseInterface<string> {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserProjector) private userProjector: ProjectorInterface<User>,
|
||||
@inject(TYPES.Auth_SessionProjector) private sessionProjector: ProjectorInterface<Session>,
|
||||
@@ -22,14 +23,18 @@ export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
@inject(TYPES.Auth_CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_AUTH_JWT_TTL) private jwtTTL: number,
|
||||
@inject(TYPES.Auth_GetSetting)
|
||||
private getSettingUseCase: GetSetting,
|
||||
@inject(TYPES.Auth_TransitionStatusRepository)
|
||||
private transitionStatusRepository: TransitionStatusRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateCrossServiceTokenDTO): Promise<CreateCrossServiceTokenResponse> {
|
||||
async execute(dto: CreateCrossServiceTokenDTO): Promise<Result<string>> {
|
||||
let user: User | undefined | null = dto.user
|
||||
if (user === undefined && dto.userUuid !== undefined) {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
throw new Error(userUuidOrError.getError())
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
@@ -37,23 +42,44 @@ export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with uuid ${dto.userUuid}`)
|
||||
return Result.fail(`Could not find user with uuid ${dto.userUuid}`)
|
||||
}
|
||||
|
||||
const transitionStatus = await this.transitionStatusRepository.getStatus(user.uuid)
|
||||
|
||||
const roles = await user.roles
|
||||
|
||||
const authTokenData: CrossServiceTokenData = {
|
||||
user: this.projectUser(user),
|
||||
roles: this.projectRoles(roles),
|
||||
shared_vault_owner_context: undefined,
|
||||
ongoing_transition: transitionStatus === 'STARTED',
|
||||
}
|
||||
|
||||
if (dto.sharedVaultOwnerContext !== undefined) {
|
||||
const uploadBytesLimitSettingOrError = await this.getSettingUseCase.execute({
|
||||
settingName: SettingName.NAMES.FileUploadBytesLimit,
|
||||
userUuid: dto.sharedVaultOwnerContext,
|
||||
})
|
||||
if (uploadBytesLimitSettingOrError.isFailed()) {
|
||||
return Result.fail(uploadBytesLimitSettingOrError.getError())
|
||||
}
|
||||
const uploadBytesLimitSetting = uploadBytesLimitSettingOrError.getValue()
|
||||
if (uploadBytesLimitSetting.sensitive) {
|
||||
return Result.fail('Shared vault owner upload bytes limit setting is sensitive!')
|
||||
}
|
||||
const uploadBytesLimit = parseInt(uploadBytesLimitSetting.setting.value as string)
|
||||
|
||||
authTokenData.shared_vault_owner_context = {
|
||||
upload_bytes_limit: uploadBytesLimit,
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.session !== undefined) {
|
||||
authTokenData.session = this.projectSession(dto.session)
|
||||
}
|
||||
|
||||
return {
|
||||
token: this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL),
|
||||
}
|
||||
return Result.ok(this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL))
|
||||
}
|
||||
|
||||
private projectUser(user: User): { uuid: string; email: string } {
|
||||
|
||||
@@ -6,6 +6,7 @@ export type CreateCrossServiceTokenDTO = Either<
|
||||
{
|
||||
user: User
|
||||
session?: Session
|
||||
sharedVaultOwnerContext?: string
|
||||
},
|
||||
{
|
||||
userUuid: string
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export type CreateCrossServiceTokenResponse = {
|
||||
token: string
|
||||
}
|
||||
@@ -73,35 +73,30 @@ describe('GetSetting', () => {
|
||||
|
||||
describe('no subscription', () => {
|
||||
it('should find a setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.DropboxBackupFrequency,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'bar' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should not find a setting if the setting name is invalid', async () => {
|
||||
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid setting name: invalid',
|
||||
},
|
||||
})
|
||||
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not get a setting for user if it does not exist', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Setting DROPBOX_BACKUP_FREQUENCY for user 1-2-3 not found!',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.DropboxBackupFrequency,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not retrieve a sensitive setting for user', async () => {
|
||||
@@ -112,21 +107,19 @@ describe('GetSetting', () => {
|
||||
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
sensitive: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not retrieve a subscription setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'No subscription found.',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should retrieve a sensitive setting for user if explicitly told to', async () => {
|
||||
@@ -137,14 +130,13 @@ describe('GetSetting', () => {
|
||||
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MfaSecret,
|
||||
allowSensitiveRetrieval: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MfaSecret,
|
||||
allowSensitiveRetrieval: true,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'bar' },
|
||||
})
|
||||
@@ -159,10 +151,12 @@ describe('GetSetting', () => {
|
||||
})
|
||||
|
||||
it('should find a setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'sub-bar' },
|
||||
})
|
||||
@@ -171,14 +165,11 @@ describe('GetSetting', () => {
|
||||
it('should not get a suscription setting for user if it does not exist', async () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Subscription setting MUTE_SIGN_IN_EMAILS for user 1-2-3 not found!',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not retrieve a sensitive subscription setting for user', async () => {
|
||||
@@ -188,10 +179,12 @@ describe('GetSetting', () => {
|
||||
.fn()
|
||||
.mockReturnValue(subscriptionSetting)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
sensitive: true,
|
||||
})
|
||||
})
|
||||
@@ -205,10 +198,12 @@ describe('GetSetting', () => {
|
||||
})
|
||||
|
||||
it('should find a setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'sub-bar' },
|
||||
})
|
||||
@@ -221,10 +216,12 @@ describe('GetSetting', () => {
|
||||
})
|
||||
|
||||
it('should find a regular subscription only setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.FileUploadBytesLimit }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.FileUploadBytesLimit,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'sub-bar' },
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { SettingProjector } from '../../../Projection/SettingProjector'
|
||||
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
|
||||
@@ -14,7 +14,7 @@ import { GetSettingResponse } from './GetSettingResponse'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
|
||||
@injectable()
|
||||
export class GetSetting implements UseCaseInterface {
|
||||
export class GetSetting implements UseCaseInterface<GetSettingResponse> {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_SettingProjector) private settingProjector: SettingProjector,
|
||||
@inject(TYPES.Auth_SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector,
|
||||
@@ -24,15 +24,10 @@ export class GetSetting implements UseCaseInterface {
|
||||
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetSettingDto): Promise<GetSettingResponse> {
|
||||
async execute(dto: GetSettingDto): Promise<Result<GetSettingResponse>> {
|
||||
const settingNameOrError = SettingName.create(dto.settingName)
|
||||
if (settingNameOrError.isFailed()) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: settingNameOrError.getError(),
|
||||
},
|
||||
}
|
||||
return Result.fail(settingNameOrError.getError())
|
||||
}
|
||||
const settingName = settingNameOrError.getValue()
|
||||
|
||||
@@ -47,12 +42,7 @@ export class GetSetting implements UseCaseInterface {
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'No subscription found.',
|
||||
},
|
||||
}
|
||||
return Result.fail('No subscription found.')
|
||||
}
|
||||
|
||||
const subscriptionSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
|
||||
@@ -62,28 +52,21 @@ export class GetSetting implements UseCaseInterface {
|
||||
})
|
||||
|
||||
if (subscriptionSetting === null) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: `Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`,
|
||||
},
|
||||
}
|
||||
return Result.fail(`Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`)
|
||||
}
|
||||
|
||||
if (subscriptionSetting.sensitive && !dto.allowSensitiveRetrieval) {
|
||||
return {
|
||||
success: true,
|
||||
return Result.ok({
|
||||
sensitive: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const simpleSubscriptionSetting = await this.subscriptionSettingProjector.projectSimple(subscriptionSetting)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
return Result.ok({
|
||||
userUuid: dto.userUuid,
|
||||
setting: simpleSubscriptionSetting,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setting = await this.settingService.findSettingWithDecryptedValue({
|
||||
@@ -92,27 +75,20 @@ export class GetSetting implements UseCaseInterface {
|
||||
})
|
||||
|
||||
if (setting === null) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: `Setting ${settingName.value} for user ${dto.userUuid} not found!`,
|
||||
},
|
||||
}
|
||||
return Result.fail(`Setting ${settingName.value} for user ${dto.userUuid} not found!`)
|
||||
}
|
||||
|
||||
if (setting.sensitive && !dto.allowSensitiveRetrieval) {
|
||||
return {
|
||||
success: true,
|
||||
return Result.ok({
|
||||
sensitive: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const simpleSetting = await this.settingProjector.projectSimple(setting)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
return Result.ok({
|
||||
userUuid: dto.userUuid,
|
||||
setting: simpleSetting,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import { Either } from '@standardnotes/common'
|
||||
|
||||
import { SimpleSetting } from '../../Setting/SimpleSetting'
|
||||
|
||||
export type GetSettingResponse =
|
||||
| {
|
||||
success: true
|
||||
userUuid: string
|
||||
setting: SimpleSetting
|
||||
}
|
||||
| {
|
||||
success: true
|
||||
sensitive: true
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
error: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
export type GetSettingResponse = Either<
|
||||
{
|
||||
userUuid: string
|
||||
setting: SimpleSetting
|
||||
},
|
||||
{
|
||||
sensitive: true
|
||||
}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { Role } from '../../Role/Role'
|
||||
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
|
||||
import { User } from '../../User/User'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { GetTransitionStatus } from './GetTransitionStatus'
|
||||
|
||||
describe('GetTransitionStatus', () => {
|
||||
let transitionStatusRepository: TransitionStatusRepositoryInterface
|
||||
let userRepository: UserRepositoryInterface
|
||||
let user: User
|
||||
let role: Role
|
||||
|
||||
const createUseCase = () => new GetTransitionStatus(transitionStatusRepository, userRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
|
||||
transitionStatusRepository.getStatus = jest.fn().mockReturnValue(null)
|
||||
|
||||
role = {} as jest.Mocked<Role>
|
||||
role.name = RoleName.NAMES.CoreUser
|
||||
|
||||
user = {
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
email: 'test@test.te',
|
||||
} as jest.Mocked<User>
|
||||
user.roles = Promise.resolve([role])
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
})
|
||||
|
||||
it('returns transition status FINISHED', async () => {
|
||||
role.name = RoleName.NAMES.TransitionUser
|
||||
user.roles = Promise.resolve([role])
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual('FINISHED')
|
||||
})
|
||||
|
||||
it('returns transition status STARTED', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('STARTED')
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual('STARTED')
|
||||
})
|
||||
|
||||
it('returns transition status TO-DO', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual('TO-DO')
|
||||
})
|
||||
|
||||
it('returns transition status FAILED', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('FAILED')
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual('FAILED')
|
||||
})
|
||||
|
||||
it('return error if user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
|
||||
})
|
||||
|
||||
it('return error if user not found', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('User not found.')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Result, RoleName, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { GetTransitionStatusDTO } from './GetTransitionStatusDTO'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
|
||||
|
||||
export class GetTransitionStatus implements UseCaseInterface<'TO-DO' | 'STARTED' | 'FINISHED' | 'FAILED'> {
|
||||
constructor(
|
||||
private transitionStatusRepository: TransitionStatusRepositoryInterface,
|
||||
private userRepository: UserRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetTransitionStatusDTO): Promise<Result<'TO-DO' | 'STARTED' | 'FINISHED' | 'FAILED'>> {
|
||||
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('User not found.')
|
||||
}
|
||||
|
||||
const roles = await user.roles
|
||||
for (const role of roles) {
|
||||
if (role.name === RoleName.NAMES.TransitionUser) {
|
||||
return Result.ok('FINISHED')
|
||||
}
|
||||
}
|
||||
|
||||
const transitionStatus = await this.transitionStatusRepository.getStatus(userUuid.value)
|
||||
if (transitionStatus === null) {
|
||||
return Result.ok('TO-DO')
|
||||
}
|
||||
|
||||
return Result.ok(transitionStatus)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface GetTransitionStatusDTO {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -93,7 +93,7 @@ describe('UpdateSetting', () => {
|
||||
settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(true)
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
|
||||
@@ -7,7 +7,6 @@ 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(
|
||||
@@ -34,23 +33,20 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
|
||||
return Result.fail(`Could not find regular user subscription for user with uuid: ${userUuid.value}`)
|
||||
}
|
||||
|
||||
await this.updateUploadBytesUsedSetting(regularSubscription, user, dto.bytesUsed)
|
||||
await this.updateUploadBytesUsedSetting(regularSubscription, dto.bytesUsed)
|
||||
|
||||
if (sharedSubscription !== null) {
|
||||
await this.updateUploadBytesUsedSetting(sharedSubscription, user, dto.bytesUsed)
|
||||
await this.updateUploadBytesUsedSetting(sharedSubscription, dto.bytesUsed)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async updateUploadBytesUsedSetting(
|
||||
subscription: UserSubscription,
|
||||
user: User,
|
||||
bytesUsed: number,
|
||||
): Promise<void> {
|
||||
private async updateUploadBytesUsedSetting(subscription: UserSubscription, bytesUsed: number): Promise<void> {
|
||||
let bytesAlreadyUsed = '0'
|
||||
const subscriptionUser = await subscription.user
|
||||
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
|
||||
userUuid: (await subscription.user).uuid,
|
||||
userUuid: subscriptionUser.uuid,
|
||||
userSubscriptionUuid: subscription.uuid,
|
||||
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
|
||||
})
|
||||
@@ -60,7 +56,7 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
|
||||
|
||||
await this.subscriptionSettingService.createOrReplace({
|
||||
userSubscription: subscription,
|
||||
user,
|
||||
user: subscriptionUser,
|
||||
props: {
|
||||
name: SettingName.NAMES.FileUploadBytesUsed,
|
||||
unencryptedValue: (+bytesAlreadyUsed + bytesUsed).toString(),
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { RoleName, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
|
||||
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
|
||||
import { UpdateTransitionStatus } from './UpdateTransitionStatus'
|
||||
|
||||
describe('UpdateTransitionStatus', () => {
|
||||
let transitionStatusRepository: TransitionStatusRepositoryInterface
|
||||
let roleService: RoleServiceInterface
|
||||
|
||||
const createUseCase = () => new UpdateTransitionStatus(transitionStatusRepository, roleService)
|
||||
|
||||
beforeEach(() => {
|
||||
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
|
||||
transitionStatusRepository.removeStatus = jest.fn()
|
||||
transitionStatusRepository.updateStatus = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addRoleToUser = jest.fn()
|
||||
})
|
||||
|
||||
it('should remove transition status and add TransitionUser role', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
status: 'FINISHED',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(transitionStatusRepository.removeStatus).toHaveBeenCalledWith('00000000-0000-0000-0000-000000000000')
|
||||
expect(roleService.addRoleToUser).toHaveBeenCalledWith(
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
RoleName.create(RoleName.NAMES.TransitionUser).getValue(),
|
||||
)
|
||||
})
|
||||
|
||||
it('should update transition status', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
status: 'STARTED',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(transitionStatusRepository.updateStatus).toHaveBeenCalledWith(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'STARTED',
|
||||
)
|
||||
})
|
||||
|
||||
it('should return error when user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid',
|
||||
status: 'STARTED',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Result, RoleName, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
|
||||
import { UpdateTransitionStatusDTO } from './UpdateTransitionStatusDTO'
|
||||
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
|
||||
|
||||
export class UpdateTransitionStatus implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private transitionStatusRepository: TransitionStatusRepositoryInterface,
|
||||
private roleService: RoleServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: UpdateTransitionStatusDTO): Promise<Result<void>> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
if (dto.status === 'FINISHED') {
|
||||
await this.transitionStatusRepository.removeStatus(dto.userUuid)
|
||||
|
||||
await this.roleService.addRoleToUser(userUuid, RoleName.create(RoleName.NAMES.TransitionUser).getValue())
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.status)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface UpdateTransitionStatusDTO {
|
||||
userUuid: string
|
||||
status: 'STARTED' | 'FINISHED' | 'FAILED'
|
||||
}
|
||||
@@ -257,7 +257,7 @@ describe('VerifyMFA', () => {
|
||||
})
|
||||
|
||||
it('should not pass if user is not found and pseudo u2f is required', async () => {
|
||||
booleanSelector.select = jest.fn().mockReturnValueOnce(false).mockReturnValueOnce(true)
|
||||
booleanSelector.select = jest.fn().mockReturnValueOnce(true).mockReturnValueOnce(true)
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
|
||||
@@ -48,33 +48,33 @@ export class VerifyMFA implements UseCaseInterface {
|
||||
|
||||
const user = await this.userRepository.findOneByUsernameOrEmail(username)
|
||||
if (user == null) {
|
||||
const mfaSelectorHash = crypto
|
||||
const secondFactorSelectorHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(`mfa-selector-${dto.email}${this.pseudoKeyParamsKey}`)
|
||||
.digest('hex')
|
||||
const u2fSelectorHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(`u2f-selector-${dto.email}${this.pseudoKeyParamsKey}`)
|
||||
.update(`second-factor-selector-${dto.email}${this.pseudoKeyParamsKey}`)
|
||||
.digest('hex')
|
||||
|
||||
const isPseudoMFARequired = this.booleanSelector.select(mfaSelectorHash, [true, false])
|
||||
const isPseudoSecondFactorRequired = this.booleanSelector.select(secondFactorSelectorHash, [true, false])
|
||||
if (isPseudoSecondFactorRequired) {
|
||||
const u2fSelectorHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(`u2f-selector-${dto.email}${this.pseudoKeyParamsKey}`)
|
||||
.digest('hex')
|
||||
|
||||
const isPseudoU2FRequired = this.booleanSelector.select(u2fSelectorHash, [true, false])
|
||||
const isPseudoU2FRequired = this.booleanSelector.select(u2fSelectorHash, [true, false])
|
||||
|
||||
if (isPseudoMFARequired) {
|
||||
return {
|
||||
success: false,
|
||||
errorTag: ErrorTag.MfaRequired,
|
||||
errorMessage: 'Please enter your two-factor authentication code.',
|
||||
errorPayload: { mfa_key: `mfa_${uuidv4()}` },
|
||||
}
|
||||
}
|
||||
|
||||
if (isPseudoU2FRequired) {
|
||||
return {
|
||||
success: false,
|
||||
errorTag: ErrorTag.U2FRequired,
|
||||
errorMessage: 'Please authenticate with your U2F device.',
|
||||
if (isPseudoU2FRequired) {
|
||||
return {
|
||||
success: false,
|
||||
errorTag: ErrorTag.U2FRequired,
|
||||
errorMessage: 'Please authenticate with your U2F device.',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
errorTag: ErrorTag.MfaRequired,
|
||||
errorMessage: 'Please enter your two-factor authentication code.',
|
||||
errorPayload: { mfa_key: `mfa_${uuidv4()}` },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { TransitionStatusRepositoryInterface } from '../../Domain/Transition/TransitionStatusRepositoryInterface'
|
||||
|
||||
export class InMemoryTransitionStatusRepository implements TransitionStatusRepositoryInterface {
|
||||
private statuses: Map<string, 'STARTED' | 'FAILED'> = new Map()
|
||||
|
||||
async updateStatus(userUuid: string, status: 'STARTED' | 'FAILED'): Promise<void> {
|
||||
this.statuses.set(userUuid, status)
|
||||
}
|
||||
|
||||
async removeStatus(userUuid: string): Promise<void> {
|
||||
this.statuses.delete(userUuid)
|
||||
}
|
||||
|
||||
async getStatus(userUuid: string): Promise<'STARTED' | 'FAILED' | null> {
|
||||
const status = this.statuses.get(userUuid) || null
|
||||
|
||||
return status
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { results } from 'inversify-express-utils'
|
||||
import { User } from '../../Domain/User/User'
|
||||
import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
|
||||
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AnnotatedInternalController', () => {
|
||||
let getUserFeatures: GetUserFeatures
|
||||
@@ -73,7 +74,7 @@ describe('AnnotatedInternalController', () => {
|
||||
request.params.userUuid = '1-2-3'
|
||||
request.params.settingName = 'foobar'
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: true })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSetting(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
@@ -91,7 +92,7 @@ describe('AnnotatedInternalController', () => {
|
||||
request.params.userUuid = '1-2-3'
|
||||
request.params.settingName = 'foobar'
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: false })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSetting(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
@@ -36,16 +36,26 @@ export class AnnotatedInternalController extends BaseHttpController {
|
||||
|
||||
@httpGet('/users/:userUuid/settings/:settingName')
|
||||
async getSetting(request: Request): Promise<results.JsonResult> {
|
||||
const result = await this.doGetSetting.execute({
|
||||
const resultOrError = await this.doGetSetting.execute({
|
||||
userUuid: request.params.userUuid,
|
||||
settingName: request.params.settingName,
|
||||
allowSensitiveRetrieval: true,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
return this.json(result)
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json(result, 400)
|
||||
return this.json({
|
||||
success: true,
|
||||
...resultOrError.getValue(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossService
|
||||
import { GetActiveSessionsForUser } from '../../Domain/UseCase/GetActiveSessionsForUser'
|
||||
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
|
||||
import { Session } from '../../Domain/Session/Session'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AnnotatedSessionsController', () => {
|
||||
let getActiveSessionsForUser: GetActiveSessionsForUser
|
||||
@@ -45,7 +46,7 @@ describe('AnnotatedSessionsController', () => {
|
||||
sessionProjector.projectCustom = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
|
||||
createCrossServiceToken = {} as jest.Mocked<CreateCrossServiceToken>
|
||||
createCrossServiceToken.execute = jest.fn().mockReturnValue({ token: 'foobar' })
|
||||
createCrossServiceToken.execute = jest.fn().mockReturnValue(Result.ok('foobar'))
|
||||
|
||||
request = {
|
||||
params: {},
|
||||
|
||||
@@ -10,6 +10,7 @@ import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings'
|
||||
import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting'
|
||||
import { User } from '../../Domain/User/User'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AnnotatedSettingsController', () => {
|
||||
let deleteSetting: DeleteSetting
|
||||
@@ -85,7 +86,7 @@ describe('AnnotatedSettingsController', () => {
|
||||
uuid: '1-2-3',
|
||||
}
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: true })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
@@ -119,7 +120,7 @@ describe('AnnotatedSettingsController', () => {
|
||||
uuid: '1-2-3',
|
||||
}
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: false })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
@@ -6,6 +6,7 @@ import { results } from 'inversify-express-utils'
|
||||
import { AnnotatedSubscriptionSettingsController } from './AnnotatedSubscriptionSettingsController'
|
||||
import { User } from '../../Domain/User/User'
|
||||
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AnnotatedSubscriptionSettingsController', () => {
|
||||
let getSetting: GetSetting
|
||||
@@ -41,7 +42,7 @@ describe('AnnotatedSubscriptionSettingsController', () => {
|
||||
uuid: '1-2-3',
|
||||
}
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: true })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
@@ -58,7 +59,7 @@ describe('AnnotatedSubscriptionSettingsController', () => {
|
||||
uuid: '1-2-3',
|
||||
}
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: false })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
@@ -14,6 +14,7 @@ import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempt
|
||||
import { InviteToSharedSubscription } from '../../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription'
|
||||
import { UpdateUser } from '../../Domain/UseCase/UpdateUser'
|
||||
import { User } from '../../Domain/User/User'
|
||||
import { GetTransitionStatus } from '../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
|
||||
|
||||
describe('AnnotatedUsersController', () => {
|
||||
let updateUser: UpdateUser
|
||||
@@ -24,6 +25,7 @@ describe('AnnotatedUsersController', () => {
|
||||
let increaseLoginAttempts: IncreaseLoginAttempts
|
||||
let changeCredentials: ChangeCredentials
|
||||
let inviteToSharedSubscription: InviteToSharedSubscription
|
||||
let getTransitionStatus: GetTransitionStatus
|
||||
|
||||
let request: express.Request
|
||||
let response: express.Response
|
||||
@@ -38,6 +40,7 @@ describe('AnnotatedUsersController', () => {
|
||||
clearLoginAttempts,
|
||||
increaseLoginAttempts,
|
||||
changeCredentials,
|
||||
getTransitionStatus,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -69,6 +72,9 @@ describe('AnnotatedUsersController', () => {
|
||||
inviteToSharedSubscription = {} as jest.Mocked<InviteToSharedSubscription>
|
||||
inviteToSharedSubscription.execute = jest.fn()
|
||||
|
||||
getTransitionStatus = {} as jest.Mocked<GetTransitionStatus>
|
||||
getTransitionStatus.execute = jest.fn()
|
||||
|
||||
request = {
|
||||
headers: {},
|
||||
body: {},
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
|
||||
import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
|
||||
import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
|
||||
import { BaseUsersController } from './Base/BaseUsersController'
|
||||
import { GetTransitionStatus } from '../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
|
||||
|
||||
@controller('/users')
|
||||
export class AnnotatedUsersController extends BaseUsersController {
|
||||
@@ -29,6 +30,7 @@ export class AnnotatedUsersController extends BaseUsersController {
|
||||
@inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts,
|
||||
@inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts,
|
||||
@inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials,
|
||||
@inject(TYPES.Auth_GetTransitionStatus) override getTransitionStatusUseCase: GetTransitionStatus,
|
||||
) {
|
||||
super(
|
||||
updateUser,
|
||||
@@ -38,6 +40,7 @@ export class AnnotatedUsersController extends BaseUsersController {
|
||||
clearLoginAttempts,
|
||||
increaseLoginAttempts,
|
||||
changeCredentialsUseCase,
|
||||
getTransitionStatusUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,6 +54,11 @@ export class AnnotatedUsersController extends BaseUsersController {
|
||||
return super.keyParams(request)
|
||||
}
|
||||
|
||||
@httpGet('/transition-status', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
|
||||
override async transitionStatus(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
return super.transitionStatus(request, response)
|
||||
}
|
||||
|
||||
@httpDelete('/:userUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
|
||||
override async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
return super.deleteAccount(request, response)
|
||||
|
||||
@@ -45,12 +45,25 @@ export class BaseSessionsController extends BaseHttpController {
|
||||
|
||||
const user = authenticateRequestResponse.user as User
|
||||
|
||||
const result = await this.createCrossServiceToken.execute({
|
||||
const sharedVaultOwnerContext = request.headers['x-shared-vault-owner-context'] as string | undefined
|
||||
|
||||
const resultOrError = await this.createCrossServiceToken.execute({
|
||||
user,
|
||||
session: authenticateRequestResponse.session,
|
||||
sharedVaultOwnerContext,
|
||||
})
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json({ authToken: result.token })
|
||||
return this.json({ authToken: resultOrError.getValue() })
|
||||
}
|
||||
|
||||
async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
|
||||
@@ -58,13 +58,22 @@ export class BaseSettingsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
const { userUuid, settingName } = request.params
|
||||
const result = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
|
||||
|
||||
if (result.success) {
|
||||
return this.json(result)
|
||||
const resultOrError = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json(result, 400)
|
||||
return this.json({
|
||||
success: true,
|
||||
...resultOrError.getValue(),
|
||||
})
|
||||
}
|
||||
|
||||
async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
|
||||
|
||||
@@ -14,15 +14,25 @@ export class BaseSubscriptionSettingsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.doGetSetting.execute({
|
||||
const resultOrError = await this.doGetSetting.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
settingName: request.params.subscriptionSettingName.toUpperCase(),
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
return this.json(result)
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json(result, 400)
|
||||
return this.json({
|
||||
success: true,
|
||||
...resultOrError.getValue(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { GetUserSubscription } from '../../../Domain/UseCase/GetUserSubscription
|
||||
import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts'
|
||||
import { UpdateUser } from '../../../Domain/UseCase/UpdateUser'
|
||||
import { ErrorTag } from '@standardnotes/responses'
|
||||
import { GetTransitionStatus } from '../../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
|
||||
|
||||
export class BaseUsersController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -20,6 +21,7 @@ export class BaseUsersController extends BaseHttpController {
|
||||
protected clearLoginAttempts: ClearLoginAttempts,
|
||||
protected increaseLoginAttempts: IncreaseLoginAttempts,
|
||||
protected changeCredentialsUseCase: ChangeCredentials,
|
||||
protected getTransitionStatusUseCase: GetTransitionStatus,
|
||||
private controllerContainer?: ControllerContainerInterface,
|
||||
) {
|
||||
super()
|
||||
@@ -30,6 +32,7 @@ export class BaseUsersController extends BaseHttpController {
|
||||
this.controllerContainer.register('auth.users.getSubscription', this.getSubscription.bind(this))
|
||||
this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.bind(this))
|
||||
this.controllerContainer.register('auth.users.delete', this.deleteAccount.bind(this))
|
||||
this.controllerContainer.register('auth.users.transition-status', this.transitionStatus.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +106,29 @@ export class BaseUsersController extends BaseHttpController {
|
||||
return this.json(result.keyParams)
|
||||
}
|
||||
|
||||
async transitionStatus(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.getTransitionStatusUseCase.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: result.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
|
||||
|
||||
return this.json({
|
||||
status: result.getValue(),
|
||||
})
|
||||
}
|
||||
|
||||
async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (request.params.userUuid !== response.locals.user.uuid) {
|
||||
return this.json(
|
||||
|
||||
@@ -46,10 +46,20 @@ export class BaseWebSocketsController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.createCrossServiceToken.execute({
|
||||
const resultOrError = await this.createCrossServiceToken.execute({
|
||||
userUuid: token.userUuid,
|
||||
})
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json({ authToken: result.token })
|
||||
return this.json({ authToken: resultOrError.getValue() })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as IORedis from 'ioredis'
|
||||
|
||||
import { TransitionStatusRepositoryInterface } from '../../Domain/Transition/TransitionStatusRepositoryInterface'
|
||||
|
||||
export class RedisTransitionStatusRepository implements TransitionStatusRepositoryInterface {
|
||||
private readonly PREFIX = 'transition'
|
||||
|
||||
constructor(private redisClient: IORedis.Redis) {}
|
||||
|
||||
async updateStatus(userUuid: string, status: 'STARTED' | 'FAILED'): Promise<void> {
|
||||
await this.redisClient.set(`${this.PREFIX}:${userUuid}`, status)
|
||||
}
|
||||
|
||||
async removeStatus(userUuid: string): Promise<void> {
|
||||
await this.redisClient.del(`${this.PREFIX}:${userUuid}`)
|
||||
}
|
||||
|
||||
async getStatus(userUuid: string): Promise<'STARTED' | 'FAILED' | null> {
|
||||
const status = (await this.redisClient.get(`${this.PREFIX}:${userUuid}`)) as 'STARTED' | 'FAILED' | null
|
||||
|
||||
return status
|
||||
}
|
||||
}
|
||||
@@ -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.12.14](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.13...@standardnotes/domain-events-infra@1.12.14) (2023-08-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.12.13](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.12...@standardnotes/domain-events-infra@1.12.13) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.12.12](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.11...@standardnotes/domain-events-infra@1.12.12) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.12.11",
|
||||
"version": "1.12.14",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.117.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.116.0...@standardnotes/domain-events@2.117.0) (2023-08-24)
|
||||
|
||||
### Features
|
||||
|
||||
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/server/issues/707)) ([05bb12c](https://github.com/standardnotes/server/commit/05bb12c97899824f06e6d01d105dec75fc328440))
|
||||
|
||||
# [2.116.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.115.1...@standardnotes/domain-events@2.116.0) (2023-08-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
|
||||
|
||||
## [2.115.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.115.0...@standardnotes/domain-events@2.115.1) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
# [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.115.0",
|
||||
"version": "2.117.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { SharedVaultFileMovedEventPayload } from './SharedVaultFileMovedEventPayload'
|
||||
|
||||
export interface SharedVaultFileMovedEvent extends DomainEventInterface {
|
||||
type: 'SHARED_VAULT_FILE_MOVED'
|
||||
payload: SharedVaultFileMovedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface SharedVaultFileMovedEventPayload {
|
||||
fileByteSize: number
|
||||
fileName: string
|
||||
from: {
|
||||
sharedVaultUuid?: string
|
||||
ownerUuid: string
|
||||
filePath: string
|
||||
}
|
||||
to: {
|
||||
sharedVaultUuid?: string
|
||||
ownerUuid: string
|
||||
filePath: string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
|
||||
import { TransitionStatusUpdatedEventPayload } from './TransitionStatusUpdatedEventPayload'
|
||||
|
||||
export interface TransitionStatusUpdatedEvent extends DomainEventInterface {
|
||||
type: 'TRANSITION_STATUS_UPDATED'
|
||||
payload: TransitionStatusUpdatedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface TransitionStatusUpdatedEventPayload {
|
||||
userUuid: string
|
||||
status: 'STARTED' | 'FINISHED' | 'FAILED'
|
||||
}
|
||||
@@ -64,6 +64,8 @@ export * from './Event/SharedSubscriptionInvitationCanceledEvent'
|
||||
export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
|
||||
export * from './Event/SharedSubscriptionInvitationCreatedEvent'
|
||||
export * from './Event/SharedSubscriptionInvitationCreatedEventPayload'
|
||||
export * from './Event/SharedVaultFileMovedEvent'
|
||||
export * from './Event/SharedVaultFileMovedEventPayload'
|
||||
export * from './Event/SharedVaultFileRemovedEvent'
|
||||
export * from './Event/SharedVaultFileRemovedEventPayload'
|
||||
export * from './Event/SharedVaultFileUploadedEvent'
|
||||
@@ -88,6 +90,8 @@ export * from './Event/SubscriptionRevertRequestedEvent'
|
||||
export * from './Event/SubscriptionRevertRequestedEventPayload'
|
||||
export * from './Event/SubscriptionSyncRequestedEvent'
|
||||
export * from './Event/SubscriptionSyncRequestedEventPayload'
|
||||
export * from './Event/TransitionStatusUpdatedEvent'
|
||||
export * from './Event/TransitionStatusUpdatedEventPayload'
|
||||
export * from './Event/UserDisabledSessionUserAgentLoggingEvent'
|
||||
export * from './Event/UserDisabledSessionUserAgentLoggingEventPayload'
|
||||
export * from './Event/UserEmailChangedEvent'
|
||||
|
||||
@@ -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.23](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.22...@standardnotes/event-store@1.11.23) (2023-08-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.22](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.21...@standardnotes/event-store@1.11.22) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.21](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.20...@standardnotes/event-store@1.11.21) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.20](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.19...@standardnotes/event-store@1.11.20) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.11.20",
|
||||
"version": "1.11.23",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
MODE=microservice # microservice | home-server
|
||||
MODE=microservice # microservice | home-server | self-hosted
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
VERSION=development
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.22.2](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.1...@standardnotes/files-server@1.22.2) (2023-08-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow self hosted to use new model of items ([#714](https://github.com/standardnotes/files/issues/714)) ([aef9254](https://github.com/standardnotes/files/commit/aef9254713560c00a90a3e84e3cd94417e8f30d2))
|
||||
|
||||
## [1.22.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.0...@standardnotes/files-server@1.22.1) (2023-08-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
# [1.22.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.21.0...@standardnotes/files-server@1.22.0) (2023-08-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/files/issues/705)) ([205a1ed](https://github.com/standardnotes/files/commit/205a1ed637b626be13fc656276508f3c7791024f))
|
||||
|
||||
# [1.21.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.4...@standardnotes/files-server@1.21.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/files/issues/704)) ([34085ac](https://github.com/standardnotes/files/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
|
||||
|
||||
## [1.20.4](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.3...@standardnotes/files-server@1.20.4) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.20.4",
|
||||
"version": "1.22.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -99,7 +99,9 @@ export class ContainerConfigLoader {
|
||||
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)
|
||||
container
|
||||
.bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory)
|
||||
.toConstantValue(new DomainEventFactory(container.get<TimerInterface>(TYPES.Files_Timer)))
|
||||
|
||||
if (isConfiguredForInMemoryCache) {
|
||||
container
|
||||
@@ -214,9 +216,26 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Files_DomainEventFactory),
|
||||
),
|
||||
)
|
||||
container.bind<GetFileMetadata>(TYPES.Files_GetFileMetadata).to(GetFileMetadata)
|
||||
container
|
||||
.bind<GetFileMetadata>(TYPES.Files_GetFileMetadata)
|
||||
.toConstantValue(
|
||||
new GetFileMetadata(
|
||||
container.get<FileDownloaderInterface>(TYPES.Files_FileDownloader),
|
||||
container.get<winston.Logger>(TYPES.Files_Logger),
|
||||
),
|
||||
)
|
||||
container.bind<RemoveFile>(TYPES.Files_RemoveFile).to(RemoveFile)
|
||||
container.bind<MoveFile>(TYPES.Files_MoveFile).to(MoveFile)
|
||||
container
|
||||
.bind<MoveFile>(TYPES.Files_MoveFile)
|
||||
.toConstantValue(
|
||||
new MoveFile(
|
||||
container.get<GetFileMetadata>(TYPES.Files_GetFileMetadata),
|
||||
container.get<FileMoverInterface>(TYPES.Files_FileMover),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory),
|
||||
container.get<winston.Logger>(TYPES.Files_Logger),
|
||||
),
|
||||
)
|
||||
container.bind<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
|
||||
|
||||
// middleware
|
||||
|
||||
@@ -4,16 +4,14 @@ import {
|
||||
DomainEventService,
|
||||
SharedVaultFileUploadedEvent,
|
||||
SharedVaultFileRemovedEvent,
|
||||
SharedVaultFileMovedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Files_Timer) private timer: TimerInterface) {}
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
createFileRemovedEvent(payload: {
|
||||
userUuid: string
|
||||
@@ -56,6 +54,34 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
}
|
||||
}
|
||||
|
||||
createSharedVaultFileMovedEvent(payload: {
|
||||
fileByteSize: number
|
||||
fileName: string
|
||||
from: {
|
||||
sharedVaultUuid?: string
|
||||
ownerUuid: string
|
||||
filePath: string
|
||||
}
|
||||
to: {
|
||||
sharedVaultUuid?: string
|
||||
ownerUuid: string
|
||||
filePath: string
|
||||
}
|
||||
}): SharedVaultFileMovedEvent {
|
||||
return {
|
||||
type: 'SHARED_VAULT_FILE_MOVED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: payload.from.sharedVaultUuid ?? payload.from.ownerUuid,
|
||||
userIdentifierType: payload.from.sharedVaultUuid ? 'shared-vault-uuid' : 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Files,
|
||||
},
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
createSharedVaultFileUploadedEvent(payload: {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
FileRemovedEvent,
|
||||
SharedVaultFileRemovedEvent,
|
||||
SharedVaultFileUploadedEvent,
|
||||
SharedVaultFileMovedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
@@ -19,6 +20,20 @@ export interface DomainEventFactoryInterface {
|
||||
fileByteSize: number
|
||||
regularSubscriptionUuid: string
|
||||
}): FileRemovedEvent
|
||||
createSharedVaultFileMovedEvent(payload: {
|
||||
fileByteSize: number
|
||||
fileName: string
|
||||
from: {
|
||||
sharedVaultUuid?: string
|
||||
ownerUuid: string
|
||||
filePath: string
|
||||
}
|
||||
to: {
|
||||
sharedVaultUuid?: string
|
||||
ownerUuid: string
|
||||
filePath: string
|
||||
}
|
||||
}): SharedVaultFileMovedEvent
|
||||
createSharedVaultFileUploadedEvent(payload: {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'reflect-metadata'
|
||||
import { Logger } from 'winston'
|
||||
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
||||
|
||||
@@ -19,10 +18,8 @@ describe('GetFileMetadata', () => {
|
||||
})
|
||||
|
||||
it('should return the file metadata', async () => {
|
||||
expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })).toEqual({
|
||||
success: true,
|
||||
size: 123,
|
||||
})
|
||||
const result = await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })
|
||||
expect(result.getValue()).toEqual(123)
|
||||
})
|
||||
|
||||
it('should not return the file metadata if it fails', async () => {
|
||||
@@ -30,9 +27,8 @@ describe('GetFileMetadata', () => {
|
||||
throw new Error('ooops')
|
||||
})
|
||||
|
||||
expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })).toEqual({
|
||||
success: false,
|
||||
message: 'Could not get file metadata.',
|
||||
})
|
||||
const result = await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,32 +1,20 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { GetFileMetadataDTO } from './GetFileMetadataDTO'
|
||||
import { GetFileMetadataResponse } from './GetFileMetadataResponse'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
@injectable()
|
||||
export class GetFileMetadata implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Files_FileDownloader) private fileDownloader: FileDownloaderInterface,
|
||||
@inject(TYPES.Files_Logger) private logger: Logger,
|
||||
) {}
|
||||
export class GetFileMetadata implements UseCaseInterface<number> {
|
||||
constructor(private fileDownloader: FileDownloaderInterface, private logger: Logger) {}
|
||||
|
||||
async execute(dto: GetFileMetadataDTO): Promise<GetFileMetadataResponse> {
|
||||
async execute(dto: GetFileMetadataDTO): Promise<Result<number>> {
|
||||
try {
|
||||
const size = await this.fileDownloader.getFileSize(`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
size,
|
||||
}
|
||||
return Result.ok(size)
|
||||
} catch (error) {
|
||||
this.logger.error(`Could not get file metadata for resource: ${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not get file metadata.',
|
||||
}
|
||||
|
||||
return Result.fail('Could not get file metadata')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export type GetFileMetadataResponse =
|
||||
| {
|
||||
success: true
|
||||
size: number
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
message: string
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user