meemo-app
Version:
A personal ideas, notes or links manager
488 lines (383 loc) • 18.2 kB
JavaScript
/* jslint node:true */
;
exports = module.exports = {
auth: auth,
login: login,
logout: logout,
profile: profile,
getAll: getAll,
get: get,
add: add,
put: put,
del: del,
getTags: getTags,
settingsSave: settingsSave,
settingsGet: settingsGet,
exportThings: exportThings,
importThings: importThings,
healthcheck: healthcheck,
fileAdd: fileAdd,
fileGet: fileGet,
public: {
users: publicUsers,
profile: publicProfile,
getAll: publicGetAll,
getThing: publicGetThing,
getFile: publicGetFile,
getRSS: publicGetRSS,
streamPage: publicStreamPage
}
};
var assert = require('assert'),
checksum = require('checksum'),
config = require('./config.js'),
fs = require('fs'),
path = require('path'),
logic = require('./logic.js'),
mkdirp = require('mkdirp'),
path = require('path'),
rss = require('rss'),
settings = require('./database/settings.js'),
tags = require('./database/tags.js'),
tar = require('tar-fs'),
tokens = require('./database/tokens.js'),
users = require('./users.js'),
UserError = users.UserError,
uuid = require('uuid'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function healthcheck(req, res, next) {
next(new HttpSuccess(200, {}));
}
function auth(req, res, next) {
// first try session
if (req.session.userId) {
req.userId = req.session.userId;
return next();
}
if (!req.query.token) return next(new HttpError(401, 'missing token'));
tokens.get(req.query.token, function (error, result) {
if (error) return next(new HttpError(401, 'invalid credentials'));
// make old versions relogin and invalidate token
if (!result.userId) {
next(new HttpError(401, 'old token'));
return tokens.del(req.query.token, function () {});
}
req.token = req.query.token;
req.userId = result.userId;
next();
});
}
function login(req, res, next) {
if (typeof req.body.username !== 'string' || !req.body.username) return next(new HttpError(400, 'missing username'));
if (typeof req.body.password !== 'string' || !req.body.password) return next(new HttpError(400, 'missing password'));
users.verify(req.body.username, req.body.password, function (error, result) {
if (error && error.code === UserError.NOT_FOUND) return next(new HttpError(401, 'invalid credentials'));
if (error && error.code === UserError.NOT_AUTHORIZED) return next(new HttpError(401, 'invalid credentials'));
if (error) return next(new HttpError(500, error));
req.session.userId = result.user.username;
var token = uuid.v4();
tokens.add(token, '', result.user.username, function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { token: token, user: result.user }));
});
});
}
function logout(req, res, next) {
delete req.session.userId;
if (!req.token) return next(new HttpSuccess(200, {}));
tokens.del(req.token, function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
function profile(req, res, next) {
users.profile(req.userId, false, function (error, result) {
if (error && error.code === UserError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
var out = {
user: result
};
next(new HttpSuccess(200, out));
});
}
function getAll(req, res, next) {
var query = { $or: [] };
if (req.query.filter) {
query.$or.push({
$text: { $search: String(req.query.filter) }
});
} else {
query.$or.push({ content: { $exists: true }});
}
if (req.query.sticky) {
query.$or.push({ sticky: true });
}
var archiveQuery;
if (req.query.archived) {
archiveQuery = {
archived: true
};
} else {
archiveQuery = { $or: [{
archived: false
}, {
archived: { $exists: false }
}]};
}
var endQuery = { $and: [ archiveQuery, query ]};
var skip = isNaN(parseInt(req.query.skip)) ? 0 : parseInt(req.query.skip);
var limit = isNaN(parseInt(req.query.limit)) ? 10 : parseInt(req.query.limit);
logic.getAll(req.userId, endQuery, skip, limit, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { things: result }));
});
}
function get(req, res, next) {
logic.get(req.userId, req.params.id, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { thing: result }));
});
}
function add(req, res, next) {
if (typeof req.body.content !== 'string' || !req.body.content) return next(new HttpError(400, 'content must be a string'));
if (req.body.attachments && !Array.isArray(req.body.attachments)) return next(new HttpError(400, 'attachments must be an array'));
if (!req.body.attachments) req.body.attachments = [];
logic.add(req.userId, req.body.content, req.body.attachments, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { thing: result }));
});
}
function put(req, res, next) {
if (typeof req.body.content !== 'string' || !req.body.content) return next(new HttpError(400, 'content must be a string'));
if (req.body.attachments && !Array.isArray(req.body.attachments)) return next(new HttpError(400, 'attachments must be an array'));
if (req.body.public && typeof req.body.public !== 'boolean') return next(new HttpError(400, 'public must be a boolean'));
if (req.body.shared && typeof req.body.shared !== 'boolean') return next(new HttpError(400, 'shared must be a boolean'));
if (req.body.archived && typeof req.body.archived !== 'boolean') return next(new HttpError(400, 'archived must be a boolean'));
if (req.body.sticky && typeof req.body.sticky !== 'boolean') return next(new HttpError(400, 'sticky must be a boolean'));
if (!req.body.attachments) req.body.attachments = [];
req.body.public = !!req.body.public;
req.body.shared = !!req.body.shared;
req.body.archived = !!req.body.archived;
req.body.sticky = !!req.body.sticky;
logic.put(req.userId, req.params.id, req.body.content, req.body.attachments, req.body.public, req.body.shared, req.body.archived, req.body.sticky, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { thing: result }));
});
}
function del(req, res, next) {
logic.del(req.userId, req.params.id, function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
function getTags(req, res, next) {
tags.get(req.userId, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { tags: result }));
});
}
function settingsSave(req, res, next) {
settings.put(req.userId, req.body.settings, function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
function settingsGet(req, res, next) {
settings.get(req.userId, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { settings: result }));
});
}
function exportThings(req, res, next) {
// Just to make sure the folder exists in case a user has never uploaded an attachment
var attachmentFolder = path.join(config.attachmentDir, req.userId);
mkdirp.sync(attachmentFolder);
logic.exp(req.userId, function (error, result) {
if (error) return next(new HttpError(500, error));
var out = tar.pack(attachmentFolder, {
map: function (header) {
header.name = 'attachments/' + header.name;
return header;
}
});
// add the db dump
out.entry({ name: 'things.json' }, JSON.stringify(result, null, 4));
res.attachment('meemo-export.tar');
out.pipe(res);
});
}
function importThings(req, res, next) {
if (!req.files || !req.files[0]) return next(new HttpError('400', 'missing file'));
logic.importThings(req.userId, req.files[0].path, function (error) {
if (error) return next(new HttpError(400, error));
next(new HttpSuccess(200, {}));
});
}
function fileAdd(req, res, next) {
if (!req.files || !req.files[0]) return next(new HttpError('400', 'missing file'));
var file = req.files[0];
var fileName = checksum(file.buffer) + path.extname(file.originalname);
var attachmentFolder = path.join(config.attachmentDir, req.userId);
// ensure the directory exists
mkdirp.sync(attachmentFolder);
fs.writeFile(path.join(attachmentFolder, fileName), file.buffer, function (error) {
if (error) return next(new HttpError(500, error));
var type = file.mimetype.indexOf('image/') === 0 ? logic.TYPE_IMAGE : logic.TYPE_UNKNOWN;
next(new HttpSuccess(201, { identifier: fileName, fileName: file.originalname, type: type }));
});
}
function fileGet(req, res, next) {
if (req.session.userId) return res.sendFile(req.params.identifier, { root: path.join(config.attachmentDir, req.session.userId) });
logic.getPublic(req.params.userId, req.params.thingId, function (error) {
if (error === 'not allowed') return next(new HttpError(403, 'not allowed'));
if (error) return next(new HttpError(500, error));
res.sendFile(req.params.identifier, { root: path.join(config.attachmentDir, req.params.userId) });
});
}
function publicGetThing(req, res, next) {
logic.getPublic(req.params.userId, req.params.thingId, function (error, result) {
if (error === 'not allowed') return next(new HttpError(403, 'not allowed'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { thing: result }));
});
}
function publicGetAll(req, res, next) {
var query = {};
if (req.query && req.query.filter) {
query = {
$text: { $search: String(req.query.filter) }
};
}
var skip = isNaN(parseInt(req.query.skip)) ? 0 : parseInt(req.query.skip);
var limit = isNaN(parseInt(req.query.limit)) ? 10 : parseInt(req.query.limit);
logic.getAllPublic(req.params.userId, query, skip, limit, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { things: result }));
});
}
function publicGetFile(req, res) {
res.sendFile(req.params.fileId, { root: path.join(config.attachmentDir, req.params.userId) });
}
function publicUsers(req, res, next) {
users.list(function (error, result) {
if (error && error.code === UserError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { users: result }));
});
}
function publicProfile(req, res, next) {
users.profile(req.params.userId, false, function (error, result) {
if (error && error.code === UserError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
var out = {
username: result.username,
displayName: result.displayName,
};
settings.get(req.params.userId, function (error, result) {
if (error) return next(new HttpError(500, error));
out.title = result.title;
out.backgroundImageDataUrl = result.publicBackground ? result.backgroundImageDataUrl : undefined;
next(new HttpSuccess(200, out));
});
});
}
function publicGetRSS(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
users.profile(req.params.userId, false, function (error, user) {
if (error && error.code === UserError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
settings.get(req.params.userId, function (error, config) {
if (error) return next(new HttpError(500, error));
logic.getAllPublic(req.params.userId, {}, 0, 50, function (error, result) {
if (error) return next(new HttpError(500, error));
var webServer = process.env.CLOUDRON_APP_ORIGIN || 'http://localhost';
var feed = new rss({
title: config.title,
image_url: webServer + '/img/logo128.png',
site_url: webServer
});
// generate the rss feed items
result.forEach(function (r) {
var title = r.content.split('\n').filter(function (l) { return !!l.trim(); })[0];
feed.item({
title: title,
url: webServer + '/blog/' + 'TODO', // TODO
author: user.displayName + '( ' + user.username + ' )',
date: new Date(r.createdAt),
description: md.render(r.richContent)
});
});
res.type('application/rss+xml').status(200).send(feed.xml());
});
});
});
}
function publicStreamPage(req, res) {
assert.strictEqual(typeof req.params.userId, 'string');
res.sendFile(path.resolve(__dirname, '../public/stream.html'));
}
// THIS DOES NOT BELONG HERE
function markdownTargetBlank(md) {
// stash the default renderer
var defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
var href = tokens[idx].attrs[tokens[idx].attrIndex('href')][1];
if (href.indexOf('https://') === 0 || href.indexOf('http://') === 0) {
// in case another plugin added that attribute already
var aIndex = tokens[idx].attrIndex('target');
if (aIndex < 0) {
tokens[idx].attrPush(['target', '_blank']); // add new attribute
} else {
tokens[idx].attrs[aIndex][1] = '_blank'; // replace value of existing attr
}
}
return defaultRender(tokens, idx, options, env, self);
};
}
function colorizeIt(md/*, options*/) {
var regexp = /\:([#\w\-]+)\:/;
function isColor(color) {
// https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
var colors = [
'clear',
'aliceblue', 'lightsalmon', 'antiquewhite', 'lightseagreen', 'aqua', 'lightskyblue', 'aquamarine', 'lightslategray', 'azure', 'lightsteelblue', 'beige', 'lightyellow', 'bisque', 'lime', 'black', 'limegreen', 'blanchedalmond', 'linen', 'blue', 'magenta', 'blueviolet', 'maroon', 'brown', 'mediumaquamarine', 'burlywood', 'mediumblue', 'cadetblue', 'mediumorchid', 'chartreuse', 'mediumpurple', 'chocolate', 'mediumseagreen', 'coral', 'mediumslateblue', 'cornflowerblue', 'mediumspringgreen', 'cornsilk', 'mediumturquoise', 'crimson', 'mediumvioletred', 'cyan', 'midnightblue', 'darkblue', 'mintcream', 'darkcyan', 'mistyrose', 'darkgoldenrod', 'moccasin', 'darkgray', 'navajowhite', 'darkgreen', 'navy', 'darkkhaki', 'oldlace', 'darkmagenta', 'olive', 'darkolivegreen', 'olivedrab', 'darkorange', 'orange', 'darkorchid', 'orangered', 'darkred', 'orchid', 'darksalmon', 'palegoldenrod', 'darkseagreen', 'palegreen', 'darkslateblue', 'paleturquoise', 'darkslategray', 'palevioletred', 'darkturquoise', 'papayawhip', 'darkviolet', 'peachpuff', 'deeppink', 'peru', 'deepskyblue', 'pink', 'dimgray', 'plum', 'dodgerblue', 'powderblue', 'firebrick', 'purple', 'floralwhite', 'red', 'forestgreen', 'rosybrown', 'fuchsia', 'royalblue', 'gainsboro', 'saddlebrown', 'ghostwhite', 'salmon', 'gold', 'sandybrown', 'goldenrod', 'seagreen', 'gray', 'seashell', 'green', 'sienna', 'greenyellow', 'silver', 'honeydew', 'skyblue', 'hotpink', 'slateblue', 'indianred', 'slategray', 'indigo', 'snow', 'ivory', 'springgreen', 'khaki', 'steelblue', 'lavender', 'tan', 'lavenderblush', 'teal', 'lawngreen', 'thistle', 'lemonchiffon', 'tomato', 'lightblue', 'turquoise', 'lightcoral', 'violet', 'lightcyan', 'wheat', 'lightgoldenrodyellow', 'white', 'lightgreen', 'whitesmoke', 'lightgrey', 'yellow', 'lightpink', 'yellowgreen'
];
if (color[0] === '#') return true;
return colors.indexOf(color) !== -1;
}
md.inline.ruler.push('colorizeIt', function (state, silent) {
// slowwww... maybe use an advanced regexp engine for this
var match = regexp.exec(state.src.slice(state.pos));
if (!match) return false;
if (!isColor(match[1])) return false;
// valid match found, now we need to advance cursor
state.pos += match[0].length;
// don't insert any tokens in silent mode
if (silent) return true;
var token = state.push('colorizeIt', '', 0);
token.meta = { color: match[1] };
return true;
});
md.renderer.rules.colorizeIt = function (tokens, id/*, options, env*/) {
if (tokens[id].meta.color === 'clear') {
return '</span>';
} else {
return '<span style="color: ' + tokens[id].meta.color + ';">';
}
};
}
var md = require('markdown-it')({
breaks: true,
html: true,
linkify: true
})
.use(require('markdown-it-emoji'))
.use(colorizeIt)
.use(require('markdown-it-checkbox'))
.use(markdownTargetBlank);
md.renderer.rules.emoji = function(token, idx) {
return require('twemoji').parse(token[idx].content);
};