From 54ac3d74086a4915bbcfe82e10017359b1783d3b Mon Sep 17 00:00:00 2001 From: Daniele Guido Date: Sat, 4 Jan 2025 20:23:43 +0100 Subject: [PATCH] Feature/change plan service (#481) * Add available plan service in configuration `availablePlans` * Create and register service `user-request-change-plan` * Upgrade after npm audit fix --- package-lock.json | 20 +++--- src/configuration.ts | 1 + src/models/user-change-plan-request.ts | 69 +++++++++++++++++++ src/schema/common/config.json | 7 ++ src/services/index.ts | 1 + .../user-change-plan-request.class.ts | 66 ++++++++++++++++++ .../user-change-plan-request.service.ts | 40 +++++++++++ 7 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 src/models/user-change-plan-request.ts create mode 100644 src/services/user-change-plan-request/user-change-plan-request.class.ts create mode 100644 src/services/user-change-plan-request/user-change-plan-request.service.ts diff --git a/package-lock.json b/package-lock.json index 7d448b10..fa918476 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5473,10 +5473,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", - "license": "MIT", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -5497,7 +5496,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -5512,6 +5511,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-openapi-validator": { @@ -9407,10 +9410,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", - "license": "MIT" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "node_modules/path-type": { "version": "4.0.0", diff --git a/src/configuration.ts b/src/configuration.ts index 6e1f46de..cb32776b 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -30,6 +30,7 @@ export interface Configuration extends Config { celeryClient?: CeleryClient cacheManager: Cache openApiValidatorMiddlewares: any[] + availablePlans: string[] } const configurationValidator = getValidator(configurationSchema, ajv) diff --git a/src/models/user-change-plan-request.ts b/src/models/user-change-plan-request.ts new file mode 100644 index 00000000..b1f3aac5 --- /dev/null +++ b/src/models/user-change-plan-request.ts @@ -0,0 +1,69 @@ +import type { Sequelize } from 'sequelize' +import { DataTypes, Model, InferAttributes, InferCreationAttributes, CreationOptional } from 'sequelize' +import Group from './groups.model' + +export default class UserChangePlanRequest extends Model< + InferAttributes, + InferCreationAttributes +> { + declare id: CreationOptional + declare dateCreated: Date + declare dateLastModified: Date + declare status: string + declare notes: string + declare changelog: object + declare planId: number + declare userId: number + + static initModel(client: Sequelize) { + const groupModel = Group.initModel(client) + const model = UserChangePlanRequest.init( + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + unique: true, + }, + dateCreated: { + type: DataTypes.DATE, + allowNull: false, + field: 'date_created', + }, + dateLastModified: { + type: DataTypes.DATE, + allowNull: false, + field: 'date_last_modified', + }, + status: { + type: DataTypes.STRING, + allowNull: false, + }, + changelog: { + type: DataTypes.JSON, + allowNull: false, + }, + notes: { + type: DataTypes.STRING, + allowNull: true, + }, + planId: { + type: DataTypes.INTEGER, + allowNull: false, + field: 'plan_id', + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + field: 'user_id', + }, + }, + { + sequelize: client, + tableName: 'impresso_userchangeplanrequest', + } + ) + model.hasOne(groupModel, { foreignKey: 'id', sourceKey: 'planId', as: 'plan' }) + return model + } +} diff --git a/src/schema/common/config.json b/src/schema/common/config.json index 160b33e0..86725de3 100644 --- a/src/schema/common/config.json +++ b/src/schema/common/config.json @@ -11,6 +11,13 @@ "type": "boolean", "description": "If `true`, the app serves a public API" }, + "availablePlans": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of available plans" + }, "allowedCorsOrigins": { "type": "array", "items": { diff --git a/src/services/index.ts b/src/services/index.ts index a636bc3a..9cb0adff 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -63,6 +63,7 @@ const internalApiServices = [ 'password-reset', 'change-password', 'terms-of-use', + 'user-change-plan-request', 'user-requests', 'user-requests-reviews', 'newspapers', diff --git a/src/services/user-change-plan-request/user-change-plan-request.class.ts b/src/services/user-change-plan-request/user-change-plan-request.class.ts new file mode 100644 index 00000000..08667d90 --- /dev/null +++ b/src/services/user-change-plan-request/user-change-plan-request.class.ts @@ -0,0 +1,66 @@ +import type { Sequelize } from 'sequelize' +import initDebug from 'debug' +import type { ImpressoApplication } from '../../types' +import User from '../../models/users.model' +import { NotFound } from '@feathersjs/errors' +import UserChangePlanRequest from '../../models/user-change-plan-request' + +const debug = initDebug('impresso:services/change-plan') + +export interface ServiceOptions { + app: ImpressoApplication + name: string +} + +export class Service { + app: ImpressoApplication + name: string + sequelizeClient?: Sequelize + constructor({ app, name }: ServiceOptions) { + this.app = app + this.name = name + this.sequelizeClient = app.get('sequelizeClient') + debug('Service initialized.') + } + + async find(params: { user: { id: number } }) { + if (!this.sequelizeClient) { + throw new Error('Sequelize client not available') + } + debug('[find] plan request for user.pk', params.user.id, params.user) + const userChangePlanRequestModel = UserChangePlanRequest.initModel(this.sequelizeClient) + const userChangePlanRequest = await userChangePlanRequestModel.findOne({ + where: { + userId: params.user.id, + }, + include: ['plan'], + }) + if (!userChangePlanRequest) { + throw new NotFound() + } + return userChangePlanRequest?.get() + } + + async create(data: any, params: { user: { id: number } }) { + const client = this.app.get('celeryClient') + if (!client) { + throw new Error('Celery client not available') + } + + debug('[create] plan request for user.pk', params.user.id, 'plan:', data.plan, params.user) + + return client + .run({ + task: 'impresso.tasks.email_plan_change', + // email_plan_change(self, user_id: int, plan: str = None) + args: [params.user.id, data.plan], + }) + .catch((error: Error) => { + debug('[create] error:', error) + throw error + }) + .then(() => ({ + response: 'ok', + })) + } +} diff --git a/src/services/user-change-plan-request/user-change-plan-request.service.ts b/src/services/user-change-plan-request/user-change-plan-request.service.ts new file mode 100644 index 00000000..55116eab --- /dev/null +++ b/src/services/user-change-plan-request/user-change-plan-request.service.ts @@ -0,0 +1,40 @@ +import { Service } from './user-change-plan-request.class' +import { ImpressoApplication } from '../../types' +import { HookContext, ServiceOptions } from '@feathersjs/feathers' +import { authenticateAround } from '../../hooks/authenticate' +import { BadRequest } from '@feathersjs/errors' + +export default (app: ImpressoApplication) => { + app.use( + '/user-change-plan-request', + new Service({ + app, + name: 'user-change-plan-request', + }), + { + events: [], + } as ServiceOptions + ) + const service = app.service('user-change-plan-request') + service.hooks({ + around: { + all: [authenticateAround()], + }, + before: { + create: [ + (context: HookContext) => { + const { plan } = context.data + if (!plan || typeof plan !== 'string') { + throw new BadRequest('`plan` param is required') + } + const availablePlans = context.app.get('availablePlans') + + if (!availablePlans.includes(plan)) { + throw new BadRequest('Invalid plan, should be one of:', availablePlans) + } + return context + }, + ], + }, + }) +}