accept PayPal IPNs and add them to the database

This commit is contained in:
Evert Prants 2017-08-27 20:47:52 +03:00
parent 0d04fb69cf
commit d083aaa346
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
7 changed files with 234 additions and 3 deletions

View File

@ -1,6 +1,7 @@
import path from 'path' import path from 'path'
import cprog from 'child_process' import cprog from 'child_process'
import config from '../../scripts/load-config' import config from '../../scripts/load-config'
import http from '../../scripts/http'
import models from './models' import models from './models'
import crypto from 'crypto' import crypto from 'crypto'
import notp from 'notp' import notp from 'notp'
@ -48,6 +49,8 @@ function keysAvailable (object, required) {
return found return found
} }
let txnStore = []
const API = { const API = {
Hash: (len) => { Hash: (len) => {
return crypto.randomBytes(len).toString('hex') return crypto.randomBytes(len).toString('hex')
@ -369,6 +372,92 @@ const API = {
return true return true
} }
} }
},
Payment: {
handleIPN: async function (body) {
let sandboxed = body.test_ipn === '1'
let url = 'https://ipnpb.' + (sandboxed ? 'sandbox.' : '') + 'paypal.com/cgi-bin/webscr'
console.debug('Incoming payment')
let verification = await http.POST(url, {}, Object.assign({
cmd: '_notify-validate'
}, body))
if (verification !== 'VERIFIED') return null
if (sandboxed) {
console.debug('Sandboxed payment:', body)
} else {
console.debug('IPN Verified Notification:', body)
}
// TODO: add database field for this
if (body.txn_id) {
if (txnStore.indexOf(body.txn_id) !== -1) return true
txnStore.push(body.txn_id)
}
let user
let source = []
if (sandboxed) {
source.push('virtual')
}
// TODO: add hooks
let custom = body.custom.split(',')
for (let i in custom) {
let str = custom[i]
if (str.indexOf('userid:') === 0) {
body.user_id = parseInt(str.split(':')[1])
} else if (str.indexOf('mcu:') === 0) {
source.push('mcu:' + str.split(':')[1])
}
}
if (body.user_id != null) {
user = await API.User.get(body.user_id)
} else if (body.payer_email != null) {
user = await API.User.get(body.payer_email)
}
let donation = {
user_id: user ? user.id : null,
amount: (body.mc_gross || body.payment_gross || 'Unknown') + ' ' + (body.mc_currency || 'EUR'),
source: source.join(';'),
note: body.memo || '',
read: 0,
created_at: new Date(body.payment_date)
}
console.log('Server receieved a successful PayPal IPN message.')
return models.Donation.query().insert(donation)
},
userContributions: async function (user) {
user = await API.User.ensureObject(user)
let dbq = await models.Donation.query().where('user_id', user.id)
let contribs = []
for (let i in dbq) {
let contrib = dbq[i]
let obj = {
amount: contrib.amount,
donated: contrib.created_at
}
let srcs = contrib.source.split(';')
for (let j in srcs) {
if (srcs[j].indexOf('mcu') === 0) {
obj.minecraft_username = srcs[j].split(':')[1]
}
}
contribs.push(obj)
}
return contribs
}
} }
} }

View File

