Skip to content

Commit

Permalink
Add new checks on plan (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pierre-Gilles committed Nov 20, 2023
1 parent b71c808 commit b4c4202
Show file tree
Hide file tree
Showing 16 changed files with 552 additions and 9 deletions.
16 changes: 13 additions & 3 deletions core/api/account/account.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,15 @@ module.exports = function AccountModel(logger, db, redisClient, stripeService, m
// await stripeService.addTaxRate(subscription.id);

const { email } = customer;
const stripeProductId = subscription?.items?.data[0]?.price?.product;

// we first test if an account already exist with this email
const account = await db.t_account.findOne({ name: email });

// it means an account already exist with this email
if (account !== null) {
throw new AlreadyExistError();
telegramService.sendAlert(`Customer already have an account! Email = ${email}, language = ${language}.`);
throw new AlreadyExistError(`User ${email} already have an account!`);
}

const newAccount = {
Expand All @@ -88,6 +90,7 @@ module.exports = function AccountModel(logger, db, redisClient, stripeService, m
stripe_subscription_id: subscription.id,
current_period_end: new Date(subscription.current_period_end * 1000),
status: 'active',
plan: stripeProductId === process.env.STRIPE_LITE_PLAN_PRODUCT_ID ? 'lite' : 'plus',
};

const insertedAccount = await db.t_account.insert(newAccount);
Expand Down Expand Up @@ -467,12 +470,18 @@ module.exports = function AccountModel(logger, db, redisClient, stripeService, m
break;

case 'customer.subscription.updated':
// update status
// eslint-disable-next-line no-case-declarations
const stripeProductId = event.data.object.items?.data[0]?.price?.product;
// Update status
await db.t_account.update(
account.id,
{
status: event.data.object.status,
current_period_end: new Date(event.data.object.current_period_end * 1000),
current_period_end:
event.data.object.status === 'active' || event.data.object.status === 'trialing'
? new Date(event.data.object.current_period_end * 1000)
: new Date(),
plan: stripeProductId === process.env.STRIPE_LITE_PLAN_PRODUCT_ID ? 'lite' : 'plus',
},
{
fields: ['id'],
Expand Down Expand Up @@ -523,6 +532,7 @@ module.exports = function AccountModel(logger, db, redisClient, stripeService, m

case 'customer.subscription.deleted':
// subscription is canceled, remove the client
telegramService.sendAlert(`Subscription canceled! Customer email = ${email}, language = ${language}`);
break;

default:
Expand Down
3 changes: 0 additions & 3 deletions core/api/camera/camera.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,6 @@ module.exports = function CameraController(
* @apiGroup Camera
*/
async function startStreaming(req, res, next) {
const user = await userModel.getMySelf(req.user);
telegramService.sendAlert(`User ${user.email} starting stream !`);
const streamAccessKey = (await randomBytes(36)).toString('hex');
await redisClient.set(`${STREAMING_ACCESS_KEY_PREFIX}:${streamAccessKey}`, req.user.id, {
EX: 60 * 60, // 1 hour in second
Expand Down Expand Up @@ -217,7 +215,6 @@ module.exports = function CameraController(
*/
async function cleanCameraLive(req, res) {
validateSessionId(req.params.session_id);
telegramService.sendAlert(`End of camera stream, session_id = ${req.params.session_id} !`);
const folder = `${req.instance.id}/${req.params.session_id}`;
await emptyS3Directory(process.env.CAMERA_STORAGE_BUCKET, folder);
res.json({ success: true });
Expand Down
17 changes: 17 additions & 0 deletions core/api/instance/instance.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,22 @@ module.exports = function InstanceModel(logger, db, redisClient, jwtService, fin
return instanceId;
}

async function getAccountByInstanceId(instanceId) {
const accounts = await db.query(
`
SELECT t_account.id, t_account.plan, t_account.status
FROM t_account
JOIN t_instance ON t_instance.account_id = t_account.id
WHERE t_instance.id = $1;
`,
[instanceId],
);
if (accounts.length === 0) {
throw new NotFoundError('Instance not found');
}
return accounts[0];
}

async function setInstanceAsPrimaryInstance(accountId, instanceId) {
await db.withTransaction(async (tx) => {
// set all other instances in account as secondary instance
Expand Down Expand Up @@ -240,5 +256,6 @@ module.exports = function InstanceModel(logger, db, redisClient, jwtService, fin
getPrimaryInstanceByAccount,
getPrimaryInstanceIdByUserId,
setInstanceAsPrimaryInstance,
getAccountByInstanceId,
};
};
16 changes: 15 additions & 1 deletion core/api/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ module.exports.load = function Routes(app, io, controllers, middlewares) {
app.post(
'/openai/ask',
asyncMiddleware(middlewares.accessTokenInstanceAuth),
middlewares.checkUserPlan('plus'),
middlewares.openAIAuthAndRateLimit,
asyncMiddleware(controllers.openAIController.ask),
);
Expand Down Expand Up @@ -375,31 +376,37 @@ module.exports.load = function Routes(app, io, controllers, middlewares) {
app.get(
'/enedis/initialize',
asyncMiddleware(middlewares.accessTokenAuth({ scope: 'dashboard:write' })),
middlewares.checkUserPlan('plus'),
asyncMiddleware(controllers.enedisController.initialize),
);
app.post(
'/enedis/finalize',
asyncMiddleware(middlewares.accessTokenAuth({ scope: 'dashboard:write' })),
middlewares.checkUserPlan('plus'),
asyncMiddleware(controllers.enedisController.finalize),
);
app.get(
'/enedis/metering_data/consumption_load_curve',
asyncMiddleware(middlewares.accessTokenInstanceAuth),
middlewares.checkUserPlan('plus'),
asyncMiddleware(controllers.enedisController.meteringDataConsumptionLoadCurve),
);
app.get(
'/enedis/metering_data/daily_consumption',
asyncMiddleware(middlewares.accessTokenInstanceAuth),
middlewares.checkUserPlan('plus'),
asyncMiddleware(controllers.enedisController.meteringDataDailyConsumption),
);
app.post(
'/enedis/refresh_all',
asyncMiddleware(middlewares.accessTokenAuth({ scope: 'dashboard:write' })),
middlewares.checkUserPlan('plus'),
asyncMiddleware(controllers.enedisController.refreshAllData),
);
app.get(
'/enedis/sync',
asyncMiddleware(middlewares.accessTokenAuth({ scope: 'dashboard:read' })),
middlewares.checkUserPlan('plus'),
asyncMiddleware(controllers.enedisController.getEnedisSync),
);

Expand All @@ -425,12 +432,18 @@ module.exports.load = function Routes(app, io, controllers, middlewares) {
);

// Backup
app.get('/backups', asyncMiddleware(middlewares.accessTokenInstanceAuth), controllers.backupController.get);
app.get(
'/backups',
asyncMiddleware(middlewares.accessTokenInstanceAuth),
middlewares.checkUserPlan('plus'),
controllers.backupController.get,
);

// Backup multi-part upload
app.post(
'/backups/multi_parts/initialize',
asyncMiddleware(middlewares.accessTokenInstanceAuth),
middlewares.checkUserPlan('plus'),
controllers.backupController.initializeMultipartUpload,
);
app.post(
Expand All @@ -451,6 +464,7 @@ module.exports.load = function Routes(app, io, controllers, middlewares) {
app.post(
'/cameras/streaming/start',
asyncMiddleware(middlewares.accessTokenAuth({ scope: 'dashboard:read' })),
middlewares.checkUserPlan('plus'),
controllers.cameraController.startStreaming,
);
app.post(
Expand Down
3 changes: 2 additions & 1 deletion core/api/user/user.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ module.exports = function UserModel(logger, db, redisClient, jwtService, mailSer
`
SELECT t_user.id, t_user.name, t_user.email, t_user.role, t_user.language,
t_user.profile_url, t_user.gladys_user_id, t_user.gladys_4_user_id, t_user.account_id,
(t_account.current_period_end + interval '24 hour') as current_period_end
(t_account.current_period_end + interval '24 hour') as current_period_end, t_account.plan as plan,
t_account.status as status
FROM t_user
JOIN t_account ON t_user.account_id = t_account.id
WHERE t_user.id = $1
Expand Down
2 changes: 2 additions & 0 deletions core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const requestExecutionTime = require('./middleware/requestExecutionTime');
const AdminApiAuth = require('./middleware/adminApiAuth');
const OpenAIAuthAndRateLimit = require('./middleware/openAIAuthAndRateLimit');
const CameraStreamAccessKeyAuth = require('./middleware/cameraStreamAccessKeyAuth');
const CheckUserPlan = require('./middleware/checkUserPlan');

// Routes
const routes = require('./api/routes');
Expand Down Expand Up @@ -236,6 +237,7 @@ module.exports = async (port) => {
adminApiAuth: AdminApiAuth(logger, legacyRedisClient),
openAIAuthAndRateLimit: OpenAIAuthAndRateLimit(logger, legacyRedisClient, db),
cameraStreamAccessKeyAuth: CameraStreamAccessKeyAuth(redisClient, logger),
checkUserPlan: CheckUserPlan(models.userModel, models.instanceModel, logger),
};

routes.load(app, io, controllers, middlewares);
Expand Down
34 changes: 34 additions & 0 deletions core/middleware/checkUserPlan.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const { PaymentRequiredError } = require('../common/error');
const asyncMiddleware = require('./asyncMiddleware');

const ALLOWED_ACCOUNT_STATUS = ['active', 'trialing'];

module.exports = function checkUserPlan(userModel, instanceModel, logger) {
return function checkUserPlanByPlan(plan) {
return asyncMiddleware(async (req, res, next) => {
let account;
// This middleware serves user
if (req.user) {
logger.debug(`checkUserPlan: Verify that user ${req.user.id} has access to plan ${plan} and is active.`);
account = await userModel.getMySelf(req.user);
}
// and instances!
if (req.instance) {
logger.debug(
`checkUserPlan: Verify that instance ${req.instance.id} has access to plan ${plan} and is active.`,
);
account = await instanceModel.getAccountByInstanceId(req.instance.id);
}

if (account.plan !== plan) {
throw new PaymentRequiredError(`Account is in plan ${account.plan} and should be in plan ${plan}`);
}

if (ALLOWED_ACCOUNT_STATUS.indexOf(account.status) === -1) {
throw new PaymentRequiredError(`Account is not active`);
}

next();
});
};
};
3 changes: 2 additions & 1 deletion core/middleware/errorMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ module.exports = function getErrorMiddleware(logger) {
error instanceof ForbiddenError ||
error instanceof UnauthorizedError ||
error instanceof BadRequestError ||
error instanceof TooManyRequestsError
error instanceof TooManyRequestsError ||
error instanceof PaymentRequiredError
) {
return res.status(error.getStatus()).json(error.jsonError());
}
Expand Down
53 changes: 53 additions & 0 deletions migrations/20231110144143-add-plan-to-account.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

var dbm;
var type;
var seed;
var fs = require('fs');
var path = require('path');
var Promise;

/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function(options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
Promise = options.Promise;
};

exports.up = function(db) {
var filePath = path.join(__dirname, 'sqls', '20231110144143-add-plan-to-account-up.sql');
return new Promise( function( resolve, reject ) {
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
if (err) return reject(err);
console.log('received data: ' + data);

resolve(data);
});
})
.then(function(data) {
return db.runSql(data);
});
};

exports.down = function(db) {
var filePath = path.join(__dirname, 'sqls', '20231110144143-add-plan-to-account-down.sql');
return new Promise( function( resolve, reject ) {
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
if (err) return reject(err);
console.log('received data: ' + data);

resolve(data);
});
})
.then(function(data) {
return db.runSql(data);
});
};

exports._meta = {
"version": 1
};
5 changes: 5 additions & 0 deletions migrations/sqls/20231110144143-add-plan-to-account-down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE t_account
DROP COLUMN plan;

DROP TYPE account_plan_type;

8 changes: 8 additions & 0 deletions migrations/sqls/20231110144143-add-plan-to-account-up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TYPE account_plan_type AS ENUM(
'plus',
'lite'
);

ALTER TABLE t_account
ADD COLUMN plan account_plan_type DEFAULT 'plus' NOT NULL;

2 changes: 2 additions & 0 deletions test/bootstrap.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ before(async function Before() {
'-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----';
process.env.POSTGRESQL_DATABASE = process.env.POSTGRESQL_DATABASE_TEST;
process.env.STRIPE_SECRET_KEY = 'test';
process.env.STRIPE_ENDPOINT_SECRET = 'test';
process.env.STRIPE_LITE_PLAN_PRODUCT_ID = 'lite-product-id';
process.env.OPEN_AI_MAX_REQUESTS_PER_MONTH_PER_ACCOUNT = 100;

// starting 2 backends to try multi-server socket exchange
Expand Down
Loading

0 comments on commit b4c4202

Please sign in to comment.