Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new checks on plan #155

Merged
merged 6 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@
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');

Check warning on line 215 in core/api/instance/instance.model.js

View check run for this annotation

Codecov / codecov/patch

core/api/instance/instance.model.js#L215

Added line #L215 was not covered by tests
}
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 @@
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