Skip to content

Commit

Permalink
Merge pull request #288 from AdExNetwork/analytics-auth
Browse files Browse the repository at this point in the history
Per-channel analytics should require auth, fixes #287
  • Loading branch information
Ivo Georgiev committed Jun 12, 2020
2 parents 47354f4 + f8ce2ad commit ec29925
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 17 deletions.
33 changes: 30 additions & 3 deletions routes/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ const notCached = fn => (req, res, next) =>
const validate = celebrate({ query: { ...schemas.eventTimeAggr, segmentByChannel: Joi.string() } })

// Global statistics
router.get('/', validate, redisCached(400, analytics))
// WARNING: redisCached can only be used on methods that are called w/o auth
router.get('/', validate, redisCached(500, analytics))
router.get('/for-publisher', validate, authRequired, notCached(analytics))
router.get('/for-advertiser', validate, authRequired, notCached(advertiserAnalytics))

// Advanced statistics
router.get('/advanced', validate, authRequired, notCached(advancedAnalytics))

// :id is channelId: needs to be named that way cause of channelIfExists
router.get('/:id', validate, channelIfExists, redisCached(600, analytics))
router.get('/:id', validate, channelAdvertiserIfOwns, notCached(advertiserChannelAnalytics))
router.get('/for-publisher/:id', validate, authRequired, channelIfExists, notCached(analytics))

const MAX_LIMIT = 500
Expand Down Expand Up @@ -137,6 +138,10 @@ async function advertiserAnalytics(req) {
return analytics(req, await getAdvertiserChannels(req), true)
}

async function advertiserChannelAnalytics(req) {
return analytics(req, [req.params.id], true)
}

