Compare commits

...

6 Commits

Author SHA1 Message Date
standardci
c9bf024109 chore(release): publish new version
- @standardnotes/api-gateway@1.87.6
 - @standardnotes/home-server@1.22.10
 - @standardnotes/websockets-server@1.21.3
2023-12-06 15:02:30 +00:00
Karol Sójko
529795d393 fix(api-gateway): add grpc logs for internal errors 2023-12-06 15:42:12 +01:00
Karol Sójko
79ae07623f fix(websockets): change error message on unknown errors 2023-12-05 14:58:48 +01:00
Micah Zoltu
6bdb524489 Adds support for loading environment vars from file. (#938)
* Adds support for loading environment from file.
2023-12-05 12:19:30 +01:00
standardci
480693fb9f chore(release): publish new version
- @standardnotes/auth-server@1.175.0
 - @standardnotes/home-server@1.22.9
2023-12-04 12:18:52 +00:00
Karol Sójko
e150930072 feat(auth): add renewal of shared subscriptions (#952) 2023-12-04 12:58:14 +01:00
19 changed files with 388 additions and 8 deletions

View File

@@ -1,5 +1,27 @@
#!/bin/bash
# usage: file_env VAR [DEFAULT]
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
exit 1
fi
local val="$def"
if [ "${!var:-}" ]; then
val="${!var}"
elif [ "${!fileVar:-}" ]; then
val="$(< "${!fileVar}")"
fi
export "$var"="$val"
unset "$fileVar"
}
# Setup environment variables
export MODE="self-hosted"
@@ -44,10 +66,12 @@ if [ -z "$DB_PORT" ]; then
echo "DB_PORT is not set. Please set it in your .env file."
exit 1
fi
file_env 'DB_USERNAME'
if [ -z "$DB_USERNAME" ]; then
echo "DB_USERNAME is not set. Please set it in your .env file."
exit 1
fi
file_env 'DB_PASSWORD'
if [ -z "$DB_PASSWORD" ]; then
echo "DB_PASSWORD is not set. Please set it in your .env file."
exit 1
@@ -89,11 +113,13 @@ fi
# SHARED #
##########
file_env 'AUTH_JWT_SECRET'
if [ -z "$AUTH_JWT_SECRET" ]; then
echo "AUTH_JWT_SECRET is not set. Please set it in your .env file. You can run 'openssl rand -hex 32' to generate a random string."
exit 1
fi
file_env 'VALET_TOKEN_SECRET'
if [ -z "$VALET_TOKEN_SECRET" ]; then
echo "VALET_TOKEN_SECRET is not set. Please set it in your .env file. You can run 'openssl rand -hex 32' to generate a random string."
exit 1
@@ -120,6 +146,7 @@ if [ -z "$AUTH_SERVER_DISABLE_USER_REGISTRATION" ]; then
export AUTH_SERVER_DISABLE_USER_REGISTRATION=false
fi
file_env 'AUTH_SERVER_PSEUDO_KEY_PARAMS_KEY'
if [ -z "$AUTH_SERVER_PSEUDO_KEY_PARAMS_KEY" ]; then
export AUTH_SERVER_PSEUDO_KEY_PARAMS_KEY=$(openssl rand -hex 32)
fi
@@ -142,6 +169,7 @@ if [ -z "$AUTH_SERVER_EPHEMERAL_SESSION_AGE" ]; then
export AUTH_SERVER_EPHEMERAL_SESSION_AGE=259200
fi
file_env 'AUTH_SERVER_ENCRYPTION_SERVER_KEY'
if [ -z "$AUTH_SERVER_ENCRYPTION_SERVER_KEY" ]; then
echo "AUTH_SERVER_ENCRYPTION_SERVER_KEY is not set. Please set it in your .env file. You can run 'openssl rand -hex 32' to generate a random string."
exit 1
@@ -161,9 +189,11 @@ fi
if [ -z "$AUTH_SERVER_SNS_ENDPOINT" ]; then
export AUTH_SERVER_SNS_ENDPOINT="http://localstack:4566"
fi
file_env 'AUTH_SERVER_SNS_SECRET_ACCESS_KEY'
if [ -z "$AUTH_SERVER_SNS_SECRET_ACCESS_KEY" ]; then
export AUTH_SERVER_SNS_SECRET_ACCESS_KEY="x"
fi
file_env 'AUTH_SERVER_SNS_ACCESS_KEY_ID'
if [ -z "$AUTH_SERVER_SNS_ACCESS_KEY_ID" ]; then
export AUTH_SERVER_SNS_ACCESS_KEY_ID="x"
fi
@@ -176,9 +206,11 @@ fi
if [ -z "$AUTH_SERVER_SQS_AWS_REGION" ]; then
export AUTH_SERVER_SQS_AWS_REGION="us-east-1"
fi
file_env 'AUTH_SERVER_SQS_ACCESS_KEY_ID'
if [ -z "$AUTH_SERVER_SQS_ACCESS_KEY_ID" ]; then
export AUTH_SERVER_SQS_ACCESS_KEY_ID="x"
fi
file_env 'AUTH_SERVER_SQS_SECRET_ACCESS_KEY'
if [ -z "$AUTH_SERVER_SQS_SECRET_ACCESS_KEY" ]; then
export AUTH_SERVER_SQS_SECRET_ACCESS_KEY="x"
fi
@@ -218,9 +250,11 @@ fi
if [ -z "$SYNCING_SERVER_SNS_ENDPOINT" ]; then
export SYNCING_SERVER_SNS_ENDPOINT="http://localstack:4566"
fi
file_env 'SYNCING_SERVER_SNS_SECRET_ACCESS_KEY'
if [ -z "$SYNCING_SERVER_SNS_SECRET_ACCESS_KEY" ]; then
export SYNCING_SERVER_SNS_SECRET_ACCESS_KEY="x"
fi
file_env 'SYNCING_SERVER_SNS_ACCESS_KEY_ID'
if [ -z "$SYNCING_SERVER_SNS_ACCESS_KEY_ID" ]; then
export SYNCING_SERVER_SNS_ACCESS_KEY_ID="x"
fi
@@ -233,9 +267,11 @@ fi
if [ -z "$SYNCING_SERVER_SQS_AWS_REGION" ]; then
export SYNCING_SERVER_SQS_AWS_REGION="us-east-1"
fi
file_env 'SYNCING_SERVER_SQS_ACCESS_KEY_ID'
if [ -z "$SYNCING_SERVER_SQS_ACCESS_KEY_ID" ]; then
export SYNCING_SERVER_SQS_ACCESS_KEY_ID="x"
fi
file_env 'SYNCING_SERVER_SQS_SECRET_ACCESS_KEY'
if [ -z "$SYNCING_SERVER_SQS_SECRET_ACCESS_KEY" ]; then
export SYNCING_SERVER_SQS_SECRET_ACCESS_KEY="x"
fi
@@ -278,9 +314,11 @@ fi
if [ -z "$FILES_SERVER_SNS_ENDPOINT" ]; then
export FILES_SERVER_SNS_ENDPOINT="http://localstack:4566"
fi
file_env 'FILES_SERVER_SNS_SECRET_ACCESS_KEY'
if [ -z "$FILES_SERVER_SNS_SECRET_ACCESS_KEY" ]; then
export FILES_SERVER_SNS_SECRET_ACCESS_KEY="x"
fi
file_env 'FILES_SERVER_SNS_ACCESS_KEY_ID'
if [ -z "$FILES_SERVER_SNS_ACCESS_KEY_ID" ]; then
export FILES_SERVER_SNS_ACCESS_KEY_ID="x"
fi
@@ -293,9 +331,11 @@ fi
if [ -z "$FILES_SERVER_SQS_AWS_REGION" ]; then
export FILES_SERVER_SQS_AWS_REGION="us-east-1"
fi
file_env 'FILES_SERVER_SQS_ACCESS_KEY_ID'
if [ -z "$FILES_SERVER_SQS_ACCESS_KEY_ID" ]; then
export FILES_SERVER_SQS_ACCESS_KEY_ID="x"
fi
file_env 'FILES_SERVER_SQS_SECRET_ACCESS_KEY'
if [ -z "$FILES_SERVER_SQS_SECRET_ACCESS_KEY" ]; then
export FILES_SERVER_SQS_SECRET_ACCESS_KEY="x"
fi
@@ -322,9 +362,11 @@ fi
if [ -z "$REVISIONS_SERVER_SNS_ENDPOINT" ]; then
export REVISIONS_SERVER_SNS_ENDPOINT="http://localstack:4566"
fi
file_env 'REVISIONS_SERVER_SNS_SECRET_ACCESS_KEY'
if [ -z "$REVISIONS_SERVER_SNS_SECRET_ACCESS_KEY" ]; then
export REVISIONS_SERVER_SNS_SECRET_ACCESS_KEY="x"
fi
file_env 'REVISIONS_SERVER_SNS_ACCESS_KEY_ID'
if [ -z "$REVISIONS_SERVER_SNS_ACCESS_KEY_ID" ]; then
export REVISIONS_SERVER_SNS_ACCESS_KEY_ID="x"
fi
@@ -337,9 +379,11 @@ fi
if [ -z "$REVISIONS_SERVER_SQS_AWS_REGION" ]; then
export REVISIONS_SERVER_SQS_AWS_REGION="us-east-1"
fi
file_env 'REVISIONS_SERVER_SQS_ACCESS_KEY_ID'
if [ -z "$REVISIONS_SERVER_SQS_ACCESS_KEY_ID" ]; then
export REVISIONS_SERVER_SQS_ACCESS_KEY_ID="x"
fi
file_env 'REVISIONS_SERVER_SQS_SECRET_ACCESS_KEY'
if [ -z "$REVISIONS_SERVER_SQS_SECRET_ACCESS_KEY" ]; then
export REVISIONS_SERVER_SQS_SECRET_ACCESS_KEY="x"
fi

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.87.6](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.87.5...@standardnotes/api-gateway@1.87.6) (2023-12-06)
### Bug Fixes
* **api-gateway:** add grpc logs for internal errors ([529795d](https://github.com/standardnotes/server/commit/529795d393442727833f318234d308543c1ea731))
## [1.87.5](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.87.4...@standardnotes/api-gateway@1.87.5) (2023-12-01)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

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

View File

@@ -210,6 +210,7 @@ export class ContainerConfigLoader {
container.get<MapperInterface<SyncResponse, SyncResponseHttpRepresentation>>(
TYPES.Mapper_SyncResponseGRPCMapper,
),
container.get<winston.Logger>(TYPES.ApiGateway_Logger),
),
)
container

View File

@@ -4,12 +4,15 @@ import { MapperInterface } from '@standardnotes/domain-core'
import { Metadata } from '@grpc/grpc-js'
import { SyncResponseHttpRepresentation } from '../../Mapping/Sync/Http/SyncResponseHttpRepresentation'
import { Status } from '@grpc/grpc-js/build/src/constants'
import { Logger } from 'winston'
export class GRPCSyncingServerServiceProxy {
constructor(
private syncingClient: ISyncingClient,
private syncRequestGRPCMapper: MapperInterface<Record<string, unknown>, SyncRequest>,
private syncResponseGRPCMapper: MapperInterface<SyncResponse, SyncResponseHttpRepresentation>,
private logger: Logger,
) {}
async sync(
@@ -39,12 +42,31 @@ export class GRPCSyncingServerServiceProxy {
})
}
if (error.code === Status.INTERNAL) {
this.logger.error(
`[GRPCSyncingServerServiceProxy] Internal gRPC error: ${error.message}. Payload: ${JSON.stringify(
payload,
)}`,
)
}
return reject(error)
}
return resolve({ status: 200, data: this.syncResponseGRPCMapper.toProjection(syncResponse) })
})
} catch (error) {
if (
'code' in (error as Record<string, unknown>) &&
(error as Record<string, unknown>).code === Status.INTERNAL
) {
this.logger.error(
`[GRPCSyncingServerServiceProxy] Internal gRPC error: ${JSON.stringify(error)}. Payload: ${JSON.stringify(
payload,
)}`,
)
}
reject(error)
}
})

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.175.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.174.4...@standardnotes/auth-server@1.175.0) (2023-12-04)
### Features
* **auth:** add renewal of shared subscriptions ([#952](https://github.com/standardnotes/server/issues/952)) ([e150930](https://github.com/standardnotes/server/commit/e15093007264489cf975a4d454e4a19e84bb13b7))
## [1.174.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.174.3...@standardnotes/auth-server@1.174.4) (2023-12-01)
### Bug Fixes

View File

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

View File

@@ -281,6 +281,7 @@ import { CSVFileReaderInterface } from '../Domain/CSV/CSVFileReaderInterface'
import { S3CsvFileReader } from '../Infra/S3/S3CsvFileReader'
import { DeleteAccountsFromCSVFile } from '../Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile'
import { AccountDeletionVerificationPassedEventHandler } from '../Domain/Handler/AccountDeletionVerificationPassedEventHandler'
import { RenewSharedSubscriptions } from '../Domain/UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions'
export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {}
@@ -1270,6 +1271,19 @@ export class ContainerConfigLoader {
container.get<TriggerEmailBackupForUser>(TYPES.Auth_TriggerEmailBackupForUser),
),
)
container
.bind<RenewSharedSubscriptions>(TYPES.Auth_RenewSharedSubscriptions)
.toConstantValue(
new RenewSharedSubscriptions(
container.get<ListSharedSubscriptionInvitations>(TYPES.Auth_ListSharedSubscriptionInvitations),
container.get<SharedSubscriptionInvitationRepositoryInterface>(
TYPES.Auth_SharedSubscriptionInvitationRepository,
),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
if (!isConfiguredForHomeServer) {
container
.bind<DeleteAccountsFromCSVFile>(TYPES.Auth_DeleteAccountsFromCSVFile)
@@ -1349,6 +1363,7 @@ export class ContainerConfigLoader {
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<RenewSharedSubscriptions>(TYPES.Auth_RenewSharedSubscriptions),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)

View File

@@ -169,6 +169,7 @@ const TYPES = {
Auth_TriggerEmailBackupForUser: Symbol.for('Auth_TriggerEmailBackupForUser'),
Auth_TriggerEmailBackupForAllUsers: Symbol.for('Auth_TriggerEmailBackupForAllUsers'),
Auth_DeleteAccountsFromCSVFile: Symbol.for('Auth_DeleteAccountsFromCSVFile'),
Auth_RenewSharedSubscriptions: Symbol.for('Auth_RenewSharedSubscriptions'),
// Handlers
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
Auth_AccountDeletionVerificationPassedEventHandler: Symbol.for('Auth_AccountDeletionVerificationPassedEventHandler'),

View File

@@ -11,6 +11,7 @@ import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
import { RenewSharedSubscriptions } from '../UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions'
export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInterface {
constructor(
@@ -19,6 +20,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings,
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
private roleService: RoleServiceInterface,
private renewSharedSubscriptions: RenewSharedSubscriptions,
private logger: Logger,
) {}
@@ -58,6 +60,17 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
event.payload.timestamp,
)
const renewalResult = await this.renewSharedSubscriptions.execute({
inviterEmail: user.email,
newSubscriptionId: event.payload.subscriptionId,
newSubscriptionName: event.payload.subscriptionName,
newSubscriptionExpiresAt: event.payload.subscriptionExpiresAt,
timestamp: event.payload.timestamp,
})
if (renewalResult.isFailed()) {
this.logger.error(`Could not renew shared subscriptions for user ${user.uuid}: ${renewalResult.getError()}`)
}
await this.addUserRole(user, event.payload.subscriptionName)
const result = await this.applyDefaultSubscriptionSettings.execute({

View File

@@ -0,0 +1,146 @@
import { Logger } from 'winston'
import { SharedSubscriptionInvitationRepositoryInterface } from '../../SharedSubscription/SharedSubscriptionInvitationRepositoryInterface'
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { ListSharedSubscriptionInvitations } from '../ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
import { RenewSharedSubscriptions } from './RenewSharedSubscriptions'
import { SharedSubscriptionInvitation } from '../../SharedSubscription/SharedSubscriptionInvitation'
import { InviteeIdentifierType } from '../../SharedSubscription/InviteeIdentifierType'
import { User } from '../../User/User'
import { InvitationStatus } from '../../SharedSubscription/InvitationStatus'
describe('RenewSharedSubscriptions', () => {
let listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations
let sharedSubscriptionInvitationRepository: SharedSubscriptionInvitationRepositoryInterface
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
let userRepository: UserRepositoryInterface
let logger: Logger
let sharedSubscriptionInvitation: SharedSubscriptionInvitation
let user: User
const createUseCase = () =>
new RenewSharedSubscriptions(
listSharedSubscriptionInvitations,
sharedSubscriptionInvitationRepository,
userSubscriptionRepository,
userRepository,
logger,
)
beforeEach(() => {
user = {} as jest.Mocked<User>
user.uuid = '00000000-0000-0000-0000-000000000000'
sharedSubscriptionInvitation = {} as jest.Mocked<SharedSubscriptionInvitation>
sharedSubscriptionInvitation.uuid = '00000000-0000-0000-0000-000000000000'
sharedSubscriptionInvitation.inviteeIdentifier = 'test@test.te'
sharedSubscriptionInvitation.inviteeIdentifierType = InviteeIdentifierType.Email
sharedSubscriptionInvitation.status = InvitationStatus.Accepted
listSharedSubscriptionInvitations = {} as jest.Mocked<ListSharedSubscriptionInvitations>
listSharedSubscriptionInvitations.execute = jest.fn().mockReturnValue({
invitations: [sharedSubscriptionInvitation],
})
sharedSubscriptionInvitationRepository = {} as jest.Mocked<SharedSubscriptionInvitationRepositoryInterface>
sharedSubscriptionInvitationRepository.save = jest.fn()
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
userSubscriptionRepository.save = jest.fn()
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
})
it('should renew shared subscriptions', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
inviterEmail: 'inviter@test.te',
newSubscriptionId: 123,
newSubscriptionName: 'test',
newSubscriptionExpiresAt: 123,
timestamp: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(sharedSubscriptionInvitationRepository.save).toBeCalledTimes(1)
expect(userSubscriptionRepository.save).toBeCalledTimes(1)
})
it('should log error if user not found', async () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
const useCase = createUseCase()
const result = await useCase.execute({
inviterEmail: 'inviter@test.te',
newSubscriptionId: 123,
newSubscriptionName: 'test',
newSubscriptionExpiresAt: 123,
timestamp: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(logger.error).toBeCalledTimes(1)
})
it('should log error if error occurs', async () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockImplementation(() => {
throw new Error('test')
})
const useCase = createUseCase()
const result = await useCase.execute({
inviterEmail: 'inviter@test.te',
newSubscriptionId: 123,
newSubscriptionName: 'test',
newSubscriptionExpiresAt: 123,
timestamp: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(logger.error).toBeCalledTimes(1)
})
it('should log error if username is invalid', async () => {
sharedSubscriptionInvitation.inviteeIdentifierType = InviteeIdentifierType.Email
sharedSubscriptionInvitation.inviteeIdentifier = ''
const useCase = createUseCase()
const result = await useCase.execute({
inviterEmail: 'inviter@test.te',
newSubscriptionId: 123,
newSubscriptionName: 'test',
newSubscriptionExpiresAt: 123,
timestamp: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(logger.error).toBeCalledTimes(1)
})
it('should renew shared subscription for invitations by user uuid', async () => {
sharedSubscriptionInvitation.inviteeIdentifierType = InviteeIdentifierType.Uuid
sharedSubscriptionInvitation.inviteeIdentifier = '00000000-0000-0000-0000-000000000000'
const useCase = createUseCase()
const result = await useCase.execute({
inviterEmail: 'inviter@test.te',
newSubscriptionId: 123,
newSubscriptionName: 'test',
newSubscriptionExpiresAt: 123,
timestamp: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(sharedSubscriptionInvitationRepository.save).toBeCalledTimes(1)
expect(userSubscriptionRepository.save).toBeCalledTimes(1)
})
})

View File

@@ -0,0 +1,104 @@
import { Result, UseCaseInterface, Username } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { RenewSharedSubscriptionsDTO } from './RenewSharedSubscriptionsDTO'
import { ListSharedSubscriptionInvitations } from '../ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
import { InvitationStatus } from '../../SharedSubscription/InvitationStatus'
import { SharedSubscriptionInvitationRepositoryInterface } from '../../SharedSubscription/SharedSubscriptionInvitationRepositoryInterface'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { InviteeIdentifierType } from '../../SharedSubscription/InviteeIdentifierType'
export class RenewSharedSubscriptions implements UseCaseInterface<void> {
constructor(
private listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations,
private sharedSubscriptionInvitationRepository: SharedSubscriptionInvitationRepositoryInterface,
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
private userRepository: UserRepositoryInterface,
private logger: Logger,
) {}
async execute(dto: RenewSharedSubscriptionsDTO): Promise<Result<void>> {
const result = await this.listSharedSubscriptionInvitations.execute({
inviterEmail: dto.inviterEmail,
})
const acceptedInvitations = result.invitations.filter(
(invitation) => invitation.status === InvitationStatus.Accepted,
)
for (const invitation of acceptedInvitations) {
try {
const userUuid = await this.getInviteeUserUuid(invitation.inviteeIdentifier, invitation.inviteeIdentifierType)
if (userUuid === null) {
this.logger.error(
`[SUBSCRIPTION: ${dto.newSubscriptionId}] Could not renew shared subscription for invitation: ${invitation.uuid}: Could not find user with identifier: ${invitation.inviteeIdentifier}`,
)
continue
}
await this.createSharedSubscription({
subscriptionId: dto.newSubscriptionId,
subscriptionName: dto.newSubscriptionName,
userUuid,
timestamp: dto.timestamp,
subscriptionExpiresAt: dto.newSubscriptionExpiresAt,
})
invitation.subscriptionId = dto.newSubscriptionId
invitation.updatedAt = dto.timestamp
await this.sharedSubscriptionInvitationRepository.save(invitation)
} catch (error) {
this.logger.error(
`[SUBSCRIPTION: ${dto.newSubscriptionId}] Could not renew shared subscription for invitation: ${
invitation.uuid
}: ${(error as Error).message}`,
)
}
}
return Result.ok()
}
private async createSharedSubscription(dto: {
subscriptionId: number
subscriptionName: string
userUuid: string
subscriptionExpiresAt: number
timestamp: number
}): Promise<UserSubscription> {
const subscription = new UserSubscription()
subscription.planName = dto.subscriptionName
subscription.userUuid = dto.userUuid
subscription.createdAt = dto.timestamp
subscription.updatedAt = dto.timestamp
subscription.endsAt = dto.subscriptionExpiresAt
subscription.cancelled = false
subscription.subscriptionId = dto.subscriptionId
subscription.subscriptionType = UserSubscriptionType.Shared
return this.userSubscriptionRepository.save(subscription)
}
private async getInviteeUserUuid(inviteeIdentifier: string, inviteeIdentifierType: string): Promise<string | null> {
if (inviteeIdentifierType === InviteeIdentifierType.Email) {
const usernameOrError = Username.create(inviteeIdentifier)
if (usernameOrError.isFailed()) {
return null
}
const username = usernameOrError.getValue()
const user = await this.userRepository.findOneByUsernameOrEmail(username)
if (user === null) {
return null
}
return user.uuid
}
return inviteeIdentifier
}
}

View File

@@ -0,0 +1,7 @@
export interface RenewSharedSubscriptionsDTO {
inviterEmail: string
newSubscriptionId: number
newSubscriptionExpiresAt: number
newSubscriptionName: string
timestamp: number
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.22.10](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.9...@standardnotes/home-server@1.22.10) (2023-12-06)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.9](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.8...@standardnotes/home-server@1.22.9) (2023-12-04)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.8](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.7...@standardnotes/home-server@1.22.8) (2023-12-01)
**Note:** Version bump only for package @standardnotes/home-server

View File

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

View File

@@ -3,6 +3,13 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.3](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.21.2...@standardnotes/websockets-server@1.21.3) (2023-12-06)
### Bug Fixes
* **api-gateway:** add grpc logs for internal errors ([529795d](https://github.com/standardnotes/server/commit/529795d393442727833f318234d308543c1ea731))
* **websockets:** change error message on unknown errors ([79ae076](https://github.com/standardnotes/server/commit/79ae07623fa392f1f60160ecdc34a0fcfa4d9a48))
## [1.21.2](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.21.1...@standardnotes/websockets-server@1.21.2) (2023-12-01)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.21.2",
"version": "1.21.3",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -69,7 +69,7 @@ describe('SendMessageToClient', () => {
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe(
'Could not send message to connection connection-id for user 00000000-0000-0000-0000-000000000000. Error: error',
'Could not send message to connection connection-id for user 00000000-0000-0000-0000-000000000000. Error: {}',
)
})

View File

@@ -45,9 +45,9 @@ export class SendMessageToClient implements UseCaseInterface<void> {
}
} catch (error) {
return Result.fail(
`Could not send message to connection ${connection.props.connectionId} for user ${userUuid.value}. Error: ${
(error as Error).message
}`,
`Could not send message to connection ${connection.props.connectionId} for user ${
userUuid.value
}. Error: ${JSON.stringify(error)}`,
)
}
}