@ -88,6 +88,10 @@ router.post('/external/facebook/callback', wrap(async (req, res, next) => {
let response = await APIExtern.Facebook.callback(req.session.user, sane) let response = await APIExtern.Facebook.callback(req.session.user, sane)
if (response.banned) {
return res.render('user/banned', {bans: response.banned, ipban: response.ip})
}
if (response.error) { if (response.error) {
return JsonData(req, res, response.error) return JsonData(req, res, response.error)
} }
@ -153,6 +157,10 @@ router.get('/external/twitter/callback', wrap(async (req, res) => {
} }
let response = await APIExtern.Twitter.callback(req.session.user, accessTokens, req.realIP) let response = await APIExtern.Twitter.callback(req.session.user, accessTokens, req.realIP)
if (response.banned) {
return res.render('user/banned', {bans: response.banned, ipban: response.ip})
}
if (response.error) { if (response.error) {
req.flash('message', {error: true, text: response.error}) req.flash('message', {error: true, text: response.error})
return res.redirect(uri) return res.redirect(uri)
@ -223,6 +231,10 @@ router.get('/external/discord/callback', wrap(async (req, res) => {
} }
let response = await APIExtern.Discord.callback(req.session.user, accessToken.accessToken, req.realIP) let response = await APIExtern.Discord.callback(req.session.user, accessToken.accessToken, req.realIP)
if (response.banned) {
return res.render('user/banned', {bans: response.banned, ipban: response.ip})
}
if (response.error) { if (response.error) {
req.flash('message', {error: true, text: response.error}) req.flash('message', {error: true, text: response.error})
return res.redirect(uri) return res.redirect(uri)
@ -399,6 +411,28 @@ router.post('/oauth2/authorized-clients/revoke', wrap(async (req, res, next) =>
res.status(204).end() res.status(204).end()
})) }))
/* ==================
* Donation Store
* ==================
*/
router.post('/paypal/ipn', wrap(async (req, res) => {
let content = req.body
if (content && content.payment_status && content.payment_status === 'Completed') {
await API.Payment.handleIPN(content)
}
res.status(204).end()
}))
router.get('/user/donations', wrap(async (req, res, next) => {
if (!req.session.user) return next()
let contribs = await API.Payment.userContributions(req.session.user)
res.jsonp(contribs)
}))
// 404 // 404
router.use((req, res) => { router.use((req, res) => {
res.status(404).jsonp({error: 'Not found'}) res.status(404).jsonp({error: 'Not found'})

View File

@ -43,6 +43,8 @@ router.use(wrap(async (req, res, next) => {
messages = messages[0] messages = messages[0]
} }
res.locals.message = messages
// Update user session every 30 minutes // Update user session every 30 minutes
if (req.session.user) { if (req.session.user) {
if (!req.session.user.session_refresh) { if (!req.session.user.session_refresh) {
@ -50,12 +52,20 @@ router.use(wrap(async (req, res, next) => {
} }
if (req.session.user.session_refresh < Date.now()) { if (req.session.user.session_refresh < Date.now()) {
// Check for ban
let banStatus = await API.User.getBanStatus(req.session.user.id)
if (banStatus.length) {
delete req.session.user
return next()
}
// Update user session
let udata = await API.User.get(req.session.user.id) let udata = await API.User.get(req.session.user.id)
setSession(req, udata) setSession(req, udata)
} }
} }
res.locals.message = messages
next() next()
})) }))
@ -207,6 +217,11 @@ router.get('/user/manage/email', ensureLogin, wrap(async (req, res) => {
res.render('user/email_change', {email: obfuscated, password: socialStatus.password}) res.render('user/email_change', {email: obfuscated, password: socialStatus.password})
})) }))
router.get('/donate', wrap(async (req, res, next) => {
if (!config.donations || !config.donations.business) return next()
res.render('donate', config.donations)
}))
/* /*
================= =================
POST HANDLING POST HANDLING
@ -339,6 +354,12 @@ router.post('/login', wrap(async (req, res, next) => {
if (user.activated === 0) return formError(req, res, 'Please activate your account first.') if (user.activated === 0) return formError(req, res, 'Please activate your account first.')
if (user.locked === 1) return formError(req, res, 'This account has been locked.') if (user.locked === 1) return formError(req, res, 'This account has been locked.')
// Check if the user is banned
let banStatus = await API.User.getBanStatus(user.id)
if (banStatus.length) {
return res.render('user/banned', {bans: banStatus, ipban: false})
}
// Redirect to the verification dialog if 2FA is enabled // Redirect to the verification dialog if 2FA is enabled
let totpRequired = await API.User.Login.totpTokenRequired(user) let totpRequired = await API.User.Login.totpTokenRequired(user)
if (totpRequired) { if (totpRequired) {
@ -346,8 +367,6 @@ router.post('/login', wrap(async (req, res, next) => {
return res.redirect('/login/verify') return res.redirect('/login/verify')
} }
// TODO: Ban checks
// Set session // Set session
setSession(req, user) setSession(req, user)

View File

@ -212,6 +212,33 @@ $(document).ready(function () {
loadAuthorizations() loadAuthorizations()
} }
if ($('#mcinclude').length) {
var customDef = $('#custominfo').val()
$('#mcinclude').change(function () {
$('.mcuname').slideToggle()
if (!this.checked) {
$('#custominfo').val(customDef)
} else {
if ($('#mcusername').val()) {
var mcname = 'mcu:' + $('#mcusername').val()
$('#custominfo').val(customDef ? customDef + ',' + mcname : mcname)
}
}
})
$('#mcusername').on('keyup', function () {
var mcname = 'mcu:' + $(this).val()
if ($(this).val() === '') {
$('#custominfo').val(customDef)
return
}
$('#custominfo').val(customDef ? customDef + ',' + mcname : mcname)
})
}
if ($('#newAvatar').length) { if ($('#newAvatar').length) {
$('#newAvatar').click(function (e) { $('#newAvatar').click(function (e) {
e.preventDefault() e.preventDefault()

View File

@ -255,9 +255,15 @@ input:not([type="submit"])
min-height: 380px min-height: 380px
&#totpcheck &#totpcheck
max-width: 400px max-width: 400px
&#donate
max-width: 400px
&#error &#error
width: 200px width: 200px
.check label
display: inline-block
margin-right: 5px
.pgn .pgn
display: inline-block display: inline-block
margin-left: 10px margin-left: 10px
@ -574,6 +580,15 @@ span.load
color: #fff color: #fff
background-color: #d0d0d0 background-color: #d0d0d0
select
width: 80px
background-color: #f5f5f5
border: 1px solid #c1c1c1
padding: 8px
vertical-align: top
margin-left: 5px
border-radius: 5px
@media all and (max-width: 800px) @media all and (max-width: 800px)
.navigator .navigator
padding: 0 10px padding: 0 10px

45
views/donate.pug Normal file
View File

@ -0,0 +1,45 @@
extends layout.pug
block title
|Icy Network - Donate
block body
.wrapper
.boxcont
.box#donate
h1 Donate
p Donating any amount would be highly appreciated!
- var formurl = "https://www.paypal.com/cgi-bin/webscr"
if sandbox
- formurl = "https://www.sandbox.paypal.com/cgi-bin/webscr"
form(action=formurl, method="post")
input(type="hidden" name="cmd" value="_xclick")
input(type="hidden" name="business" value=business)
input(type="hidden" name="item_name" value=name)
input(type="hidden" name="item_number" value="1")
input(type="hidden" name="no_shipping" value="1")
input(type="hidden" name="quantity" value="1")
input(type="hidden" name="tax" value="0")
input(type="hidden" name="notify_url" value=ipn_url)
label(for="amount") Amount
input(type="number", name="amount" value="1.00")
select(name="currency_code")
option(value="EUR") EUR
option(value="USD") USD
if user
input#custominfo(type="hidden", name="custom", value="userid:" + user.id)
else
input#custominfo(type="hidden", name="custom", value="")
if minecraft
.check
label(for="mcinclude") Include Minecraft Username
input(id="mcinclude" type="checkbox")
.mcuname(style="display: none;")
input(id="mcusername")
.buttoncont
a.button.donate(name="submit", onclick="$(this).closest('form').submit()")
i.fa.fa-fw.fa-paypal
|Donate
br
b Currently you can only donate using a PayPal account.

View File

@ -84,3 +84,5 @@ html
a(href="/docs/terms-of-service") Terms of Service a(href="/docs/terms-of-service") Terms of Service
span.divider | span.divider |
a(href="/docs/privacy-policy") Privacy Policy a(href="/docs/privacy-policy") Privacy Policy
span.divider |
a(href="/donate") Donate