2021-12-29 06:52:17 -05:00
const express = require ( 'express' )
2022-01-06 04:09:35 -05:00
const nodemailer = require ( 'nodemailer' )
2021-12-29 12:05:25 -05:00
const bodyParser = require ( 'body-parser' )
2021-12-29 06:52:17 -05:00
const path = require ( 'path' )
2022-01-05 13:12:28 -05:00
const { URLSafeTimedSerializer } = require ( "@constemi/itsdangerjs" )
2021-12-29 06:52:17 -05:00
2021-12-29 06:35:51 -05:00
require ( 'dotenv' ) . config ( )
2021-12-29 09:15:21 -05:00
// set up debug
2021-12-29 06:35:51 -05:00
let DEBUG
if ( process . env . DEBUG || process . env . CROWDFUNDING _SITE _DEBUG ) {
DEBUG = true
} else {
DEBUG = false
}
if ( DEBUG ) console . log ( 'Starting website in debug mode' )
2022-01-05 13:12:28 -05:00
// set up secret key
let secretKey
if ( process . env . CROWDFUNDING _SITE _SECRET _KEY ) {
secretKey = process . env . CROWDFUNDING _SITE _SECRET _KEY
} else {
if ( DEBUG ) {
secretKey = 'NotReallyASecret'
console . warn ( "Secret key is not set! We're falling back to the default one which is INSECURE and NOT TO BE USED IN PRODUCTION" )
} else {
console . error ( 'No secret key is set. You cannot run this in production without setting a secret key because that would be very insecure. Sorry.' )
process . exit ( )
}
}
2022-01-06 04:09:35 -05:00
// set up nodemailer (if configured)
let mailer
let emailFrom
if (
! ! process . env . CROWDFUNDING _SITE _EMAIL _HOST &&
! ! process . env . CROWDFUNDING _SITE _EMAIL _USER &&
! ! process . env . CROWDFUNDING _SITE _EMAIL _PASSWORD
) {
mailer = nodemailer . createTransport ( {
host : process . env . CROWDFUNDING _SITE _EMAIL _HOST ,
port : process . env . CROWDFUNDING _SITE _EMAIL _PORT || 587 ,
secure : process . env . CROWDFUNDING _SITE _EMAIL _SECURE == 'false' ? false : true ,
auth : {
user : process . env . CROWDFUNDING _SITE _EMAIL _USER ,
pass : process . env . CROWDFUNDING _SITE _EMAIL _PASSWORD ,
}
} )
if ( ! ! process . env . CROWDFUNDING _SITE _EMAIL _FROM ) {
emailFrom = process . env . CROWDFUNDING _SITE _EMAIL _FROM
} else {
emailFrom = process . env . CROWDFUNDING _SITE _EMAIL _USER
console . warn ( ` No "From" address set! Using ${ emailFrom } as a default. ` )
}
} else {
console . log ( "Email has not been configured! Some features won't work properly until this is set up :/" )
}
async function sendMail ( message ) {
if ( DEBUG || ! mailer ) {
console . log ( ` \n \n -------------------------- BEGIN EMAIL -------------------------- \n ` )
console . log ( ` From: ${ message . from } \n To: ${ message . to } \n Subject: ${ message . subject || '(no subject)' } \n \n ${ message . text } ` )
console . log ( ` \n --------------------------- END EMAIL --------------------------- ` )
return
} else {
let receipt = await mailer . sendMail ( {
from : message . from || emailFrom ,
to : message . to ,
subject : message . subject ,
text : message . text ,
html : message . html ,
} )
return receipt
}
}
2021-12-29 09:15:21 -05:00
// get goal details
const goalPeople = Number ( process . env . CROWDFUNDING _SITE _GOAL _PEOPLE ) || 750
const goalRupees = Number ( process . env . CROWDFUNDING _SITE _GOAL _RUPEES ) || 500000
2021-12-29 06:35:51 -05:00
// set up knex (for database)
const knex = require ( 'knex' ) ( {
client : 'sqlite3' ,
connection : {
filename : './donors.sqlite'
} ,
useNullAsDefault : true
} )
2022-01-05 08:51:10 -05:00
// schema to save pledges
let pledgeSchema = function ( t ) {
t . increments ( 'id' ) . primary ( )
t . timestamp ( 'created_at' ) . defaultTo ( knex . fn . now ( ) ) . notNullable ( )
t . boolean ( 'was_robot' ) . defaultTo ( true )
t . integer ( 'amount' ) . notNullable ( )
t . boolean ( 'overseas' )
t . string ( 'name' , 128 )
t . boolean ( 'anonymous' )
t . string ( 'email' , 128 )
t . string ( 'phone' , 32 )
t . integer ( 'retry_times' ) . defaultTo ( 10 )
t . boolean ( 'get_newsletter' ) . defaultTo ( false )
t . text ( 'other_message' )
}
// make sure pledges table exists
2021-12-29 06:35:51 -05:00
knex . schema . hasTable ( 'pledges' ) . then ( function ( exists ) {
if ( ! exists ) {
2022-01-05 08:51:10 -05:00
if ( DEBUG ) console . debug ( 'No pledge table exists! Creating one now...' )
return knex . schema . createTable ( 'pledges' , pledgeSchema )
}
} )
// make sure unverified pledge table exists
knex . schema . hasTable ( 'unverified_pledges' ) . then ( function ( exists ) {
if ( ! exists ) {
if ( DEBUG ) console . debug ( 'No unverified pledge table exists! Creating one now...' )
return knex . schema . createTable ( 'unverified_pledges' , pledgeSchema )
2021-12-29 06:35:51 -05:00
}
} )
// set up bookshelf (for easy interface for database)
bookshelf = require ( 'bookshelf' ) ( knex )
const Pledge = bookshelf . model ( 'Pledge' , {
tableName : 'pledges' ,
} )
2022-01-05 08:51:10 -05:00
const UnverifiedPledge = bookshelf . model ( 'UnverifiedPledge' , {
tableName : 'unverified_pledges' ,
} )
2021-12-29 06:52:17 -05:00
// decide base url and port for app (can be configured)
const baseUrl = process . env . CROWDFUNDING _SITE _BASE _URL || '/'
const port = process . env . CROWDFUNDING _SITE _PORT || 5000
2021-12-29 09:15:21 -05:00
// set up twing
const { TwingEnvironment , TwingLoaderFilesystem } = require ( 'twing' )
2022-01-06 05:37:14 -05:00
let loader = new TwingLoaderFilesystem ( [
path . resolve ( _ _dirname , '..' , 'src' ) ,
] )
2021-12-29 09:15:21 -05:00
let twing = new TwingEnvironment ( loader )
2021-12-29 06:52:17 -05:00
// set up express
const app = express ( )
const router = express . Router ( )
app . use ( baseUrl , router )
2021-12-29 12:05:25 -05:00
router . use ( bodyParser . urlencoded ( {
extended : true ,
} ) )
2021-12-29 06:52:17 -05:00
// main views
2021-12-29 09:15:21 -05:00
router . get ( '/' , async ( req , res ) => {
2021-12-29 06:52:17 -05:00
if ( DEBUG ) console . debug ( 'Returning home page' )
2021-12-29 09:15:21 -05:00
// count people
2022-01-11 02:18:28 -05:00
let { total _people , total _rupees } = ( await knex ( 'pledges' )
. sum ( 'amount as total_rupees' )
. count ( '* as total_people' ) ) [ 0 ]
2021-12-29 09:15:21 -05:00
2022-01-06 05:37:14 -05:00
twing . render ( 'index.htm.twig' , {
2021-12-29 09:15:21 -05:00
'goal_rupees' : Number ( goalRupees ) . toLocaleString ( 'en-IN' ) ,
'goal_people' : Number ( goalPeople ) . toLocaleString ( 'en-IN' ) ,
'progress_rupees' : Number ( total _rupees ) . toLocaleString ( 'en-IN' ) ,
'progress_people' : Number ( total _people ) . toLocaleString ( 'en-IN' ) ,
'percent_rupees' : ` style="width: ${ total _rupees / goalRupees * 100 } %" ` ,
'percent_people' : ` style="width: ${ total _people / goalPeople * 100 } %" ` ,
} ) . then ( ( output ) => {
res . end ( output )
} )
2021-12-29 06:52:17 -05:00
} )
2022-01-05 08:40:01 -05:00
// function to validate pledges before saving
2022-01-05 08:51:10 -05:00
function validatePledge ( body , PledgeModel = Pledge ) {
2022-01-05 08:40:01 -05:00
// errors get saved here
2021-12-29 12:05:25 -05:00
let errors = [ ]
2022-01-05 09:07:21 -05:00
let was _robot = body . was _robot
if ( was _robot != 'no' ) {
2022-01-06 07:50:50 -05:00
errors . push ( 'Only humans are allowed to donate money. Robots are too digital 🤖' )
2021-12-29 12:05:25 -05:00
}
2022-01-05 08:40:01 -05:00
let amount = body . amount
2021-12-29 12:05:25 -05:00
if ( ! amount || amount == 'custom' ) {
2022-01-05 08:40:01 -05:00
amount = body [ 'amount-custom' ]
2021-12-29 12:05:25 -05:00
}
if ( ! amount || amount <= 0 ) {
errors . push ( 'Pledge amount too small. Please choose at least a rupee!' )
}
try {
amount = Number ( amount )
} catch ( err ) {
errors . push ( 'Invalid amount. Please choose a positive number!' )
}
2022-01-05 08:40:01 -05:00
let name = body . name
2021-12-29 12:05:25 -05:00
if ( name . length <= 0 ) {
errors . push ( 'What is your name? You can be anonymous to the world but at least we should know...' )
}
2022-01-05 08:40:01 -05:00
let anonymous = body . anonymous == 'on' ? true : false
2021-12-29 12:05:25 -05:00
2022-01-05 08:40:01 -05:00
let email = body . email
2021-12-29 12:05:25 -05:00
if ( email . length < 5 ) {
errors . push ( 'Please enter a valid email address' )
}
2022-01-05 08:40:01 -05:00
let phone = body . phone
2022-01-05 08:26:13 -05:00
2022-01-05 09:07:21 -05:00
let retry _times
2022-01-05 08:26:13 -05:00
try {
2022-01-05 09:07:21 -05:00
retry _times = body . retry _times
2022-01-05 08:26:13 -05:00
} catch ( err ) {
errors . push ( 'Invalid retry count. Please choose a positive number!' )
}
2022-01-05 09:07:21 -05:00
let get _newsletter = body . get _newsletter == 'yes' ? true : false
2022-01-05 08:40:01 -05:00
let overseas = body . overseas == 'yes' ? true : false
2022-01-05 09:07:21 -05:00
let other _message = body . other _message
2021-12-29 12:05:25 -05:00
2022-01-05 08:40:01 -05:00
// enter the info
2022-01-05 08:51:10 -05:00
let pledge = new PledgeModel ( ) // may be Pledge or UnverifiedPledge
2022-01-05 09:07:21 -05:00
pledge . set ( 'was_robot' , was _robot )
2021-12-29 12:05:25 -05:00
pledge . set ( 'amount' , amount )
2021-12-29 12:06:40 -05:00
pledge . set ( 'overseas' , overseas )
2021-12-29 12:05:25 -05:00
pledge . set ( 'name' , name )
pledge . set ( 'anonymous' , anonymous )
pledge . set ( 'email' , email )
pledge . set ( 'phone' , phone )
2022-01-05 09:07:21 -05:00
pledge . set ( 'retry_times' , retry _times )
pledge . set ( 'get_newsletter' , get _newsletter )
pledge . set ( 'other_message' , other _message )
2021-12-29 12:05:25 -05:00
2022-01-05 08:40:01 -05:00
// return it all!
return {
pledge : pledge ,
errors : errors ,
}
}
2021-12-29 12:05:25 -05:00
2022-01-05 08:40:01 -05:00
router . post ( '/pledge' , async ( req , res ) => {
if ( DEBUG ) console . debug ( 'New pledge:' , req . body )
2022-01-05 09:06:13 -05:00
// check that the right submit button was pressed
let submit = req . body . submit
if ( submit != 'Save Pledge' ) {
errors . push ( "This request seems to have been tampered with. Are you sure it wasn't you doing the tampering?" )
}
2022-01-05 08:40:01 -05:00
// process the pledge with our handy function
2022-01-05 08:51:10 -05:00
let { pledge , errors } = validatePledge ( req . body , UnverifiedPledge )
2022-01-05 08:40:01 -05:00
// fail if there were errors
if ( ! ! errors . length ) {
2022-01-06 07:50:50 -05:00
let output = await twing . render ( 'form-errors.htm.twig' , {
errors : errors ,
} )
2022-01-06 08:48:05 -05:00
res . send ( output )
2022-01-06 07:50:50 -05:00
return
2022-01-05 08:40:01 -05:00
}
// save if there weren't
if ( DEBUG ) console . debug ( ` Saving pledge: ${ JSON . stringify ( pledge ) } ` )
2021-12-29 12:05:25 -05:00
try {
await pledge . save ( )
} catch ( err ) {
2022-01-06 07:50:50 -05:00
let output = await twing . render ( 'error.htm.twig' , {
error : "Sorry, something went wrong while saving your pledge and we don't know what in was. This website is still in beta so we do have a glitches once in a while. Apologies and please try again...🙁" ,
} )
2022-01-06 08:48:05 -05:00
res . send ( output )
2022-01-06 07:50:50 -05:00
return
2021-12-29 12:05:25 -05:00
}
2022-01-05 13:12:28 -05:00
// check for existing pledge
let existingPledge
try {
existingPledge = await ( Pledge
. query ( {
where : {
2022-01-06 04:09:35 -05:00
'email' : pledge . get ( 'email' ) ,
'amount' : pledge . get ( 'amount' ) ,
2022-01-05 13:12:28 -05:00
} ,
} )
. orderBy ( 'created_at' , 'DESC' )
. fetch ( ) )
} catch ( e ) {
2022-01-06 04:09:35 -05:00
if ( e . message == 'EmptyResponse' && DEBUG ) {
2022-01-05 13:12:28 -05:00
console . debug ( 'No existing pledge' )
} else {
console . warn ( ` Weird error happened: ${ e } ` )
}
}
// generate verification link
let serialiser = URLSafeTimedSerializer ( secretKey , pledge . get ( 'email' ) )
let verificationLink = ` ${ req . protocol } :// ${ req . hostname } /verify?email= ${ encodeURIComponent ( pledge . get ( 'email' ) ) } &key= ${ encodeURIComponent ( serialiser . dumps ( pledge . get ( 'amount' ) ) ) } `
2022-01-06 04:09:35 -05:00
// send out the email, along with existing pledge deets
2022-01-05 13:12:28 -05:00
console . debug ( ` Verification link generated: ${ verificationLink } ` )
2022-01-06 04:09:35 -05:00
let text = ` Hi ${ pledge . get ( 'name' ) } ,
Thank you so much for your pledge of ₹ $ { Number ( pledge . get ( 'amount' ) ) . toLocaleString ( 'en-IN' ) } to Snipette !
To finish the pledge , please verify your email by clicking the link below :
$ { verificationLink }
If you have any questions , don ' t hesitate to reach out : you can drop a line
anytime to editors @ snipettemag . com .
Thanks ,
The Snipette Team `
2022-01-06 07:50:50 -05:00
// to make things snappier, we don't `await` for the sending to finish.
let receipt = sendMail ( {
2022-01-06 04:09:35 -05:00
to : req . body . email ,
subject : ` Finish ${ existingPledge ? 'updating' : 'saving' } your pledge to Snipette ` ,
text : text ,
} )
2022-01-06 07:50:50 -05:00
// return the success page
twing . render ( 'pledge.htm.twig' , {
'email' : pledge . get ( 'email' ) ,
} ) . then ( ( output ) => {
2022-01-06 08:48:05 -05:00
res . send ( output )
2022-01-06 07:50:50 -05:00
} )
2021-12-29 12:05:25 -05:00
} )
2022-01-05 13:12:28 -05:00
// save pledge after verification complete
router . get ( '/verify' , async ( req , res ) => {
if ( DEBUG ) console . debug ( 'Validating pledge:' , req . query )
// unpack verification link (unless it's expired)
let serialiser = URLSafeTimedSerializer ( secretKey , req . query . email )
let amount
try {
amount = serialiser . loads ( req . query . key , 300 ) // number in seconds
} catch ( e ) {
if ( e . name == 'SignatureExpired' ) {
2022-01-06 07:50:50 -05:00
let output = await twing . render ( 'error.htm.twig' , {
error : "Sorry, looks like your link has expired. Please go back and try pledging again. Hopefully you'll manage it quicker this time 😛" ,
} )
2022-01-06 08:48:05 -05:00
res . send ( output )
2022-01-05 13:12:28 -05:00
return
} else {
2022-01-06 07:50:50 -05:00
let output = await twing . render ( 'error.htm.twig' , {
error : "An unknown error occurred. Please generate a new link and try again. Sorry for the inconvenience 🤦" ,
} )
2022-01-06 08:48:05 -05:00
res . send ( output )
2022-01-05 13:12:28 -05:00
return
}
}
// check against database
let pledge
try {
pledge = await ( UnverifiedPledge
. query ( {
where : {
'email' : req . query . email ,
'amount' : amount ,
} ,
} )
. orderBy ( 'created_at' , 'DESC' )
. fetch ( ) )
} catch ( e ) {
if ( e . name == 'EmptyResponse' ) {
2022-01-06 07:50:50 -05:00
let output = await twing . render ( 'error.htm.twig' , {
error : "That pledge was not found in our records. Are you sure you made it? 🔎" ,
} )
2022-01-06 08:48:05 -05:00
res . send ( output )
2022-01-05 13:12:28 -05:00
return
} else {
2022-01-06 07:50:50 -05:00
let output = await twing . render ( 'error.htm.twig' , {
error : "An unknown error occurred. Please generate a new link and try again. Sorry for the inconvenience 🤦" ,
} )
2022-01-06 08:48:05 -05:00
res . send ( output )
2022-01-05 13:12:28 -05:00
return
}
}
// prepare our new pledge
let newPledge = new Pledge ( )
newPledge . set ( 'was_robot' , pledge . get ( 'was_robot' ) )
newPledge . set ( 'amount' , pledge . get ( 'amount' ) )
newPledge . set ( 'overseas' , pledge . get ( 'overseas' ) )
newPledge . set ( 'name' , pledge . get ( 'name' ) )
newPledge . set ( 'anonymous' , pledge . get ( 'anonymous' ) )
newPledge . set ( 'email' , pledge . get ( 'email' ) )
newPledge . set ( 'phone' , pledge . get ( 'phone' ) )
newPledge . set ( 'retry_times' , pledge . get ( 'retry_times' ) )
newPledge . set ( 'get_newsletter' , pledge . get ( 'get_newsletter' ) )
newPledge . set ( 'other_message' , pledge . get ( 'other_message' ) )
// destroy previous pledges by that user
try {
await ( Pledge
. query ( {
where : {
email : req . query . email ,
}
} )
. destroy ( ) )
} catch ( e ) {
if ( e . message != 'No Rows Deleted' ) throw e
}
// save the new pledge
await newPledge . save ( )
2022-01-06 04:09:35 -05:00
// send the confirmation email
let text = ` Hi ${ pledge . get ( 'name' ) } ,
Your pledge of ₹ $ { Number ( pledge . get ( 'amount' ) ) . toLocaleString ( 'en-IN' ) } to Snipette Analog has been saved !
Thank you so much for your support . Once we hit our goals , we will contact
you to make the final payment . Meanwhile , here are the details of your
pledge for reference :
Name : $ { pledge . get ( 'name' ) }
Pledge amount : $ { Number ( pledge . get ( 'amount' ) ) . toLocaleString ( 'en-IN' ) } $ { pledge . overseas ? ' (plus $25 for shipping)' : '' }
Email : $ { pledge . get ( 'email' ) }
Phone : $ { pledge . get ( 'phone' ) || 'not provided' }
Extra message :
$ { pledge . get ( 'other_message' ) || '[no message sent]' }
We will attempt to contact you $ { pledge . get ( 'retry_times' ) <= 1 ? 'only once' : String ( pledge . get ( 'retry_times' ) ) + ' times' } before giving up .
You can update your pledge by going to the crowdfunding homepage and saving
a pledge with the same email address as you used before .
If you have any questions , don ' t hesitate to reach out : you can drop a line
anytime to editors @ snipettemag . com .
Thanks ,
The Snipette Team `
2022-01-06 07:50:50 -05:00
// to make things snappier, we don't `await` for the sending to finish
let receipt = sendMail ( {
2022-01-06 04:09:35 -05:00
to : pledge . get ( 'email' ) ,
subject : ` Your pledge has been saved! ` ,
text : text ,
} )
// Send confirmation message
2022-01-06 07:50:50 -05:00
res . redirect ( '/thanks' )
} )
router . get ( '/thanks' , async ( req , res ) => {
let output = await twing . render ( 'thanks.htm.twig' )
2022-01-06 08:48:05 -05:00
res . send ( output )
2022-01-06 07:50:50 -05:00
return
2022-01-05 13:12:28 -05:00
} )
2022-01-06 05:37:14 -05:00
router . use ( express . static ( 'src/assets' ) )
2021-12-29 12:05:25 -05:00
2021-12-29 06:52:17 -05:00
// start the listener!
app . listen ( port , ( ) => {
console . log ( ` Server is up at port ${ port } ` )
} )
// end note: in case we want to import this somewhere for testing
2021-12-29 06:35:51 -05:00
module . exports = {
knex ,
bookshelf ,
Pledge ,
2022-01-05 13:12:28 -05:00
UnverifiedPledge ,
2021-12-29 06:52:17 -05:00
router ,
2021-12-29 06:35:51 -05:00
}