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