You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
218 lines
5.1 KiB
218 lines
5.1 KiB
// minio.js
|
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
// License: Apache-2.0
|
|
|
|
'use strict';
|
|
|
|
const path = require('path');
|
|
|
|
const mongoose = require('mongoose');
|
|
const pug = require('pug');
|
|
|
|
const Link = mongoose.model('Link');
|
|
const LinkVisit = mongoose.model('LinkVisit');
|
|
|
|
const geoip = require('geoip-lite');
|
|
const striptags = require('striptags');
|
|
|
|
const isUrlValid = require('url-validation');
|
|
|
|
const { SiteService, SiteError } = require('../../lib/site-lib');
|
|
|
|
class LinkService extends SiteService {
|
|
|
|
constructor (dtp) {
|
|
super(dtp, module.exports);
|
|
|
|
this.populateLink = [
|
|
{
|
|
path: 'user',
|
|
select: '_id username username_lc displayName picture',
|
|
},
|
|
];
|
|
}
|
|
|
|
async start ( ) {
|
|
this.templates = { };
|
|
|
|
let script = path.join(this.dtp.config.root, 'app', 'views', 'link', 'components', 'list-item-standalone.pug');
|
|
this.templates.link = pug.compileFile(script);
|
|
}
|
|
|
|
renderTemplate (templateId, viewModel) {
|
|
return this.templates[templateId](viewModel);
|
|
}
|
|
|
|
async create (user, linkDefinition) {
|
|
if (!user.permissions.canCreateLinks) {
|
|
throw new SiteError(403, 'You are prohibited from creating links');
|
|
}
|
|
this.validateUrl(linkDefinition.href);
|
|
|
|
const NOW = new Date();
|
|
const link = new Link();
|
|
|
|
link.created = NOW;
|
|
link.user = user._id;
|
|
link.label = striptags(linkDefinition.label.trim());
|
|
link.href = striptags(linkDefinition.href.trim());
|
|
|
|
await link.save();
|
|
return link.toObject();
|
|
}
|
|
|
|
async update (link, linkDefinition) {
|
|
this.validateUrl(linkDefinition.href);
|
|
|
|
const updateOp = { $set: { } };
|
|
|
|
if (linkDefinition.label) {
|
|
link.label = updateOp.$set.label = striptags(linkDefinition.label.trim());
|
|
}
|
|
if (linkDefinition.href) {
|
|
link.href = updateOp.$set.href = striptags(linkDefinition.href.trim());
|
|
}
|
|
|
|
await Link.updateOne({ _id: link._id }, updateOp);
|
|
}
|
|
|
|
async getById (linkId) {
|
|
const link = await Link
|
|
.findById(linkId)
|
|
.populate(this.populateLink)
|
|
.lean();
|
|
return link;
|
|
}
|
|
|
|
async getForUser (user, pagination) {
|
|
let links;
|
|
if (pagination) {
|
|
links = await Link
|
|
.find({ user: user._id })
|
|
.sort({ order: 1 })
|
|
.skip(pagination.skip)
|
|
.limit(pagination.cpp)
|
|
.lean();
|
|
} else {
|
|
links = await Link
|
|
.find({ user: user._id })
|
|
.sort({ order: 1 })
|
|
.lean();
|
|
}
|
|
return links;
|
|
}
|
|
|
|
async getRecent (maxCount = 3) {
|
|
const links = await Link
|
|
.find()
|
|
.sort({ created: -1 })
|
|
.limit(maxCount)
|
|
.lean();
|
|
return links;
|
|
}
|
|
|
|
async getPopular (maxCount) {
|
|
const links = await Link
|
|
.find()
|
|
.sort({ 'stats.uniqueVisitCount': -1 })
|
|
.limit(maxCount)
|
|
.lean();
|
|
return links;
|
|
}
|
|
|
|
async getAdmin (pagination) {
|
|
const links = await Link
|
|
.find()
|
|
.sort({ created: -1 })
|
|
.skip(pagination.skip)
|
|
.limit(pagination.cpp)
|
|
.populate([
|
|
{
|
|
path: 'user',
|
|
select: '_id username username_lc displayName picture',
|
|
},
|
|
])
|
|
.lean();
|
|
return links;
|
|
}
|
|
|
|
async getTotalCount ( ) {
|
|
return await Link.estimatedDocumentCount();
|
|
}
|
|
|
|
async remove (link) {
|
|
this.log.debug('removing link visit records', { link: link._id });
|
|
await LinkVisit.deleteMany({ link: link._id });
|
|
|
|
const { resource: resourceService } = this.dtp.services;
|
|
await resourceService.remove('Link', link._id);
|
|
}
|
|
|
|
async recordVisit (link, req) {
|
|
const NOW = new Date();
|
|
const visit = new LinkVisit();
|
|
|
|
visit.created = NOW;
|
|
visit.link = link._id;
|
|
|
|
if (req.user) {
|
|
visit.user = req.user._id;
|
|
}
|
|
|
|
/*
|
|
* We geo-analyze (but do not store) the IP address.
|
|
*/
|
|
const ipAddress = req.ip;
|
|
const geo = geoip.lookup(ipAddress);
|
|
if (geo) {
|
|
visit.geoip = {
|
|
country: geo.country,
|
|
region: geo.region,
|
|
eu: geo.eu,
|
|
timezone: geo.timezone,
|
|
city: geo.city,
|
|
};
|
|
if (Array.isArray(geo.ll) && (geo.ll.length === 2)) {
|
|
visit.geoip.location = {
|
|
type: 'Point',
|
|
coordinates: geo.ll,
|
|
};
|
|
}
|
|
}
|
|
|
|
await visit.save();
|
|
|
|
await Link.updateOne({ _id: link._id }, { $inc: { 'stats.totalVisitCount': 1 } });
|
|
}
|
|
|
|
async setItemOrder (links) {
|
|
for await (const link of links) {
|
|
this.log.debug('set link order', { link });
|
|
await Link.updateOne({ _id: link._id }, { $set: { order: link.order } });
|
|
}
|
|
}
|
|
|
|
validateUrl (href) {
|
|
if (!isUrlValid(href)) {
|
|
throw new SiteError(406, 'Invalid link URL');
|
|
}
|
|
|
|
const urlTest = href.toLowerCase().trim();
|
|
this.log.debug('testing URL for validity', { urlTest });
|
|
|
|
if (!urlTest.startsWith('https://') && !urlTest.startsWith('http://')) {
|
|
throw new SiteError(406, 'Invalid link URL');
|
|
}
|
|
|
|
if (urlTest.startsWith('https://libertylinks') || urlTest.startsWith('http://libertylinks') ||
|
|
urlTest.startsWith('https://www.libertylinks') || urlTest.startsWith('http://libertylinks')) {
|
|
throw new SiteError(406, 'Invalid link URL');
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
slug: 'link',
|
|
name: 'link',
|
|
create: (dtp) => { return new LinkService(dtp); },
|
|
};
|