From 61c631dd37417d7381481a2d14ea6acbad7c201d Mon Sep 17 00:00:00 2001 From: Erik Christensen Date: Thu, 7 Nov 2013 18:45:57 -0800 Subject: [PATCH 1/2] Initial implementation of evidence hosting --- models/badge-instance.js | 12 ++++++++++++ models/badge.js | 16 +++++++++------- routes/api.js | 8 ++++++-- routes/badge.js | 35 ++++++++++++++++++++++++++++++++++- routes/index.js | 5 +++++ routes/render.js | 23 +++++++++++++++++++++++ views/public/evidence.html | 21 +++++++++++++++++++++ 7 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 views/public/evidence.html diff --git a/models/badge-instance.js b/models/badge-instance.js index 715d7ba..61f9891 100644 --- a/models/badge-instance.js +++ b/models/badge-instance.js @@ -9,6 +9,17 @@ const regex = { email: /[a-z0-9!#$%&'*+\/=?\^_`{|}~\-]+(?:\.[a-z0-9!#$%&'*+\/=?\^_`{|}~\-]+)*@(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?/i }; +const EvidenceFileSchema = new Schema({ + path: { + type: String, + required: true + }, + mimeType: { + type: String, + required: true + } +}); + const BadgeInstanceSchema = new Schema({ _id: { type: String, @@ -36,6 +47,7 @@ const BadgeInstanceSchema = new Schema({ type: String, trim: true, }, + evidenceFiles: [EvidenceFileSchema], seen: { type: Boolean, required: true, diff --git a/models/badge.js b/models/badge.js index 47064f0..71f4476 100644 --- a/models/badge.js +++ b/models/badge.js @@ -548,10 +548,7 @@ Badge.prototype.redeemClaimCode = function redeemClaimCode(code, email, cb) { if (!claim.multi && claim.claimedBy && claim.claimedBy !== email) return cb(null, false); claim.claimedBy = email; - Badge.temporaryEvidence.destroy(claim, function(err) { - if (err) return cb(err); - cb(null, true); - }); + cb(null, true); }; Badge.prototype.removeClaimCode = function removeClaimCode(code, cb) { @@ -623,10 +620,12 @@ Badge.prototype.award = function award(options, callback) { const categories = this.categories; const weight = this.weight; const evidence = options.evidence; + const evidenceFiles = options.evidenceFiles; const instance = new BadgeInstance({ user: email, badge: this.id, evidence: evidence, + evidenceFiles: evidenceFiles }); // We don't want to fail with an error if the user already has the @@ -649,9 +648,12 @@ Badge.prototype.award = function award(options, callback) { }); }; -Badge.prototype.awardOrFind = function awardOrFind(email, callback) { - const query = { userBadgeKey: [email, this.id].join('.') }; - this.award(email, function (err, instance) { +Badge.prototype.awardOrFind = function awardOrFind(options, callback) { + if (typeof options === 'string') + options = {user: options}; + + const query = { userBadgeKey: [options.user, this.id].join('.') }; + this.award({ user: options.user, evidenceFiles: options.evidenceFiles }, function (err, instance) { if (!instance) { BadgeInstance.findOne(query, function (err, instance) { if (err) return callback(err); diff --git a/routes/api.js b/routes/api.js index 426a74c..0482366 100644 --- a/routes/api.js +++ b/routes/api.js @@ -373,6 +373,7 @@ function tryAwardingBadge(opts, res, successCb) { const badge = opts.badge; const email = opts.email; const evidence = opts.evidence; + const evidenceFiles = opts.evidenceFiles; if (!badge) return res.json(404, {status: 'error', reason: 'badge not found'}); @@ -381,7 +382,8 @@ function tryAwardingBadge(opts, res, successCb) { return badge.award({ email: email, - evidence: evidence + evidence: evidence, + evidenceFiles: evidenceFiles }, function (err, instance, autoAwardedInstances) { if (err) { // TODO: log error properly @@ -484,10 +486,12 @@ exports.awardBadgeFromClaimCode = function(req, res, next) { return res.json(400, {status: 'error', reason: 'missing email address'}); getUnclaimedBadgeFromCode(code, req, res, next, function(badge) { + var claim = badge.getClaimCode(code); tryAwardingBadge({ badge: badge, email: email, - evidence: evidence + evidence: evidence, + evidenceFiles: claim.evidence }, res, function(success) { async.series([ badge.redeemClaimCode.bind(badge, code, email), diff --git a/routes/badge.js b/routes/badge.js index 26a1adf..3cd9b7f 100644 --- a/routes/badge.js +++ b/routes/badge.js @@ -6,6 +6,8 @@ const BadgeInstance = require('../models/badge-instance'); const Work = require('../models/work'); const util = require('../lib/util'); const async = require('async'); +const s3 = require('../lib/s3'); +const mime = require('mime'); function handleTagInput(input) { return ( @@ -146,6 +148,36 @@ exports.assertion = function assertion(req, res) { }); }; +exports.getEvidenceFile = function getEvidenceFile(req, res) { + var assertionId = req.param('hash'); + var evidenceIndex = req.param('index'); + + BadgeInstance.findOne({ _id: assertionId }, function (err, instance) { + if (err) + return res.send(500, err); + if (!instance) + return res.send(404); + + if (evidenceIndex < 0 || evidenceIndex >= instance.evidenceFiles.length) + return res.send(404); + + var evidence = instance.evidenceFiles[evidenceIndex]; + + s3.get(evidence.path).on('response', function(s) { + if (err) return res.json(500, { + status: 'error', + reason: 'cannot retrieve evidence' + }); + res.type(evidence.mimeType); + var ext = mime.extension(evidence.mimeType); + var filename = 'evidence-' + evidenceIndex + (ext ? '.' + ext : ''); + res.set('Content-Disposition', + 'attachment; filename="' + filename + '"'); + s.pipe(res); + }).end(); + }); +}; + exports.meta = function meta(req, res) { req.badge.populate('program', function(err) { if (err) @@ -242,6 +274,7 @@ exports.awardToUser = function awardToUser(req, res, next) { var email = (form.email || '').trim(); var code = (form.code || '').trim(); var badge = req.badge; + var evidenceFiles = req.claim.evidence; badge.redeemClaimCode(code, email, function(err, claimSuccess) { if (err) return res.send(reportError(err)); @@ -251,7 +284,7 @@ exports.awardToUser = function awardToUser(req, res, next) { if (claimSuccess === null) return res.send({ status: 'not-found' }); - badge.awardOrFind(email, function (err, instance) { + badge.awardOrFind({ user: email, evidenceFiles: evidenceFiles }, function (err, instance) { if (err) return res.send(reportError(err)); badge.save(function (err) { if (err) return res.send(reportError(err)); diff --git a/routes/index.js b/routes/index.js index 2b85991..9a534ae 100644 --- a/routes/index.js +++ b/routes/index.js @@ -154,6 +154,11 @@ exports.define = function defineRoutes(app) { app.get('/badge/criteria/:shortname', [ findBadgeByParamShortname ], render.criteria); + app.get('/badge/evidence/:shortname', [ + findBadgeByParamShortname + ], render.evidence); + app.get('/badge/evidence/:hash/:index', badge.getEvidenceFile); + app.get('/program/meta/:programId', [ issuer.findProgramById ], issuer.meta); diff --git a/routes/render.js b/routes/render.js index cfa9891..c3c3e99 100644 --- a/routes/render.js +++ b/routes/render.js @@ -1,6 +1,7 @@ const Issuer = require('../models/issuer'); const Program = require('../models/program'); const Badge = require('../models/badge'); +const BadgeInstance = require('../models/badge-instance'); const phrases = require('../lib/phrases'); const logger = require('../lib/logger'); const async = require('async'); @@ -170,6 +171,28 @@ exports.criteria = function criteria(req, res) { }); } +exports.evidence = function evidence(req, res) { + var email = req.query.email; + var badge = req.badge; + + BadgeInstance.findOne({ badge: badge._id, user: email }, function (err, instance) { + if (err) + return res.send(500, err); + if (!instance) + return res.send(404); + instance.populate('badge', function(err) { + if (err) + return res.send(500, err); + + return res.render('public/evidence.html', { + instance: instance, + user: req.session.user, + csrf: req.session._csrf + }); + }); + }); +} + exports.anonymousHome = function all(req, res) { return res.render('public/anonymous-home.html', { user: req.session.user, diff --git a/views/public/evidence.html b/views/public/evidence.html new file mode 100644 index 0000000..e85a8c0 --- /dev/null +++ b/views/public/evidence.html @@ -0,0 +1,21 @@ +{% extends "public/layout.html" %} +{% block title %}{{ instance.badge.name }}{% endblock %} +{% block body %} +

{{ instance.badge.name }}

+ {% if instance.evidence or instance.evidenceFiles.length %} +
+
Evidence:
+
+

+ {{ instance.evidence }} +

+ + {% for file in instance.evidenceFiles %} +

Attachment {{ loop.index }}

+ {% endfor %} +
+
+ {% else %} +

No evidence found for this badge

+ {% endif %} +{% endblock %} From 1d7898e5689e595ce0c80e36ba13afd0124041fa Mon Sep 17 00:00:00 2001 From: Erik Christensen Date: Thu, 7 Nov 2013 18:52:48 -0800 Subject: [PATCH 2/2] Changed endpoint to use assertion id --- routes/index.js | 4 +--- routes/render.js | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/routes/index.js b/routes/index.js index 9a534ae..c17d7d5 100644 --- a/routes/index.js +++ b/routes/index.js @@ -154,9 +154,7 @@ exports.define = function defineRoutes(app) { app.get('/badge/criteria/:shortname', [ findBadgeByParamShortname ], render.criteria); - app.get('/badge/evidence/:shortname', [ - findBadgeByParamShortname - ], render.evidence); + app.get('/badge/evidence/:hash', render.evidence); app.get('/badge/evidence/:hash/:index', badge.getEvidenceFile); app.get('/program/meta/:programId', [ diff --git a/routes/render.js b/routes/render.js index c3c3e99..55c74a8 100644 --- a/routes/render.js +++ b/routes/render.js @@ -172,10 +172,9 @@ exports.criteria = function criteria(req, res) { } exports.evidence = function evidence(req, res) { - var email = req.query.email; - var badge = req.badge; + var assertionId = req.param('hash'); - BadgeInstance.findOne({ badge: badge._id, user: email }, function (err, instance) { + BadgeInstance.findOne({ _id: assertionId }, function (err, instance) { if (err) return res.send(500, err); if (!instance)