Skip to content

Commit

Permalink
feat(auth): add script for fixing subscriptions with missing id state (
Browse files Browse the repository at this point in the history
…#1030)

* fix(auth): add subscription id safe guards on handlers

* feat(domain-events): add subscription state events

* feat(domain-events): add subscription state events

* feat(auth): add handling of subscription state fetched events

* feat(auth): add script for fixing subscriptions state
  • Loading branch information
karolsojko authored Jan 19, 2024
1 parent 6f07aaf commit 86b0508
Show file tree
Hide file tree
Showing 22 changed files with 363 additions and 0 deletions.
74 changes: 74 additions & 0 deletions packages/auth/bin/fix_subscriptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import 'reflect-metadata'

import { Logger } from 'winston'

import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { UserSubscriptionRepositoryInterface } from '../src/Domain/Subscription/UserSubscriptionRepositoryInterface'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
import { Uuid } from '@standardnotes/domain-core'

const fixSubscriptions = async (
userRepository: UserRepositoryInterface,
userSubscriptionRepository: UserSubscriptionRepositoryInterface,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
): Promise<void> => {
const subscriptions = await userSubscriptionRepository.findBySubscriptionId(0)

for (const subscription of subscriptions) {
const userUuidOrError = Uuid.create(subscription.userUuid)
if (userUuidOrError.isFailed()) {
continue
}
const userUuid = userUuidOrError.getValue()

const user = await userRepository.findOneByUuid(userUuid)
if (!user) {
continue
}

await domainEventPublisher.publish(
domainEventFactory.createSubscriptionStateRequestedEvent({
userEmail: user.email,
}),
)
}
}

const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
const env: Env = new Env()
env.load()

const logger: Logger = container.get(TYPES.Auth_Logger)

logger.info('Starting to fix subscriptions with missing subscriptionId ...')

const userRepository = container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository)
const userSubscriptionRepository = container.get<UserSubscriptionRepositoryInterface>(
TYPES.Auth_UserSubscriptionRepository,
)
const domainEventFactory = container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory)
const domainEventPublisher = container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher)

Promise.resolve(
fixSubscriptions(userRepository, userSubscriptionRepository, domainEventFactory, domainEventPublisher),
)
.then(() => {
logger.info('Finished fixing subscriptions with missing subscriptionId.')

process.exit(0)
})
.catch((error) => {
logger.error('Failed to fix subscriptions with missing subscriptionId.', {
error: error.message,
stack: error.stack,
})

process.exit(1)
})
})
11 changes: 11 additions & 0 deletions packages/auth/docker/entrypoint-fix-subscriptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict'

const path = require('path')

const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()

const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/fix_subscriptions.js')))

Object.defineProperty(exports, '__esModule', { value: true })

exports.default = index
4 changes: 4 additions & 0 deletions packages/auth/docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ case "$COMMAND" in
exec node docker/entrypoint-fix-roles.js
;;

'fix-subscriptions' )
exec node docker/entrypoint-fix-subscriptions.js
;;

'delete-accounts' )
FILE_NAME=$1 && shift 1
MODE=$1 && shift 1
Expand Down
12 changes: 12 additions & 0 deletions packages/auth/src/Bootstrap/Container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ import { RenewSharedSubscriptions } from '../Domain/UseCase/RenewSharedSubscript
import { FixStorageQuotaForUser } from '../Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser'
import { FileQuotaRecalculatedEventHandler } from '../Domain/Handler/FileQuotaRecalculatedEventHandler'
import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
import { SubscriptionStateFetchedEventHandler } from '../Domain/Handler/SubscriptionStateFetchedEventHandler'

export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {}
Expand Down Expand Up @@ -1579,6 +1580,16 @@ export class ContainerConfigLoader {
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<SubscriptionStateFetchedEventHandler>(TYPES.Auth_SubscriptionStateFetchedEventHandler)
.toConstantValue(
new SubscriptionStateFetchedEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)

const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Auth_AccountDeletionRequestedEventHandler)],
Expand Down Expand Up @@ -1620,6 +1631,7 @@ export class ContainerConfigLoader {
'FILE_QUOTA_RECALCULATED',
container.get<FileQuotaRecalculatedEventHandler>(TYPES.Auth_FileQuotaRecalculatedEventHandler),
],
['SUBSCRIPTION_STATE_FETCHED', container.get(TYPES.Auth_SubscriptionStateFetchedEventHandler)],
])

