Compare commits

...

5 Commits

17 changed files with 239 additions and 24 deletions

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.64.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.64.0...@standardnotes/api-gateway@1.64.1) (2023-06-09)
### Bug Fixes
* **api-gateway:** direct call service proxy to return 400 responses instead of throwing errors ([e6a4cc3](https://github.com/standardnotes/api-gateway/commit/e6a4cc3098bdf84fc9d48ed0d9098ebb52afb0e7))
# [1.64.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.63.2...@standardnotes/api-gateway@1.64.0) (2023-06-05)
### Features

View File

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

View File

@@ -1,7 +1,7 @@
import { Request, Response } from 'express'
import { ServiceContainerInterface, ServiceIdentifier } from '@standardnotes/domain-core'
import { ServiceProxyInterface } from '../Http/ServiceProxyInterface'
import { ServiceContainerInterface, ServiceIdentifier } from '@standardnotes/domain-core'
export class DirectCallServiceProxy implements ServiceProxyInterface {
constructor(private serviceContainer: ServiceContainerInterface, private filesServerUrl: string) {}
@@ -34,8 +34,12 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
}
}
async callEmailServer(_request: Request, _response: Response, _endpointOrMethodIdentifier: string): Promise<void> {
throw new Error('Email server is not available.')
async callEmailServer(_request: Request, response: Response, _endpointOrMethodIdentifier: string): Promise<void> {
response.status(400).send({
error: {
message: 'Email server is not available.',
},
})
}
async callAuthServer(request: never, response: never, endpointOrMethodIdentifier: string): Promise<void> {
@@ -54,10 +58,14 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
async callAuthServerWithLegacyFormat(
_request: Request,
_response: Response,
response: Response,
_endpointOrMethodIdentifier: string,
): Promise<void> {
throw new Error('Legacy auth endpoints are no longer available.')
response.status(400).send({
error: {
message: 'Legacy auth endpoints are no longer available.',
},
})
}
async callRevisionsServer(request: never, response: never, endpointOrMethodIdentifier: string): Promise<void> {
@@ -92,22 +100,30 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
async callLegacySyncingServer(
_request: Request,
_response: Response,
response: Response,
_endpointOrMethodIdentifier: string,
): Promise<void> {
throw new Error('Legacy syncing server endpoints are no longer available.')
response.status(400).send({
error: {
message: 'Legacy syncing server endpoints are no longer available.',
},
})
}
async callPaymentsServer(_request: Request, _response: Response, _endpointOrMethodIdentifier: string): Promise<void> {
throw new Error('Payments server is not available.')
async callPaymentsServer(_request: Request, response: Response, _endpointOrMethodIdentifier: string): Promise<void> {
response.status(400).send({
error: {
message: 'Payments server is not available.',
},
})
}
async callWebSocketServer(
_request: Request,
_response: Response,
_endpointOrMethodIdentifier: string,
): Promise<void> {
throw new Error('Websockets server is not available.')
async callWebSocketServer(_request: Request, response: Response, _endpointOrMethodIdentifier: string): Promise<void> {
response.status(400).send({
error: {
message: 'Websockets server is not available.',
},
})
}
private sendDecoratedResponse(

View File

@@ -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.119.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.119.0...@standardnotes/auth-server@1.119.1) (2023-06-09)
### Bug Fixes
* **home-server:** add default value for valet token ttl ([c201ee4](https://github.com/standardnotes/server/commit/c201ee42a00d9e5402afea2f2c5848a362c1529e))
# [1.119.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.118.0...@standardnotes/auth-server@1.119.0) (2023-06-09)
### Features
* **home-server:** add activating premium features ([#624](https://github.com/standardnotes/server/issues/624)) ([72ce190](https://github.com/standardnotes/server/commit/72ce1909960fbd2ec6a47b8dbdfbe53a4f10e776))
# [1.118.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.117.0...@standardnotes/auth-server@1.118.0) (2023-06-07)
### Features

View File

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

View File

@@ -0,0 +1,5 @@
import { Result, ServiceInterface } from '@standardnotes/domain-core'
export interface AuthServiceInterface extends ServiceInterface {
activatePremiumFeatures(username: string): Promise<Result<string>>
}

View File

@@ -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?: {
@@ -489,7 +490,9 @@ export class ContainerConfigLoader {
.bind(TYPES.Auth_AUTH_JWT_TTL)
.toConstantValue(env.get('AUTH_JWT_TTL', true) ? +env.get('AUTH_JWT_TTL') : 60_000)
container.bind(TYPES.Auth_VALET_TOKEN_SECRET).toConstantValue(env.get('VALET_TOKEN_SECRET', true))
container.bind(TYPES.Auth_VALET_TOKEN_TTL).toConstantValue(+env.get('VALET_TOKEN_TTL', true))
container
.bind(TYPES.Auth_VALET_TOKEN_TTL)
.toConstantValue(env.get('VALET_TOKEN_TTL', true) ? +env.get('VALET_TOKEN_TTL', true) : 7200)
container
.bind(TYPES.Auth_WEB_SOCKET_CONNECTION_TOKEN_SECRET)
.toConstantValue(env.get('WEB_SOCKET_CONNECTION_TOKEN_SECRET', true))
@@ -781,6 +784,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)

View File

@@ -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 {

View File

@@ -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

View File

@@ -1 +1,2 @@
export * from './AuthServiceInterface'
export * from './Service'

View File

@@ -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: 'test@test.te' })
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('User not found with username: test@test.te')
})
it('should save a subscription and add role to user', async () => {
const useCase = createUseCase()
const result = await useCase.execute({ username: 'test@test.te' })
expect(result.isFailed()).toBe(false)
expect(userSubscriptionRepository.save).toHaveBeenCalled()
expect(roleService.addUserRole).toHaveBeenCalled()
})
})

View File

@@ -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.')
}
}

View File

@@ -0,0 +1,3 @@
export interface ActivatePremiumFeaturesDTO {
username: string
}

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.1](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.0...@standardnotes/home-server@1.11.1) (2023-06-09)
**Note:** Version bump only for package @standardnotes/home-server
# [1.11.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.10.0...@standardnotes/home-server@1.11.0) (2023-06-09)
### Features
* **home-server:** add activating premium features ([#624](https://github.com/standardnotes/server/issues/624)) ([72ce190](https://github.com/standardnotes/server/commit/72ce1909960fbd2ec6a47b8dbdfbe53a4f10e776))
# [1.10.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.9.0...@standardnotes/home-server@1.10.0) (2023-06-07)
### Features

View File

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

View File

@@ -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
}

View File

@@ -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