16 changed files with 423 additions and 43 deletions
@ -0,0 +1,165 @@ |
|||||
|
// link.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const DTP_COMPONENT_NAME = 'link'; |
||||
|
|
||||
|
const express = require('express'); |
||||
|
const multer = require('multer'); |
||||
|
|
||||
|
const { SiteController } = require('../../lib/site-lib'); |
||||
|
|
||||
|
class LinkController extends SiteController { |
||||
|
|
||||
|
constructor (dtp) { |
||||
|
super(dtp, DTP_COMPONENT_NAME); |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
const { dtp } = this; |
||||
|
const { limiter: limiterService } = dtp.services; |
||||
|
|
||||
|
const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${DTP_COMPONENT_NAME}` }); |
||||
|
|
||||
|
const router = express.Router(); |
||||
|
dtp.app.use('/link', router); |
||||
|
|
||||
|
router.param('linkId', this.populateLinkId.bind(this)); |
||||
|
|
||||
|
router.use(async (req, res, next) => { |
||||
|
res.locals.currentView = DTP_COMPONENT_NAME; |
||||
|
return next(); |
||||
|
}); |
||||
|
|
||||
|
router.post('/visit/:linkId', |
||||
|
limiterService.create(limiterService.config.link.postCreateLinkVisit), |
||||
|
this.postCreateLinkVisit.bind(this), |
||||
|
); |
||||
|
|
||||
|
router.post('/sort', |
||||
|
limiterService.create(limiterService.config.link.postSortLinksList), |
||||
|
this.postSortLinksList.bind(this), |
||||
|
); |
||||
|
|
||||
|
router.post('/:linkId', |
||||
|
limiterService.create(limiterService.config.link.postUpdateLink), |
||||
|
upload.none(), |
||||
|
this.postUpdateLink.bind(this), |
||||
|
); |
||||
|
|
||||
|
router.post('/', this.postCreateLink.bind(this)); |
||||
|
|
||||
|
router.delete('/:linkId', this.deleteLink.bind(this)); |
||||
|
} |
||||
|
|
||||
|
async populateLinkId (req, res, next, linkId) { |
||||
|
const { link: linkService } = this.dtp.services; |
||||
|
try { |
||||
|
res.locals.link = await linkService.getById(linkId); |
||||
|
return next(); |
||||
|
} catch (error) { |
||||
|
this.log.error('failed to populate link ID', { linkId, error }); |
||||
|
return next(error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async postCreateLinkVisit (req, res, next) { |
||||
|
const { link: linkService, resource: resourceService } = this.dtp.services; |
||||
|
try { |
||||
|
/* |
||||
|
* Do these jobs in parallel so the total work gets done faster |
||||
|
*/ |
||||
|
const jobs = [ |
||||
|
linkService.recordVisit(res.locals.link, req), |
||||
|
resourceService.recordView(req, 'Link', res.locals.link._id), |
||||
|
]; |
||||
|
await Promise.all(jobs); // we don't care about any specific results
|
||||
|
|
||||
|
res.redirect(res.locals.link.href); // off you go!
|
||||
|
} catch (error) { |
||||
|
this.log.error('failed to create link', { error }); |
||||
|
return next(error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async postSortLinksList (req, res) { |
||||
|
const { link: linkService, displayEngine: displayEngineService } = this.dtp.services; |
||||
|
try { |
||||
|
const displayList = displayEngineService.createDisplayList('sort-links-list'); |
||||
|
await linkService.setItemOrder(req.body.updateOps); |
||||
|
displayList.showNotification( |
||||
|
'List sort order updated', |
||||
|
'success', |
||||
|
'bottom-center', |
||||
|
2000, |
||||
|
); |
||||
|
res.status(200).json({ success: true, displayList }); |
||||
|
} catch (error) { |
||||
|
this.log.error('failed to create link', { error }); |
||||
|
return res.status(error.statusCode || 500).json({ |
||||
|
success: false, |
||||
|
message: error.message, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async postUpdateLink (req, res) { |
||||
|
const { link: linkService, displayEngine: displayEngineService } = this.dtp.services; |
||||
|
try { |
||||
|
const displayList = displayEngineService.createDisplayList('update-link'); |
||||
|
await linkService.update(res.locals.link, req.body); |
||||
|
displayList.showNotification( |
||||
|
'Link updated', |
||||
|
'success', |
||||
|
'bottom-center', |
||||
|
2000, |
||||
|
); |
||||
|
res.status(200).json({ success: true, displayList }); |
||||
|
} catch (error) { |
||||
|
this.log.error('failed to create link', { error }); |
||||
|
return res.status(error.statusCode || 500).json({ |
||||
|
success: false, |
||||
|
message: error.message, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async postCreateLink (req, res, next) { |
||||
|
const { link: linkService } = this.dtp.services; |
||||
|
try { |
||||
|
res.locals.link = await linkService.create(req.user, req.body); |
||||
|
res.redirect('/'); |
||||
|
} catch (error) { |
||||
|
this.log.error('failed to create link', { error }); |
||||
|
return next(error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async deleteLink (req, res, next) { |
||||
|
const { link: linkService, displayEngine: displayEngineService } = this.dtp.services; |
||||
|
try { |
||||
|
const displayList = displayEngineService.createDisplayList('update-link'); |
||||
|
|
||||
|
await linkService.remove(res.locals.link); |
||||
|
|
||||
|
displayList.removeElement(`li[data-link-id="${res.locals.link._id}"]`); |
||||
|
displayList.showNotification( |
||||
|
'Link removed', |
||||
|
'success', |
||||
|
'bottom-center', |
||||
|
2000, |
||||
|
); |
||||
|
res.status(200).json({ success: true, displayList }); |
||||
|
} catch (error) { |
||||
|
this.log.error('failed to remove link', { linkId: res.locals.link._id, error }); |
||||
|
return next(error); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = async (dtp) => { |
||||
|
let controller = new LinkController(dtp); |
||||
|
return controller; |
||||
|
}; |
@ -0,0 +1,13 @@ |
|||||
|
// geo-types.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
const Schema = mongoose.Schema; |
||||
|
|
||||
|
module.exports.GeoPoint = new Schema({ |
||||
|
type: { type: String, enum: ['String'], default: 'String', required: true }, |
||||
|
coordinates: { type: [Number], required: true }, |
||||
|
}); |
@ -0,0 +1,35 @@ |
|||||
|
// link-visit.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
const Schema = mongoose.Schema; |
||||
|
|
||||
|
const { GeoPoint } = require('./lib/geo-types'); |
||||
|
|
||||
|
const LinkVisitSchema = new Schema({ |
||||
|
created: { type: Date, required: true, default: Date.now, index: -1, expires: '7d' }, |
||||
|
link: { type: Schema.ObjectId, required: true, index: 1, ref: 'Link' }, |
||||
|
user: { type: Schema.ObjectId, index: 1, ref: 'User' }, |
||||
|
geoip: { |
||||
|
country: { type: String }, |
||||
|
region: { type: String }, |
||||
|
eu: { type: String }, |
||||
|
timezone: { type: String }, |
||||
|
city: { type: String }, |
||||
|
location: { type: GeoPoint }, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
LinkVisitSchema.index({ |
||||
|
user: 1, |
||||
|
}, { |
||||
|
partialFilterExpression: { |
||||
|
user: { $exists: true }, |
||||
|
}, |
||||
|
name: 'link_visits_for_user', |
||||
|
}); |
||||
|
|
||||
|
module.exports = mongoose.model('LinkVisit', LinkVisitSchema); |
Loading…
Reference in new issue