async function advancedAnalytics(req) {
const evType = req.query.eventType
const publisher = toBalancesKey(req.session.uid)
Expand All @@ -156,10 +161,32 @@ function getAdvertiserChannels(req) {
return advChannels
}

function channelAdvertiserIfOwns(req, res, next) {
const channelsCol = db.getMongo().collection('channels')
if (!req.session) {
res.status(403).json(null)
return
}
const uid = req.session.uid
channelsCol
.countDocuments({ _id: req.params.id, creator: uid }, { limit: 1 })
.then(function(n) {
if (!n) {
res.status(403).json(null)
} else {
next()
}
})
.catch(next)
}

function redisCached(seconds, fn) {
return function(req, res, next) {
if (req.session) {
res.status(500).json(null)
return
}
const key = `CACHE:${req.originalUrl}`

redisGet(key)
.then(cached => {
if (cached) {
Expand Down
37 changes: 24 additions & 13 deletions test/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const {
getDummySig,
fetchPost,
getValidEthChannel,
randomAddress
randomAddress,
fetchWithAuth
} = require('./lib')
const cfg = require('../cfg')
const dummyVals = require('./prep-db/mongo')
Expand Down Expand Up @@ -458,9 +459,11 @@ tape('should record clicks', async function(t) {

await postEvsAsCreator(leaderUrl, channel.id, evs)
// Technically we don't need to tick, since the events should be reflected immediately
const analytics = await fetch(`${leaderUrl}/analytics/${channel.id}?eventType=CLICK`).then(r =>
r.json()
)
const analytics = await fetchWithAuth(
`${leaderUrl}/analytics/${channel.id}?eventType=CLICK`,
dummyVals.auth.creator,
{}
).then(r => r.json())
t.equal(analytics.aggr[0].value, num.toString(), 'proper number of CLICK events')

t.end()
Expand Down Expand Up @@ -497,8 +500,10 @@ tape('should record: correct payout with targetingRules, consistent with balance
await postEvsAsCreator(leaderUrl, channel.id, evs)

// Technically we don't need to tick, since the events should be reflected immediately
const analytics = await fetch(
`${leaderUrl}/analytics/${channel.id}?eventType=CLICK&metric=eventPayouts`
const analytics = await fetchWithAuth(
`${leaderUrl}/analytics/${channel.id}?eventType=CLICK&metric=eventPayouts`,
dummyVals.auth.creator,
{}
).then(r => r.json())

t.equal(analytics.aggr[0].value, (num * 2 + num).toString(), 'proper payout amount')
Expand Down Expand Up @@ -533,7 +538,7 @@ tape('analytics routes return correct values', async function(t) {
const sumValues = vals => vals.map(x => parseInt(x.value, 10)).reduce((a, b) => a + b, 0)
const urls = [
['', null, resp => sumValues(resp.aggr) >= 20],
[`/${channel.id}`, null, resp => sumValues(resp.aggr) === 20],
[`/${channel.id}`, dummyVals.auth.creator, resp => sumValues(resp.aggr) === 20],
['/for-publisher', dummyVals.auth.publisher, resp => sumValues(resp.aggr) >= 10],
['/for-advertiser', dummyVals.auth.creator, resp => sumValues(resp.aggr) >= 20],
[`/for-publisher/${channel.id}`, dummyVals.auth.publisher, resp => sumValues(resp.aggr) === 10],
Expand Down Expand Up @@ -604,8 +609,10 @@ tape('targetingRules: event to update them works', async function(t) {
const evs = genEvents(num, randomAddress(), 'IMPRESSION')
await postEvsAsCreator(leaderUrl, channel.id, evs, { 'cf-ipcountry': 'US' })
// Technically we don't need to tick, since the events should be reflected immediately
const analytics = await fetch(
`${leaderUrl}/analytics/${channel.id}?eventType=IMPRESSION&metric=eventPayouts`
const analytics = await fetchWithAuth(
`${leaderUrl}/analytics/${channel.id}?eventType=IMPRESSION&metric=eventPayouts`,
dummyVals.auth.creator,
{}
).then(r => r.json())
t.equal(analytics.aggr[0].value, (num * 30).toString(), 'proper payout amount')

Expand Down Expand Up @@ -641,8 +648,10 @@ tape('targetingRules: onlyShowIf is respected and returns a HTTP error code', as
t.equal(postOneResp.status, 469, 'returned proper HTTP response code')

// Technically we don't need to tick, since the events should be reflected immediately
const analytics = await fetch(
`${leaderUrl}/analytics/${channel.id}?eventType=IMPRESSION&metric=eventPayouts`
const analytics = await fetchWithAuth(
`${leaderUrl}/analytics/${channel.id}?eventType=IMPRESSION&metric=eventPayouts`,
dummyVals.auth.creator,
{}
).then(r => r.json())
t.equal(analytics.aggr[0].value, num.toString(), 'proper payout amount')

Expand Down Expand Up @@ -711,8 +720,10 @@ tape('targetingRules: multiple rules are applied, pricingBounds are honored', as
)

const getLastAnalytics = async (ev, metric) => {
const resp = await fetch(
`${leaderUrl}/analytics/${channel.id}?eventType=${ev}&metric=${metric}`
const resp = await fetchWithAuth(
`${leaderUrl}/analytics/${channel.id}?eventType=${ev}&metric=${metric}`,
dummyVals.auth.creator,
{}
)
const { aggr } = await resp.json()
return aggr[0] ? aggr[0].value : '0'
Expand Down
14 changes: 13 additions & 1 deletion test/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ function fetchPost(url, authToken, body, headers = {}) {
})
}

function fetchWithAuth(url, authToken, headers = {}) {
return fetch(url, {
method: 'GET',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
...headers
}
})
}

function postEvents(url, channelId, events, auth = dummyVals.auth.creator, headers = {}) {
// It is important to use creator auth, otherwise we'd hit rate limits
return fetchPost(`${url}/channel/${channelId}/events`, auth, { events }, headers)
Expand Down Expand Up @@ -114,5 +125,6 @@ module.exports = {
validUntil,
withdrawPeriodStart,
getValidEthChannel,
randomAddress
randomAddress,
fetchWithAuth
}

0 comments on commit ec29925

Please sign in to comment.