if (isConfiguredForHomeServer) {
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/Bootstrap/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ const TYPES = {
),
Auth_UserInvitedToSharedVaultEventHandler: Symbol.for('Auth_UserInvitedToSharedVaultEventHandler'),
Auth_FileQuotaRecalculatedEventHandler: Symbol.for('Auth_FileQuotaRecalculatedEventHandler'),
Auth_SubscriptionStateFetchedEventHandler: Symbol.for('Auth_SubscriptionStateFetchedEventHandler'),
// Services
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
Auth_SessionService: Symbol.for('Auth_SessionService'),
Expand Down
16 changes: 16 additions & 0 deletions packages/auth/src/Domain/Event/DomainEventFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
SessionRefreshedEvent,
AccountDeletionVerificationRequestedEvent,
FileQuotaRecalculationRequestedEvent,
SubscriptionStateRequestedEvent,
} from '@standardnotes/domain-events'
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
import { TimerInterface } from '@standardnotes/time'
Expand All @@ -34,6 +35,21 @@ import { KeyParamsData } from '@standardnotes/responses'
@injectable()
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Auth_Timer) private timer: TimerInterface) {}
createSubscriptionStateRequestedEvent(dto: { userEmail: string }): SubscriptionStateRequestedEvent {
return {
type: 'SUBSCRIPTION_STATE_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.userEmail,
userIdentifierType: 'email',
},
origin: DomainEventService.Auth,
},
payload: dto,
}
}

createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent {
return {
type: 'FILE_QUOTA_RECALCULATION_REQUESTED',
Expand Down
2 changes: 2 additions & 0 deletions packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import {
SessionRefreshedEvent,
AccountDeletionVerificationRequestedEvent,
FileQuotaRecalculationRequestedEvent,
SubscriptionStateRequestedEvent,
} from '@standardnotes/domain-events'
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
import { KeyParamsData } from '@standardnotes/responses'

export interface DomainEventFactoryInterface {
createSubscriptionStateRequestedEvent(dto: { userEmail: string }): SubscriptionStateRequestedEvent
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent
createWebSocketMessageRequestedEvent(dto: { userUuid: string; message: JSONString }): WebSocketMessageRequestedEvent
createEmailRequestedEvent(dto: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { Logger } from 'winston'

@injectable()
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
Expand All @@ -12,9 +13,20 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
@inject(TYPES.Auth_OfflineUserSubscriptionRepository)
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
@inject(TYPES.Auth_Logger) private logger: Logger,
) {}

async handle(event: SubscriptionCancelledEvent): Promise<void> {
if (!event.payload.subscriptionId) {
this.logger.error('Subscription ID is missing', {
codeTag: 'SubscriptionCancelledEventHandler.handle',
subscriptionId: event.payload.subscriptionId,
userId: event.payload.userEmail,
})

return
}

if (event.payload.offline) {
await this.updateOfflineSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
) {}

async handle(event: SubscriptionExpiredEvent): Promise<void> {
if (!event.payload.subscriptionId) {
this.logger.error('Subscription ID is missing', {
codeTag: 'SubscriptionExpiredEventHandler.handle',
subscriptionId: event.payload.subscriptionId,
userId: event.payload.userEmail,
})

return
}

if (event.payload.offline) {
await this.updateOfflineSubscriptionEndsAt(event.payload.subscriptionId, event.payload.timestamp)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
) {}

async handle(event: SubscriptionPurchasedEvent): Promise<void> {
if (!event.payload.subscriptionId) {
this.logger.error('Subscription ID is missing', {
codeTag: 'SubscriptionPurchasedEventHandler.handle',
subscriptionId: event.payload.subscriptionId,
userId: event.payload.userEmail,
})

return
}

if (event.payload.offline) {
const offlineUserSubscription = await this.createOfflineSubscription(
event.payload.subscriptionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
) {}

async handle(event: SubscriptionReassignedEvent): Promise<void> {
if (!event.payload.subscriptionId) {
this.logger.error('Subscription ID is missing', {
codeTag: 'SubscriptionReassignedEventHandler.handle',
subscriptionId: event.payload.subscriptionId,
userId: event.payload.userEmail,
})

return
}

const usernameOrError = Username.create(event.payload.userEmail)
if (usernameOrError.isFailed()) {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
) {}

async handle(event: SubscriptionRefundedEvent): Promise<void> {
if (!event.payload.subscriptionId) {
this.logger.error('Subscription ID is missing', {
codeTag: 'SubscriptionRefundedEventHandler.handle',
subscriptionId: event.payload.subscriptionId,
userId: event.payload.userEmail,
})

return
}

if (event.payload.offline) {
await this.updateOfflineSubscriptionEndsAt(event.payload.subscriptionId, event.payload.timestamp)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
) {}

async handle(event: SubscriptionRenewedEvent): Promise<void> {
if (!event.payload.subscriptionId) {
this.logger.error('Subscription ID is missing', {
codeTag: 'SubscriptionRenewedEventHandler.handle',
subscriptionId: event.payload.subscriptionId,
userId: event.payload.userEmail,
})

return
}

if (event.payload.offline) {
const offlineUserSubscription = await this.offlineUserSubscriptionRepository.findOneBySubscriptionId(
event.payload.subscriptionId,
Expand Down
Loading

0 comments on commit 86b0508

Please sign in to comment.