src/logic/admin.js
const fs = require("fs");
const path = require("path");
const {Readable} = require("stream");
const async = require("async");
const csv = require("csv-streamify");
const models = require("../lib/models");
const options = require("../lib/options");
const urls = require("../lib/urls")(options);
const {cloneModel} = require("../lib/clone");
const addUser = (
{email, password, canViewPrivateSources, siteAdmin},
callback,
) => {
const User = models("User");
User.findOne({email}, (err, user) => {
if (user) {
user.email = email;
user.password = password;
user.canViewPrivateSources = !!canViewPrivateSources;
user.siteAdmin = !!siteAdmin;
// Validation is done this way to avoid this error:
// https://github.com/Automattic/mongoose/issues/6949
const error = user.validateSync();
if (error) {
return callback(error);
}
user.save({validateBeforeSave: false}, callback);
return;
}
const newUser = new User({
email,
password,
canViewPrivateSources: !!canViewPrivateSources,
siteAdmin: !!siteAdmin,
});
newUser.save(callback);
});
};
const addSource = (
{_id, name, shortName, url, isPrivate, type, converter},
i18n,
callback,
) => {
if (!_id || !name || !shortName) {
return callback(
new Error(i18n.gettext("Required field not provided.")),
);
}
if (!/^[a-z0-9-]+$/.test(_id)) {
return callback(
new Error(
"Invalid character specified for ID (only letters, numbers, and hyphen allowed).",
),
);
}
const Source = models("Source");
Source.findById(_id, (err, source) => {
if (source) {
source.name = name;
source.shortName = shortName;
source.url = url || source.url;
source.private = !!isPrivate;
// Validation is done this way to avoid this error:
// https://github.com/Automattic/mongoose/issues/6949
const error = source.validateSync();
if (error) {
return callback(error);
}
source.save({validateBeforeSave: false}, callback);
return;
}
const newSource = new Source({
_id,
name,
shortName,
url,
private: !!isPrivate,
type,
converter,
});
// Create directories to hold images
try {
const dir = newSource.getDirBase();
fs.mkdirSync(dir, {recursive: true});
fs.mkdirSync(path.join(dir, "images"), {recursive: true});
fs.mkdirSync(path.join(dir, "scaled"), {recursive: true});
fs.mkdirSync(path.join(dir, "thumbs"), {recursive: true});
} catch (e) {
return callback(
new Error(
i18n.gettext("Error creating source image directories."),
),
);
}
newSource.save(err => {
if (err) {
return callback(
new Error(i18n.gettext("Error creating source.")),
);
}
return callback();
});
});
};
module.exports = function(app) {
const {auth, isAdmin} = require("./shared/auth");
return {
admin(req, res) {
const {i18n, query} = req;
res.render("Admin", {
title: i18n.gettext("Admin"),
success: query.success,
error: query.error,
});
},
addUserPage(req, res) {
const {i18n, query} = req;
res.render("AddUser", {
title: i18n.gettext("Add or Update User"),
success: query.success,
error: query.error,
});
},
addUser(req, res, next) {
const {i18n, lang} = req;
const {
username: email,
password,
canViewPrivateSources,
siteAdmin,
} = req.body;
const handleSave = (err, user) => {
if (err) {
return next(
new Error(
i18n.gettext("Error creating or updating user."),
),
);
}
res.redirect(
urls.gen(
lang,
`/admin/add-user?success=${encodeURIComponent(
i18n.format(
i18n.gettext(
"Created or updated user: %(user)s",
),
{user: user.email},
),
)}`,
),
);
};
addUser(
{
email,
password,
canViewPrivateSources,
siteAdmin,
},
handleSave,
);
},
addUsersPage(req, res) {
const {i18n, query} = req;
res.render("AddUsers", {
title: i18n.gettext("Bulk Add or Update Users"),
success: query.success,
error: query.error,
});
},
addUsers(req, res, next) {
const {lang, i18n} = req;
let createdOrUpdated = 0;
const failed = [];
const stream = new Readable();
stream.push(req.body.usernames);
stream.push(null);
stream.pipe(
csv({objectMode: true}, (err, results) => {
if (err) {
return next(
new Error(
i18n.gettext(
"Error parsing usernames and passwords.",
),
),
);
}
async.eachLimit(
results,
4,
([username, password], callback) => {
addUser(
{
email: username,
password,
canViewPrivateSources:
req.body.canViewPrivateSources,
siteAdmin: req.body.siteAdmin,
},
err => {
if (err) {
failed.push(username);
} else {
createdOrUpdated += 1;
}
callback();
},
);
},
() => {
if (err) {
return next(err);
}
const qs = [];
if (createdOrUpdated > 0) {
qs.push(
`success=${encodeURIComponent(
i18n.format(
i18n.gettext(
"Created or updated %(num)s users.",
),
{num: createdOrUpdated},
),
)}`,
);
}
if (failed.length > 0) {
qs.push(
`error=${encodeURIComponent(
i18n.format(
i18n.gettext(
"Failed to create or update %(num)s users, including: %(failed)s",
),
{
num: failed.length,
failed: failed
.slice(0, 10)
.join(", "),
},
),
)}`,
);
}
res.redirect(
urls.gen(
lang,
`/admin/add-users${
qs.length > 0 ? `?${qs.join("&")}` : ""
}`,
),
);
},
);
}),
);
},
addSourcePage(req, res) {
const {i18n, query} = req;
res.render("AddSource", {
title: i18n.gettext("Add or Update Source"),
success: query.success,
error: query.error,
});
},
addSource(req, res, next) {
const {i18n, lang} = req;
addSource(req.body, i18n, err => {
if (err) {
return next(err);
}
// Update the internal source cache
const Source = models("Source");
Source.cacheSources(() => {
res.redirect(
urls.gen(
lang,
`/admin/manage-sources?success=${encodeURIComponent(
i18n.format(
i18n.gettext(
"New source created: %(source)s",
),
{source: req.body.name},
),
)}`,
),
);
});
});
},
addSourcesPage(req, res) {
const {i18n, query} = req;
res.render("AddSources", {
title: i18n.gettext("Bulk Add or Update Sources"),
success: query.success,
error: query.error,
});
},
addSources(req, res, next) {
const {i18n, lang} = req;
const {isPrivate, type, converter, sources} = req.body;
let createdOrUpdated = 0;
const failed = [];
const stream = new Readable();
stream.push(sources);
stream.push(null);
stream.pipe(
csv({objectMode: true}, (err, results) => {
if (err) {
return next(
new Error(
i18n.gettext("Error parsing source data."),
),
);
}
async.eachLimit(
results,
4,
([_id, name, shortName, url], callback) => {
addSource(
{
_id,
name,
shortName,
url,
isPrivate,
type,
converter,
},
i18n,
err => {
if (err) {
failed.push(name);
} else {
createdOrUpdated += 1;
}
callback();
},
);
},
() => {
if (err) {
return next(err);
}
const qs = [];
if (createdOrUpdated > 0) {
qs.push(
`success=${encodeURIComponent(
i18n.format(
i18n.gettext(
"Created or updated %(num)s sources.",
),
{num: createdOrUpdated},
),
)}`,
);
}
if (failed.length > 0) {
qs.push(
`error=${encodeURIComponent(
i18n.format(
i18n.gettext(
"Failed to create or update %(num)s sources, including: %(failed)s",
),
{
num: failed.length,
failed: failed
.slice(0, 10)
.join(", "),
},
),
)}`,
);
}
// Update the internal source cache
const Source = models("Source");
Source.cacheSources(() => {
res.redirect(
urls.gen(
lang,
`/admin/manage-sources${
qs.length > 0
? `?${qs.join("&")}`
: ""
}`,
),
);
});
},
);
}),
);
},
manageSourcesPage(req, res) {
const {i18n, query, user} = req;
const Source = models("Source");
// Only show sources on the homepage that the user is allowed to see
const sources = Source.getSourcesByViewable(user)
.map(source => {
const cloned = cloneModel(source, i18n);
cloned.numRecords = source.numRecords;
cloned.numImages = source.numImages;
return cloned;
})
.sort((a, b) => a._id.localeCompare(b._id));
res.render("ManageSources", {
title: i18n.gettext("Manage Sources"),
success: query.success,
error: query.error,
sources,
});
},
routes() {
app.get("/admin", auth, isAdmin, this.admin);
app.get("/admin/add-user", auth, isAdmin, this.addUserPage);
app.post("/admin/add-user", auth, isAdmin, this.addUser);
app.get("/admin/add-users", auth, isAdmin, this.addUsersPage);
app.post("/admin/add-users", auth, isAdmin, this.addUsers);
app.get("/admin/add-source", auth, isAdmin, this.addSourcePage);
app.post("/admin/add-source", auth, isAdmin, this.addSource);
app.get("/admin/add-sources", auth, isAdmin, this.addSourcesPage);
app.post("/admin/add-sources", auth, isAdmin, this.addSources);
app.get(
"/admin/manage-sources",
auth,
isAdmin,
this.manageSourcesPage,
);
},
};
};