Merge pull request #39 from manojVivek/trial_license

Trial license
This commit is contained in:
Manoj Vivek 2019-09-15 11:55:28 +05:30 committed by GitHub
commit 4042191f4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 647 additions and 7 deletions

6
server/environment.yml Normal file
View file

@ -0,0 +1,6 @@
MONGO_USERNAME:
MONGO_PASSWORD:
MONGO_HOSTNAME:
MONGO_PORT:
MONGO_DB:
SENDGRID_API_KEY:

View file

@ -10,7 +10,11 @@
"author": "",
"license": "ISC",
"dependencies": {
"serverless": "^1.51.0"
"@sendgrid/mail": "^6.4.0",
"aws-sdk": "^2.528.0",
"mongoose": "^5.6.13",
"serverless": "^1.51.0",
"serverless-http": "^2.3.0"
},
"devDependencies": {
"@babel/core": "^7.6.0",

View file

@ -1,5 +1,3 @@
org: manojvivek
app: responsively
service: responsively-server
custom:
@ -10,11 +8,28 @@ provider:
name: aws
runtime: nodejs10.x
websocketsApiName: responsively-websocket
apiName: responsively-server
websocketsApiRouteSelectionExpression: $request.body.action # custom routes are selected by the value of the action property in the body
environment: ${file(environment.yml)}
functions:
activateTrial:
handler: src/http/lambda.activateTrial
timeout: 15
events:
- http:
path: activate-trial
method: get
validateLicenseHttp:
handler: src/http/lambda.validateLicense
timeout: 15
events:
- http:
path: validate-license
method: get
websocket-connection-manager:
handler: src/websocket/lambda.connectionHandler
timeout: 15
events:
- websocket:
route: $connect
@ -22,13 +37,21 @@ functions:
route: $disconnect
defaultHandler:
handler: src/websocket/lambda.defaultHandler
timeout: 15
events:
- websocket: $default #simple event definition without extra route property
pingHandler:
handler: src/websocket/lambda.pingHandler
timeout: 15
events:
- websocket:
route: ping # will trigger if $request.body.action === "ping"
validateLicense:
handler: src/websocket/lambda.validateLicense
timeout: 15
events:
- websocket:
route: validate # will trigger if $request.body.action === "validate"
plugins:
- serverless-webpack
- serverless-webpack

View file

@ -3,7 +3,7 @@ const webpack = require('webpack');
process.env.NODE_ENV = slsw.lib.options.stage || 'development';
const mode =
process.env.NODE_ENV === 'development' ? 'development' : 'production';
process.env.NODE_ENV === 'development' ? 'development' : 'production';
module.exports = {
entry: slsw.lib.entries,

View file

@ -0,0 +1,24 @@
const TRIAL_PLAN='TRIAL'
const SUBSCRIPTION_STATUS=Object.freeze({
ACTIVE:{
id: 'ACTIVE'
},
EXPIRED:{
id:'EXPIRED'
}
})
const SUBSCRIPTION_DURATION=Object.freeze({
MONTH:{
id: 'ACTIVE'
},
YEAR:{
id:'EXPIRED'
}
})
module.exports={
TRIAL_PLAN: TRIAL_PLAN,
SUBSCRIPTION_STATUS: SUBSCRIPTION_STATUS
}

View file

@ -0,0 +1,8 @@
class InvalidEmailError extends Error{
constructor(message){
super(message)
this.name=this.constructor.name
}
}
module.exports=InvalidEmailError

View file

@ -0,0 +1,8 @@
class InvalidLicenseError extends Error{
constructor(message){
super(message)
this.name=this.constructor.name
}
}
module.exports=InvalidLicenseError

View file

@ -0,0 +1,8 @@
class InvalidSubscriptionError extends Error{
constructor(message){
super(message)
this.name=this.constructor.name
}
}
module.exports=InvalidSubscriptionError

View file

@ -0,0 +1,8 @@
class UserExistsError extends Error{
constructor(message){
super(message)
this.name=this.constructor.name
}
}
module.exports=UserExistsError

View file

@ -0,0 +1,19 @@
const mongoose = require('mongoose')
let Schema=mongoose.Schema
const connectionsSchema=new Schema({
connection_id: {type: String},
start_time: Date
},{_id: false})
const activeConnectionsSchema=new Schema({
_id: {type: String},
connections:[connectionsSchema],
c_ts: Date,
u_ts: Date
})
const ACTIVE_CONNECTIONS_MODEL_NAME='ActiveConnection'
const COLLECTION_NAME='active_connections'
const ActiveConnection=mongoose.model(ACTIVE_CONNECTIONS_MODEL_NAME,activeConnectionsSchema,COLLECTION_NAME)
module.exports=ActiveConnection

View file

@ -0,0 +1,16 @@
const mongoose = require('mongoose')
let Schema=mongoose.Schema
const planSchema=new Schema({
name: {type: String},
user_limit: {type: Number},
duration:{type: String},
c_ts: {type: Date},
u_ts: {type: Date},
price: {type: String}
})
const PLAN_MODEL_NAME='Plan'
const COLLECTION_NAME='plans'
const Plan=mongoose.model(PLAN_MODEL_NAME,planSchema,COLLECTION_NAME)
module.exports=Plan

View file

@ -0,0 +1,16 @@
const mongoose = require('mongoose')
let Schema=mongoose.Schema
const subscriptionSchema=new Schema({
user_id: {type: String},
plan_id: {type: String},
start_date:Date,
status: {type: String},
c_ts: Date,
u_ts: Date
})
const SUBSCRIPTION_MODEL_NAME='Subscription'
const COLLECTION_NAME='subscriptions'
const Subscription=mongoose.model(SUBSCRIPTION_MODEL_NAME,subscriptionSchema,COLLECTION_NAME)
module.exports=Subscription

View file

@ -0,0 +1,15 @@
const mongoose = require('mongoose')
let Schema=mongoose.Schema
const userSchema=new Schema({
name: {type: String},
email: {type: String},
license_key:{type: String},
c_ts: Date,
u_ts: Date
})
const USER_MODEL_NAME='User'
const COLLECTION_NAME='users'
const User=mongoose.model(USER_MODEL_NAME,userSchema,COLLECTION_NAME)
module.exports=User

View file

@ -0,0 +1,73 @@
const ActiveConnection=require('../model/active-connections.model')
const subscriptionService=require('../service/subscription.service')
const planService=require('../service/plan.service')
const InvalidSubscriptionError=require('../exception/invalid-subscription.exception')
const InvalidLicenseError=require('../exception/invalid-license-error.exception')
const AWS=require('aws-sdk')
async function addNewConnection(licenseKey,connectionId){
let responseBody={}
try{
let subscription=await subscriptionService.getSubscriptionByLicenseKey(licenseKey)
if(!subscription){
throw new Error('no active subscriptions found')
}
const allowedUsers=await planService.getCurrentUserLimitForPlan(subscription.start_date,subscription.plan_id)
if(allowedUsers && allowedUsers>0){
const connectionObj={
connection_id: connectionId,
start_time: new Date()
}
const updateObj={
$push:{connections:connectionObj},
$set:{u_ts:new Date()},
$setOnInsert:{c_ts: new Date()}
}
let existingConnection=await ActiveConnection.findOneAndUpdate({_id: licenseKey},updateObj,{upsert:true})
await removeOldConnection(allowedUsers,existingConnection)
}else{
throw new InvalidSubscriptionError('0 user limit found for plan')
}
}catch(err){
console.log(err)
throw err
}
}
async function removeOldConnection(userLimit,existingConnection){
if(existingConnection && existingConnection.connections){
let connections=existingConnection.connections
if(connections.length>=userLimit){
await ActiveConnection.updateOne({_id:existingConnection._id},{$pop:{connections: -1},$set:{u_ts:new Date()}})
}
}
}
async function closeConnection(connectionId){
console.log('disconnecting:'+connectionId)
await ActiveConnection.updateOne({'connections.connection_id': connectionId},{$pull:{connections:{connection_id:connectionId}}})
console.log('disconnecting:'+connectionId+' done')
}
async function publish(event,connectionId,data){
console.log('publishing data to client connection:'+connectionId+',data:'+JSON.stringify(data))
const apigwManagementApi = new AWS.ApiGatewayManagementApi({
endpoint: event.requestContext.domainName + '/' + event.requestContext.stage
});
await apigwManagementApi.postToConnection({
ConnectionId: connectionId,
Data: JSON.stringify(data)
}).promise()
console.log('publishing data to client connection:'+connectionId+',data:'+data+' ..done')
}
module.exports={
addNewConnection,
closeConnection,
publish
}
// addNewConnection("asd","aasd").then(res=>{
// console.log('done')
// })

View file

@ -0,0 +1,31 @@
const sgMail = require('@sendgrid/mail');
const emailUtils=require('../utils/email.utils')
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const SYSTEM_MAIL='noreply@responsively.app'
async function sendLicenseKeyMail(email,licenseKey){
if(process.env.SEND_MAIL==='false'){
return
}
console.log('sending license key to email:'+email)
const msg = {
to: email,
from: SYSTEM_MAIL,
subject: 'Responsively - License Key',
html: 'Please find your license key - <strong>'+licenseKey+'</strong>',
};
await sgMail.send(msg);
}
function validateEmailExistence(email){
return emailUtils.validateEmail(email)
/**
* to do check email existence, mx lookup. figure out 10min email workarounds
*/
}
module.exports={
sendLicenseKeyMail,
validateEmailExistence
}

View file

@ -0,0 +1,51 @@
const Plan=require('../model/plan.model')
const mongoose=require('mongoose')
async function getPlan(planId){
console.log('fetching from db - planId:'+planId)
const plan=await Plan.findOne({_id: mongoose.Types.ObjectId(planId)}).exec()
console.log('plan:'+plan)
return plan
}
async function getPlanByName(planName){
console.log('fetching from db - planName:'+planName)
const plan=await Plan.findOne({name: planName}).exec()
console.log('plan:'+plan)
return plan
}
async function checkIfPlanStillValidForDate(startDate, planId){
let plan=await getPlan(planId)
return validator(startDate,plan)
return false
}
function validator(startDate, plan){
if(plan && plan.duration==='MONTH'){
console.log('calculating validity of month plan:'+plan+',startTime:'+startDate)
let endDate=new Date(startDate)
endDate.setMonth(endDate.getMonth()+1)
if(endDate.getTime()>=new Date().getTime()){
return true
}
}
return false
}
async function getCurrentUserLimitForPlan(startDate,planId){
let plan=await getPlan(planId)
let valid=validator(startDate,plan)
if(valid){
return plan.user_limit
}
return 0
}
module.exports={
getPlan,
getPlanByName,
checkIfPlanStillValidForDate,
getCurrentUserLimitForPlan
}

View file

@ -0,0 +1,114 @@
const User=require('../model/user.model')
const Subscription=require('../model/subscription.model')
const userService=require('../service/user.service')
const planService=require('../service/plan.service')
const constants=require('../constants/constants')
const emailService=require('../service/email.service')
const InvalidLicenseError=require('../exception/invalid-license-error.exception')
const UserExistsError=require('../exception/user-exists-error.exception')
const mongoose=require('mongoose')
async function createUserAndActivateTrial(email){
try{
let trialPlan=await planService.getPlanByName(constants.TRIAL_PLAN)
if(await userService.checkIfUserExists(email)){
throw new UserExistsError('User exists')
}
let user=await userService.createUser(email)
let subscription=new Subscription({
user_id: user._id,
plan_id: trialPlan._id,
start_date: new Date(),
status: constants.SUBSCRIPTION_STATUS.ACTIVE.id,
})
await insertSubscription(subscription)
await emailService.sendLicenseKeyMail(email,user.license_key)
}catch(err){
console.log(err)
throw err
}
}
async function insertSubscription(subscription){
subscription.c_ts=new Date()
subscription.u_ts=new Date()
try{
await subscription.save()
}catch(err){
console.log('error saving subscription'+subscription);
throw err
}
}
async function getSubscriptionByLicenseKey(licenseKey){
try{
if(!licenseKey){
throw new InvalidLicenseError('Invalid License :'+licenseKey)
}
let user=await User.findOne({license_key: licenseKey}).exec()
if(!user){
throw new InvalidLicenseError('Invalid License:'+licenseKey)
}
let subscription= await Subscription.findOne({user_id: user._id,status: constants.SUBSCRIPTION_STATUS.ACTIVE.id}).exec()
return subscription
}catch(err){
console.log(err)
throw err
}
}
async function validateLicenseKey(licenseKey){
try{
let subscription= await getSubscriptionByLicenseKey(licenseKey)
let licenseValid=await planService.checkIfPlanStillValidForDate(subscription.start_date,subscription.plan_id)
if(licenseValid){
console.log('license active!');
return true
}
console.log('license expired!');
return false
}catch(err){
console.log(err)
throw err
}
}
async function constructLicenseValidationResponse(licenseKey){
let responseBody={}
try{
if(!licenseKey){
throw new InvalidLicenseError('licenseKey is empty')
}
responseBody.status=await validateLicenseKey(licenseKey)
if(responseBody.status){
responseBody.statusCode=200
responseBody.message='success'
}else{
responseBody.statusCode=403
responseBody.message='license expired'
}
}catch(err){
console.log(err)
responseBody.status=false
if(err instanceof InvalidLicenseError){
responseBody.statusCode=403
responseBody.message='Invalid License'
}else{
responseBody.statusCode=500
responseBody.message='Internal Server Error'
}
}
return responseBody
}
module.exports={
createUserAndActivateTrial,
validateLicenseKey,
constructLicenseValidationResponse,
getSubscriptionByLicenseKey
}

View file

@ -0,0 +1,65 @@
const User=require('../model/user.model')
const Plan=require('../model/plan.model')
const planService=require('../service/plan.service')
const constants=require('../constants/constants')
const crypto=require('crypto')
const emailService=require('../service/email.service')
const InvalidEmailError=require('../exception/invalid-email-error.exception')
const UserExistsError=require('../exception/user-exists-error.exception')
const mongoose=require('mongoose')
async function createUser(email){
if(!emailService.validateEmailExistence(email)){
throw new InvalidEmailError('Invalid Email:'+email)
}
let newUser=new User({
email: email,
license_key:getLicenseKey()
})
await insertUser(newUser)
return newUser
}
async function insertUser(newUser){
newUser.c_ts=new Date()
newUser.u_ts=new Date()
try{
await newUser.save()
}catch(err){
console.log(err)
if(err.name === 'MongoError' && err.code === 11000){
throw new UserExistsError('user already exists with email:'+newUser.email)
}
throw err
}
console.log('userInserted')
return newUser
}
async function getUserByEmail(email){
return await User.findOne({email: email})
}
async function checkIfUserExists(email){
let user=await getUserByEmail(email)
console.log(user)
if(user){
return true
}
return false
}
function getLicenseKey(email){
const key=crypto.randomBytes(20).toString('hex');
return key;
}
module.exports={
createUser,
getUserByEmail,
checkIfUserExists
}

View file

@ -0,0 +1,8 @@
function validateEmail(email) {
var emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return emailRegex.test(String(email).toLowerCase());
}
module.exports={
validateEmail
}

12
server/src/db/db.js Normal file
View file

@ -0,0 +1,12 @@
const mongoose = require('mongoose');
const MONGO_USERNAME = process.env.MONGO_USERNAME
const MONGO_PASSWORD = process.env.MONGO_PASSWORD
const MONGO_HOSTNAME = process.env.MONGO_HOSTNAME
const MONGO_PORT = process.env.MONGO_PORT
const MONGO_DB = process.env.MONGO_DB
const url = `mongodb+srv://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}/${MONGO_DB}`
console.log('connecting to db');
mongoose.connect(url,{ useNewUrlParser: true }, function(error){
if(error){console.log(error)}
else{console.log("connection successful")}
})

57
server/src/http/lambda.js Normal file
View file

@ -0,0 +1,57 @@
const db=require('../db/db')
const subscriptionService = require('../app/service/subscription.service')
const UserExistsError=require('../app/exception/user-exists-error.exception')
const InvalidLicenseError=require('../app/exception/invalid-license-error.exception')
const InvalidEmailError=require('../app/exception/invalid-email-error.exception')
export async function activateTrial(event, context, callback) {
let responseBody={}
let statusCode=0
context.callbackWaitsForEmptyEventLoop = false;
try{
const {email}=event.queryStringParameters
if(!email){
throw new InvalidEmailError('email is empty')
}
await subscriptionService.createUserAndActivateTrial(email)
responseBody.status=true
responseBody.statusCode=200;
responseBody.message='success'
statusCode=200
}catch(err){
console.log(err)
responseBody.status=false
if(err instanceof UserExistsError){
responseBody.statusCode=409
responseBody.message='user already activated'
}else if(err instanceof InvalidEmailError){
responseBody.statusCode=400
responseBody.message='invalid email'
}else{
responseBody.statusCode=500
responseBody.message='Internal Server Error'
}
}
let response= {
statusCode: 200,
body: JSON.stringify(responseBody)
}
callback(null, response)
}
export async function validateLicense(event, context, callback) {
let responseBody={}
let statusCode=0
context.callbackWaitsForEmptyEventLoop = false;
try{
const {licenseKey}=event.queryStringParameters
responseBody=await subscriptionService.constructLicenseValidationResponse(licenseKey)
}catch(err){
console.log(err)
}
let response= {
statusCode: 200,
body: JSON.stringify(responseBody)
}
callback(null, response)
}

View file

@ -1,14 +1,61 @@
var db=require('../db/db')
const userService = require('../app/service/user.service')
const activeConnectionService = require('../app/service/active-connections.service')
const subscriptionService = require('../app/service/subscription.service')
const InvalidLicenseError=require('../app/exception/invalid-license-error.exception')
const InvalidSubscriptionError=require('../app/exception/invalid-subscription.exception')
export async function connectionHandler(event, context, callback) {
let responseBody={}
const connectionId=event.requestContext.connectionId
try{
console.log(event)
if(event.requestContext.eventType==='CONNECT'){
const {licenseKey}=event.queryStringParameters
await activeConnectionService.addNewConnection(licenseKey, connectionId)
responseBody.status=true
responseBody.statusCode=200
responseBody.message='connection established'
}else if(event.requestContext.eventType==='DISCONNECT'){
await activeConnectionService.closeConnection(connectionId)
}
}catch(err){
console.log(err)
responseBody.status=false
if(err instanceof InvalidSubscriptionError){
responseBody.statusCode=403
responseBody.message='invalid subscription'
}else if(err instanceof InvalidLicenseError){
responseBody.statusCode=403
responseBody.message='invalid license'
}else{
responseBody.statusCode=500
responseBody.message='server error'
}
}
return {
statusCode: 200,
body: 'Message recieved',
body: JSON.stringify(responseBody),
};
}
export async function defaultHandler(event, context, callback) {
try{
const body=JSON.parse(event.body)
const connectionId=event.requestContext.connectionId
if(!body.data || !body.data.licenseKey){
throw new InvalidLicenseError('license Key is empty')
}
const licenseKey=body.data.licenseKey
activeConnectionService.closeConnection(licenseKey, connectionId)
}catch(err){
console.log(err)
}
return {
statusCode: 200,
body: 'Message recieved',
body: 'Connection closed',
};
}
@ -18,3 +65,30 @@ export async function pingHandler(event, context, callback) {
body: 'Message recieved',
};
}
export async function validateLicense(event, context, callback) {
let responseBody={}
let statusCode=0
context.callbackWaitsForEmptyEventLoop = false;
const connectionId=event.requestContext.connectionId
console.log(event)
try{
const body=JSON.parse(event.body)
if(!body['data'] || !body['data']['licenseKey']){
throw new InvalidLicenseError('licenseKey is empty')
}
const licenseKey=body['data']['licenseKey']
responseBody=await subscriptionService.constructLicenseValidationResponse(licenseKey)
}catch(err){
console.log(err)
responseBody.status=false
responseBody.statusCode=500
responseBody.message='internal server error'
}
await activeConnectionService.publish(event,connectionId,responseBody)
let response= {
statusCode: 200,
body: JSON.stringify(responseBody)
}
callback(null, response)
}