Skip to content

Commit

Permalink
Merged in account-subscription (pull request auth0#30)
Browse files Browse the repository at this point in the history
Account subscription
  • Loading branch information
David Lenton committed Dec 10, 2021
2 parents 525ed49 + 495709e commit f991180
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 1 deletion.
2 changes: 2 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,10 @@ if (process.env.NODE_ENV !== 'test') {
}

// Set request processing defaults
app.use('/api/account/stripe/webhook', bodyParser.json({verify:function(req,res,buf){req.rawBody=buf}}));
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ extended: false, limit: '50mb' }));

app.use(cookieParser());
app.use(cors());

Expand Down
90 changes: 90 additions & 0 deletions models/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ class User {
});
});
}
addLevel(levelId, subscription, done) {
db.get().query('INSERT INTO account_level (account_id, level_id, subscription_ref, client_id) VALUES (?, ?, ?, ?)', [this.id, levelId, subscription, this.client_id], (err) => {
return done(err);
})
}
addProfile(profileId, options, done) {
const { Audience } = require('./audience');
// This has been changed so that audience programs are also added
Expand All @@ -142,6 +147,28 @@ class User {
return done(null, result);
});
}
addProfiles(profiles, options, done) {
const _this = this;
const fn = function r(p) {
return new Promise((resolve, reject) => {
if (!p) return resolve();
_this.addProfile(p, options, (err) => {
if (err) { reject(err); }
resolve();
});
});
};
const actions = profiles.map(fn);
const results = Promise.all(actions);
results.then(() => {
return done();
});
}
addSubscription(subscription, done) {
db.get().query('REPLACE INTO account_subscription (subscription_ref, client_id) VALUES (?, ?)', [subscription, this.client_id], (err) => {
return done(err);
})
}
addTag(tag, notificationId, done) {
const _this = this;
const tags = {};
Expand All @@ -154,6 +181,22 @@ class User {
_this.setEmailTags(tagIds, true, done);
});
}
addTransaction(transaction, done) {
const _this = this;
const validUntil = transaction[6];
const transactionStatus = transaction[6];
const subscriptionRef = transaction[7];
db.get().query('INSERT INTO account_transaction (account_id, transaction_date, amount, renewal_type, valid_from, valid_until, transaction_status, transaction_ref, client_id) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', transaction, (err) => {
if (transactionStatus === 'active') {
this.setSubscription(subscriptionRef, validUntil, (err) => {
return done(err);
})
} else {
return done(err);
}
})
}
calculateREPI(options, done) {
const { Audience } = require('./audience');
options = options || {};
Expand Down Expand Up @@ -303,6 +346,15 @@ class User {
done(null, bookmarks);
});
}
getByCustomerId(customerId, done) {
db.get().query('SELECT id FROM account WHERE stripe_id = ? AND client_id = ?', [customerId, this.client_id], (err, rows) => {
if (err) return done(err);
if (rows && rows.length > 0) {
return this.get(rows[0].id, done);
}
return done(new Error('Too many or too few records'));
});
}
getDevices(done) {
// const _this = this;
db.get().query('SELECT * FROM account_device WHERE account_id = ? AND client_id = ?', [this.id, this.client_id], (err, rows) => {
Expand Down Expand Up @@ -503,6 +555,29 @@ class User {
return done(null, result[0]);
});
}
getSubscription(done) {
const query = "SELECT DISTINCT \
account.id, \
if(l.level, l.name, u.name) as level_name, \
if(l.level,l.level,1) as `level`, \
valid_until, \
account_level.level_id \
FROM \
account \
LEFT JOIN account_level \
ON account.id = account_level.account_id \
LEFT JOIN account_subscription \
ON account_level.subscription_ref = account_subscription.subscription_ref \
AND (account_subscription.valid_until >= now() OR account_subscription.valid_until is null) \
LEFT JOIN level l ON account_level.level_id = l.level \
LEFT JOIN level u ON u.level = 1 \
WHERE account.id = ? AND account.client_id = ?";
db.get().query(query, [this.id, this.client_id], (err, rows) => {
if (err) return done(err);
if (rows && rows.length > 0) return done(null, rows[0]);
return done();
});
}
getTags(done) {
db.get().query('SELECT name, id, account_id FROM profile LEFT JOIN account_profile ON profile.id = account_profile.profile_id AND account_id = ? AND account_profile.client_id = ? WHERE profile.client_id = ? ', [this.id, this.client_id, this.client_id], (err, rows) => {
if (err)
Expand Down Expand Up @@ -1192,6 +1267,11 @@ class User {
done(null, result);
});
}
setCustomerId(customerId, done) {
db.get().query('UPDATE account SET stripe_id = ? WHERE id = ? and client_id = ?', [customerId, this.id, this.client_id], (err) => {
return done(err);
})
}
setEmailTags(tagIds, state, done) {
const _this = this;
const emailTags = [];
Expand Down Expand Up @@ -1269,6 +1349,16 @@ class User {
done(null, result);
});
}
setSubscriptionDate(subscription, enddate, done) {
db.get().query('UPDATE account_subscription SET valid_until = ? WHERE subscription_ref = ? AND client_id = ?', [enddate, subscription, this.client_id], (err) => {
return done(err);
})
}
setSubscriptionStatus(subscription, status, done) {
db.get().query('UPDATE account_subscription SET status = ? WHERE subscription_ref = ? AND client_id = ?', [status, subscription, this.client_id], (err) => {
return done(err);
})
}
setTags(tags, notificationId, done) {
// Get all devices associated with this user
this.getDevices((err, devices) => {
Expand Down
3 changes: 3 additions & 0 deletions models/audience.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ class Audience {
where += 'AND name LIKE ? ';
params.push('%' + options.searchText + '%');
}
if (options.hasCommunity) {
where += 'AND community = 1 ';
}
// If this is a user, get restrict the list to the specified user's audiences
if (options.userId) {
where += 'AND profile.id IN \
Expand Down
Binary file modified mysql/database.mwb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"shortid": "^2.2.14",
"socket.io": "^2.4.1",
"stream": "0.0.2",
"stripe": "^8.191.0",
"touch-dnd": "^1.2.1",
"tree-kit": "^0.5.26",
"utf-8-validate": "^5.0.2",
Expand Down
143 changes: 143 additions & 0 deletions routes/api/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ const { OAuth2Client } = require('google-auth-library');
const appleSignin = require('apple-signin-auth');
const jwt = require('jsonwebtoken');

// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://dashboard.stripe.com/apikeys
const stripe = require('stripe')(process.env.STRIPE_KEY);

const passports = new Passports();
// const permissions = require('../../models/permissions');
const tokens = require('../../models/tokens');
Expand Down Expand Up @@ -256,6 +260,130 @@ module.exports = function (app) {
});
});

router.post('/stripe/webhook', async (req, res) => {
const appId = req.site.server.client_id;
let data;
let eventType;
// Check if webhook signing is configured.
const webhookSecret = process.env.STRIPE_WEBHOOK_KEY;
if (webhookSecret) {
// Retrieve the event by verifying the signature using the raw body and secret.
let event;
let signature = req.headers["stripe-signature"];
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
signature,
webhookSecret
);
} catch (err) {
console.log(`⚠️ Webhook signature verification failed.`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Extract the object from the event.
data = event.data;
eventType = event.type;
} else {
// Webhook signing is recommended, but if the secret is not configured in `config.js`,
// retrieve the event data directly from the request body.
data = req.body.data;
eventType = req.body.type;
}
const account = new User(appId);
const options = {
queue: app.queue,
notification: true,
hostname: req.hostname,
protocol: req.protocol,
appId: appId,
notificationId: req.site.notification.id,
notificationKey: req.site.notification.key,
sitename: req.site.sitename,
sitenameShort: req.site.content.shortname,
email: req.site.content.email
};
switch (eventType) {
case 'checkout.session.completed':
// Payment is successful and the subscription is created.
// You should provision the subscription and save the customer ID to your database.
account.id = parseInt(data.object.client_reference_id);
account.setCustomerId(data.object.customer, async (err) => {
if (err) console.log(err);
// Save the profile information for this subscription
const subscription = await stripe.subscriptions.retrieve(data.object.subscription, { expand: ['plan.product'] });
const levelId = (subscription.plan.product.metadata && subscription.plan.product.metadata.levelId) ? subscription.plan.product.metadata.levelId : 1;
const audiences = (subscription.plan.product.metadata && subscription.plan.product.metadata.audienceId) ? subscription.plan.product.metadata.audienceId.split(',') : null;
account.addSubscription(subscription.id, (err) => {
if (err) console.log(err);
account.addLevel(levelId, data.object.subscription, (err) => {
if (err) console.log(err);
if (audiences) {
account.addProfiles(audiences, options, () => {
});
}
})
})
});
break;
case 'customer.subscription.updated':
// Subscription has been updated.
// cancel_at, id
const enddate = new Date(data.object.current_period_end*1000).toMysqlFormat();
account.setSubscriptionDate(data.object.id, enddate, (err) => {
if (err) console.log(err);
})
// account.getByCustomerId(data.object.customer, (err) => {
// const created = new Date(data.object.created*1000).toMysqlFormat();
// const validFrom = new Date(data.object.current_period_start*1000).toMysqlFormat();
// const validUntil = new Date(data.object.current_period_end*1000).toMysqlFormat();
// const transactionRef = data.object.id;
// const transactionStatus = data.object.status;
// const amount = data.object.plan.amount;
// const renewalType = data.object.plan.interval;
// const transaction = [
// account.id,
// created,
// amount,
// renewalType,
// validFrom,
// validUntil,
// transactionStatus,
// transactionRef,
// appId
// ]
// account.addTransaction(transaction, (err) => {
// if (err) console.log(err);
// });
// })
break;
case 'invoice.paid':
// Continue to provision the subscription as payments continue to be made.
// Store the status in your database and check when a user accesses your service.
// This approach helps you avoid hitting rate limits.
// account.getByCustomerId(data.object.customer, (err) => {
// if (err) console.log(err);
// const enddate = new Date(data.object.period_end*1000).toMysqlFormat();
// account.addSubscription(data.object.subscription, enddate, (err) => {
// console.log(err);
// });
account.setSubscriptionStatus(data.object.subscription, 'Paid', (err) => {
if (err) console.log(err);
})
break;
case 'invoice.payment_failed':
// The payment failed or the customer does not have a valid payment method.
// The subscription becomes past_due. Notify your customer and send them to the
// customer portal to update their payment information.
account.setSubscriptionStatus(data.object.subscription, 'Failed', (err) => {
if (err) console.log(err);
})
break;
default:
// Unhandled event type
}
res.send();
});

/* POST refresh user token */
router.post('/token/refresh', cors(), (req, res, next) => {
if (!req.body || !req.body.token)
Expand Down Expand Up @@ -977,6 +1105,21 @@ module.exports = function (app) {
});
});

/* GET user subscription details */
router.get('/subscription/', (req, res, next) => {
req.user.getSubscription((err, json) => {
if (err) return next(new JsonError('Unable to load subscription details'));
if (!json) return res.send({ level: 1 });
const role = {
id: json.account_level_id,
name: json.level_name,
level: json.level,
valid_until: json.valid_until
}
res.send(role);
});
});

/* GET unique identifier for user */
router.get('/uniqueid/get/', (req, res, next) => {
req.user.getRID(null, (err, json) => {
Expand Down
7 changes: 6 additions & 1 deletion routes/api/audience.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,14 @@ module.exports = function () {
const audience = new Audience(appId);
const search = req.query.s || null;
const page = req.query.p || 1;
// Defaulting to 1 so that the RASA app
// will return only the live communities
// - when updated we can default this to null
const hasCommunity = req.query.community || 1;
const options = {
searchText: search,
baseUrl: '/audience'
baseUrl: '/audience',
hasCommunity: hasCommunity
};
// Unless the current user is an administrator or editor of the entire
// system, then restrict their access to the audiences to which they are
Expand Down

0 comments on commit f991180

Please sign in to comment.