mirror of
https://github.com/standardnotes/server
synced 2026-08-28 11:57:18 -04:00
feat(home-server): add activating premium features (#624)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import { Result, ServiceInterface } from '@standardnotes/domain-core'
|
||||
|
||||
export interface AuthServiceInterface extends ServiceInterface {
|
||||
activatePremiumFeatures(username: string): Promise<Result<string>>
|
||||
}
|
||||
@@ -251,6 +251,7 @@ import { HomeServerValetTokenController } from '../Infra/InversifyExpressUtils/H
|
||||
import { HomeServerWebSocketsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerWebSocketsController'
|
||||
import { HomeServerSessionsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSessionsController'
|
||||
import { Transform } from 'stream'
|
||||
import { ActivatePremiumFeatures } from '../Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(configuration?: {
|
||||
@@ -781,6 +782,16 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_CryptoNode),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ActivatePremiumFeatures>(TYPES.Auth_ActivatePremiumFeatures)
|
||||
.toConstantValue(
|
||||
new ActivatePremiumFeatures(
|
||||
container.get(TYPES.Auth_UserRepository),
|
||||
container.get(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get(TYPES.Auth_RoleService),
|
||||
container.get(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
|
||||
container
|
||||
.bind<CleanupSessionTraces>(TYPES.Auth_CleanupSessionTraces)
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import {
|
||||
ControllerContainerInterface,
|
||||
Result,
|
||||
ServiceConfiguration,
|
||||
ServiceContainerInterface,
|
||||
ServiceIdentifier,
|
||||
ServiceInterface,
|
||||
} from '@standardnotes/domain-core'
|
||||
|
||||
import { ContainerConfigLoader } from './Container'
|
||||
import { DirectCallDomainEventPublisher } from '@standardnotes/domain-events-infra'
|
||||
import TYPES from './Types'
|
||||
import { Container } from 'inversify'
|
||||
import { ActivatePremiumFeatures } from '../Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures'
|
||||
import { AuthServiceInterface } from './AuthServiceInterface'
|
||||
|
||||
export class Service implements AuthServiceInterface {
|
||||
private container: Container | undefined
|
||||
|
||||
export class Service implements ServiceInterface {
|
||||
constructor(
|
||||
private serviceContainer: ServiceContainerInterface,
|
||||
private controllerContainer: ControllerContainerInterface,
|
||||
@@ -18,6 +24,16 @@ export class Service implements ServiceInterface {
|
||||
this.serviceContainer.register(this.getId(), this)
|
||||
}
|
||||
|
||||
async activatePremiumFeatures(username: string): Promise<Result<string>> {
|
||||
if (!this.container) {
|
||||
return Result.fail('Container not initialized')
|
||||
}
|
||||
|
||||
const activatePremiumFeatures = this.container.get(TYPES.Auth_ActivatePremiumFeatures) as ActivatePremiumFeatures
|
||||
|
||||
return activatePremiumFeatures.execute({ username })
|
||||
}
|
||||
|
||||
async handleRequest(request: never, response: never, endpointOrMethodIdentifier: string): Promise<unknown> {
|
||||
const method = this.controllerContainer.get(endpointOrMethodIdentifier)
|
||||
|
||||
@@ -31,12 +47,16 @@ export class Service implements ServiceInterface {
|
||||
async getContainer(configuration?: ServiceConfiguration): Promise<unknown> {
|
||||
const config = new ContainerConfigLoader()
|
||||
|
||||
return config.load({
|
||||
const container = await config.load({
|
||||
controllerConatiner: this.controllerContainer,
|
||||
directCallDomainEventPublisher: this.directCallDomainEventPublisher,
|
||||
logger: configuration?.logger,
|
||||
environmentOverrides: configuration?.environmentOverrides,
|
||||
})
|
||||
|
||||
this.container = container
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
getId(): ServiceIdentifier {
|
||||
|
||||
@@ -149,6 +149,7 @@ const TYPES = {
|
||||
Auth_ListAuthenticators: Symbol.for('Auth_ListAuthenticators'),
|
||||
Auth_DeleteAuthenticator: Symbol.for('Auth_DeleteAuthenticator'),
|
||||
Auth_GenerateRecoveryCodes: Symbol.for('Auth_GenerateRecoveryCodes'),
|
||||
Auth_ActivatePremiumFeatures: Symbol.for('Auth_ActivatePremiumFeatures'),
|
||||
Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'),
|
||||
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
|
||||
// Handlers
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './AuthServiceInterface'
|
||||
export * from './Service'
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
|
||||
import { ActivatePremiumFeatures } from './ActivatePremiumFeatures'
|
||||
import { User } from '../../User/User'
|
||||
|
||||
describe('ActivatePremiumFeatures', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let roleService: RoleServiceInterface
|
||||
let timer: TimerInterface
|
||||
let user: User
|
||||
|
||||
const createUseCase = () =>
|
||||
new ActivatePremiumFeatures(userRepository, userSubscriptionRepository, roleService, timer)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockResolvedValue(user)
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.save = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.getUTCDateNDaysAhead = jest.fn().mockReturnValue(new Date('2024-01-01T00:00:00.000Z'))
|
||||
})
|
||||
|
||||
it('should return error when username is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({ username: '' })
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Username cannot be empty')
|
||||
})
|
||||
|
||||
it('should return error when user is not found', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockResolvedValue(null)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({ username: '[email protected]' })
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('User not found with username: [email protected]')
|
||||
})
|
||||
|
||||
it('should save a subscription and add role to user', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({ username: '[email protected]' })
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
|
||||
expect(userSubscriptionRepository.save).toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Result, SubscriptionPlanName, UseCaseInterface, Username } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
|
||||
import { ActivatePremiumFeaturesDTO } from './ActivatePremiumFeaturesDTO'
|
||||
|
||||
export class ActivatePremiumFeatures implements UseCaseInterface<string> {
|
||||
constructor(
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
private roleService: RoleServiceInterface,
|
||||
private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: ActivatePremiumFeaturesDTO): Promise<Result<string>> {
|
||||
const usernameOrError = Username.create(dto.username)
|
||||
if (usernameOrError.isFailed()) {
|
||||
return Result.fail(usernameOrError.getError())
|
||||
}
|
||||
const username = usernameOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUsernameOrEmail(username)
|
||||
if (user === null) {
|
||||
return Result.fail(`User not found with username: ${username.value}`)
|
||||
}
|
||||
|
||||
const timestamp = this.timer.getTimestampInMicroseconds()
|
||||
|
||||
const subscription = new UserSubscription()
|
||||
subscription.planName = SubscriptionPlanName.NAMES.ProPlan
|
||||
subscription.user = Promise.resolve(user)
|
||||
subscription.createdAt = timestamp
|
||||
subscription.updatedAt = timestamp
|
||||
subscription.endsAt = this.timer.convertDateToMicroseconds(this.timer.getUTCDateNDaysAhead(365))
|
||||
subscription.cancelled = false
|
||||
subscription.subscriptionId = 1
|
||||
subscription.subscriptionType = UserSubscriptionType.Regular
|
||||
|
||||
await this.userSubscriptionRepository.save(subscription)
|
||||
|
||||
await this.roleService.addUserRole(user, SubscriptionPlanName.NAMES.ProPlan)
|
||||
|
||||
return Result.ok('Premium features activated.')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface ActivatePremiumFeaturesDTO {
|
||||
username: string
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { ControllerContainer, ServiceContainer } from '@standardnotes/domain-core'
|
||||
import { ControllerContainer, Result, ServiceContainer } from '@standardnotes/domain-core'
|
||||
import { Service as ApiGatewayService } from '@standardnotes/api-gateway'
|
||||
import { Service as FilesService } from '@standardnotes/files-server'
|
||||
import { DirectCallDomainEventPublisher } from '@standardnotes/domain-events-infra'
|
||||
import { Service as AuthService } from '@standardnotes/auth-server'
|
||||
import { Service as AuthService, AuthServiceInterface } from '@standardnotes/auth-server'
|
||||
import { Service as SyncingService } from '@standardnotes/syncing-server'
|
||||
import { Service as RevisionsService } from '@standardnotes/revisions-server'
|
||||
import { Container } from 'inversify'
|
||||
@@ -24,6 +24,7 @@ import { HomeServerConfiguration } from './HomeServerConfiguration'
|
||||
|
||||
export class HomeServer implements HomeServerInterface {
|
||||
private serverInstance: http.Server | undefined
|
||||
private authService: AuthServiceInterface | undefined
|
||||
private logStream: PassThrough = new PassThrough()
|
||||
|
||||
async start(configuration: HomeServerConfiguration): Promise<void> {
|
||||
@@ -49,6 +50,7 @@ export class HomeServer implements HomeServerInterface {
|
||||
|
||||
const apiGatewayService = new ApiGatewayService(serviceContainer)
|
||||
const authService = new AuthService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
|
||||
this.authService = authService
|
||||
const syncingService = new SyncingService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
|
||||
const revisionsService = new RevisionsService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
|
||||
const filesService = new FilesService(serviceContainer, directCallDomainEventPublisher)
|
||||
@@ -152,6 +154,14 @@ export class HomeServer implements HomeServerInterface {
|
||||
return this.serverInstance.address() !== null
|
||||
}
|
||||
|
||||
async activatePremiumFeatures(username: string): Promise<Result<string>> {
|
||||
if (!this.isRunning() || !this.authService) {
|
||||
return Result.fail('Home server is not running.')
|
||||
}
|
||||
|
||||
return this.authService.activatePremiumFeatures(username)
|
||||
}
|
||||
|
||||
logs(): NodeJS.ReadableStream {
|
||||
return this.logStream
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { HomeServerConfiguration } from './HomeServerConfiguration'
|
||||
|
||||
export interface HomeServerInterface {
|
||||
start(configuration?: HomeServerConfiguration): Promise<void>
|
||||
activatePremiumFeatures(username: string): Promise<Result<string>>
|
||||
stop(): Promise<void>
|
||||
isRunning(): Promise<boolean>
|
||||
logs(): NodeJS.ReadableStream
|
||||
|
||||
Reference in New Issue
Block a user