diff --git a/server/api/index.js b/server/api/index.js index d8a4327..a4468c3 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -1,6 +1,7 @@ import path from 'path' import cprog from 'child_process' import config from '../../scripts/load-config' +import http from '../../scripts/http' import models from './models' import crypto from 'crypto' import notp from 'notp' @@ -48,6 +49,8 @@ function keysAvailable (object, required) { return found } +let txnStore = [] + const API = { Hash: (len) => { return crypto.randomBytes(len).toString('hex') @@ -369,6 +372,92 @@ const API = { 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 + } } } diff --git a/server/routes/api.js b/server/routes/api.js index cdc4e4a..7090782 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -88,6 +88,10 @@ router.post('/external/facebook/callback', wrap(async (req, res, next) => { 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) { 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) + if (response.banned) { + return res.render('user/banned', {bans: response.banned, ipban: response.ip}) + } + if (response.error) { req.flash('message', {error: true, text: response.error}) 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) + if (response.banned) { + return res.render('user/banned', {bans: response.banned, ipban: response.ip}) + } + if (response.error) { req.flash('message', {error: true, text: response.error}) return res.redirect(uri) @@ -399,6 +411,28 @@ router.post('/oauth2/authorized-clients/revoke', wrap(async (req, res, next) => 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 router.use((req, res) => { res.status(404).jsonp({error: 'Not found'}) diff --git a/server/routes/index.js b/server/routes/index.js index 06bb854..c328e76 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -43,6 +43,8 @@ router.use(wrap(async (req, res, next) => { messages = messages[0] } + res.locals.message = messages + // Update user session every 30 minutes if (req.session.user) { 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()) { + // 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) setSession(req, udata) } } - res.locals.message = messages 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}) })) +router.get('/donate', wrap(async (req, res, next) => { + if (!config.donations || !config.donations.business) return next() + res.render('donate', config.donations) +})) + /* ================= 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.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 let totpRequired = await API.User.Login.totpTokenRequired(user) if (totpRequired) { @@ -346,8 +367,6 @@ router.post('/login', wrap(async (req, res, next) => { return res.redirect('/login/verify') } - // TODO: Ban checks - // Set session setSession(req, user) diff --git a/src/script/main.js b/src/script/main.js index 3cc67c1..2d64655 100644 --- a/src/script/main.js +++ b/src/script/main.js @@ -212,6 +212,33 @@ $(document).ready(function () { 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) { $('#newAvatar').click(function (e) { e.preventDefault() diff --git a/src/style/main.styl b/src/style/main.styl index 8759560..0396e27 100644 --- a/src/style/main.styl +++ b/src/style/main.styl @@ -255,9 +255,15 @@ input:not([type="submit"]) min-height: 380px &#totpcheck max-width: 400px + &#donate + max-width: 400px &#error width: 200px +.check label + display: inline-block + margin-right: 5px + .pgn display: inline-block margin-left: 10px @@ -574,6 +580,15 @@ span.load color: #fff 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) .navigator padding: 0 10px diff --git a/views/donate.pug b/views/donate.pug new file mode 100644 index 0000000..c547c64 --- /dev/null +++ b/views/donate.pug @@ -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. diff --git a/views/layout.pug b/views/layout.pug index 91fc9f7..1eac9fb 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -84,3 +84,5 @@ html a(href="/docs/terms-of-service") Terms of Service span.divider | a(href="/docs/privacy-policy") Privacy Policy + span.divider | + a(href="/donate") Donate