save-server
Version:
A powerful ShareX image and URL server, with support for multiple users.
310 lines (282 loc) • 9.46 kB
JavaScript
// User handler
// While called "id", the "id" actually refers to a user's username, as this is what we use to uniquely identify a user.
const express = require("express");
const users = express.Router();
const auth = require("../middleware/auth");
const db = require("../util/db");
const {
errorGenerator,
errorCatch,
generateToken,
errors,
dest,
hashRounds,
adminUser,
getBase
} = require("../util");
const { isLength, isAlphanumeric, isEmpty } = require("validator");
const { compare, hash } = require("bcrypt");
const fs = require("fs");
const path = require("path");
const BIG = 1000000;
const validUsername = (str) => str && typeof str === "string" && isLength(str, {
min: 3,
max: 50
}) && isAlphanumeric(str);
const validPassword = (str) => str && typeof str === "string" && isLength(str, {
min: 3,
max: 100
});
users.post("/login", errorCatch(async function (req, res) {
const username = req.body.username;
const password = req.body.password;
if (validUsername(username)) {
if (!validPassword(password)) {
return res.status(400).send(errorGenerator(400, "Invalid password: Must be less than 100 characters."));
}
// It passes all checks
const user = await db.getUser(username);
if (user && user.password) {
const correct = await compare(password, user.password);
if (correct) {
if (user.token) {
// They have a token. Return it.
res.cookie("authorization", user.token, {
httpOnly: true,
/* A week */
expires: new Date(Date.now() + 604800000),
secure: req.secure || false,
sameSite: "Lax"
});
return res.send({ success: true, message: "Logged in." });
} else {
// No token - generate.
const token = await generateToken();
await db.setToken(user.username, token);
res.cookie("authorization", token, {
httpOnly: true,
/* A week */
expires: new Date(Date.now() + 604800000),
secure: req.secure || false,
sameSite: "Lax"
});
return res.send({ success: true, message: "Logged in." });
}
} else {
return res.status(403).send(errorGenerator(403, "Forbidden: Invalid username or password."));
}
} else {
return res.status(404).send(errorGenerator(404, "User not found."));
}
} else {
return res.status(400).send(errorGenerator(400, "Invalid username."));
}
}));
users.use(auth);
users.get("/", errorCatch(async function (req, res) {
const users = await db.getUsers();
res.send({ users, success: true });
}));
// Create is an authenticated route - only the root user can do it.
users.post("/create", errorCatch(async function (req, res) {
if (!req.user || !req.user.isAdmin) {
return res.status(403).send(errors.forbidden);
}
const username = req.body.username;
const password = req.body.password;
if (validUsername(username)) {
if (!validPassword(password)) {
return res.status(400).send(errorGenerator(400, "Invalid password. - Must be > 3 and < 50."));
}
// It passes all checks
const user = await db.getUser(username);
if (user) {
return res.status(404).send(errorGenerator(400, "Username is already in use."));
} else {
const hashed = await hash(password, hashRounds);
await db.addUser(username, hashed);
res.send({ success: true, username });
}
} else {
return res.status(400).send(errorGenerator(400, "Invalid username."));
}
}));
// Routes for root account or @me.
users.use("/:id/", async function (req, res, next) {
const { id } = req.params;
if (id) {
if (id === "@me") {
req.target = req.user;
return next();
} else {
if (!isEmpty(id) && isAlphanumeric(id)) {
const user = await db.getUser(id);
if (user) {
req.target = user;
if (user.username === adminUser) {
user.isAdmin = true;
}
if (req.target.username !== req.user.username) {
// isAdmin is added by the auth middleware
if (req.user.isAdmin) {
return next();
} else {
// They're not admin, but trying to edit another user. no!
return res.status(403).send(errors.forbidden);
}
} else {
return next();
}
} else {
return res.status(404).send(errorGenerator(404, "User not found."));
}
} else {
return res.status(400).send(errorGenerator(400, "Invalid username."));
}
}
} else {
throw new Error("No id on user middleware... what?");
}
});
users.delete("/:id", errorCatch(async function (req, res, next) {
if (req.target.isAdmin) {
return res.status(400).send(errorGenerator(400, "The admin user cannot be deleted."));
}
const deleteFiles = req.query.deleteFiles && req.query.deleteFiles === "true";
const files = await db.getAllUserFiles(req.target.username);
if (deleteFiles) {
console.log(`Deleting user ${req.target.username} AND all of their files.`);
}
for (const file of files) {
const loc = `${file.id}${file.extension ? `.${file.extension}` : ""}`;
if (deleteFiles) {
fs.unlink(path.join(dest, loc), (err) => {
if (err) {
if (err.code === "ENOENT") {
console.log(`Tried to delete file ${loc} but it was already removed.`);
} else {
return next(err);
}
}
});
} else {
await db.setFilesOwner(req.target.username, adminUser);
}
}
await db.removeUser(req.target.username);
res.send({ success: true, message: "User deleted." });
}));
users.get("/:id/config", errorCatch(async function (req, res) {
// Generate config
const isLink = req.query.link && req.query.link === "true";
const urlBase = `${getBase(req)}/api`;
let token = req.target.token;
if (!token) {
token = await generateToken();
await db.setToken(req.target.username, token);
}
const config = {
Version: "12.4.1",
Headers: {
Authorization: token
},
RequestMethod: "POST",
URL: "$json:url$",
};
if (isLink) {
config.Name = `${req.hostname} ${req.target.username} URL service`;
config.DestinationType = "URLShortener, URLSharingService";
config.RequestURL = `${urlBase}/links`;
config.Headers["shorten-url"] = "$input$";
} else {
config.Name = `${req.hostname} ${req.target.username} Upload`;
config.DestinationType = "ImageUploader, TextUploader, FileUploader";
config.RequestURL = `${urlBase}/files`;
config.Body = "MultipartFormData";
config.FileFormName = "files";
config.URL = "$json:url$";
config.ThumbnailURL = "$json:url$";
config.DeletionURL = "$json:deletionUrl$";
}
res.set({ "Content-Disposition": `attachment; filename="${req.target.username} ${isLink ? "Link shorten" : "Upload"}.sxcu"` });
res.setHeader("content-type", "application/sxcu");
const stringified = JSON.stringify(config, null, "\t");
res.send(stringified);
}));
users.patch("/:id/password", errorCatch(async function (req, res) {
// ID being @me or an ID.
// Allows the user to change password by providing current password, or if root by force.
const { newPassword, oldPassword } = req.body;
if (validPassword(newPassword)) {
if (!req.user.isAdmin) {
// They are not admin, so we need to make sure they supplied correct current password
if (validPassword(oldPassword)) {
if (!req.target.password) {
throw new Error("Target user password is not set. This should not be possible.");
}
const correct = await compare(oldPassword, req.target.password);
if (!correct) {
return res.status(403).send(errorGenerator(403, "Incorrect current password!"));
}
} else {
return res.status(400).send(errorGenerator(400, "Invalid old password."));
}
}
// they are good - either admin or provided correct pass.
const hashed = await hash(newPassword, hashRounds);
await db.setPassword(req.target.username, hashed);
return res.send({ success: true, updatedUser: req.target.username });
} else {
return res.status(400).send(errorGenerator(400, "Invalid or no new password provided."));
}
}));
users.patch("/:id/token", errorCatch(async function (req, res) {
// ID being @me or an ID.
// Allows the user to generate a new token
const newToken = await generateToken();
await db.setToken(req.target.username, newToken);
if (req.user.username === req.target.username) {
res.cookie("authorization", newToken, {
httpOnly: true,
/* A week */
expires: new Date(Date.now() + 604800000),
secure: req.secure || false,
sameSite: "Lax"
});
}
return res.send({ success: true, token: newToken });
}));
users.post("/:id/logout", errorCatch(async function (req, res) {
if (req.user.username === req.target.username) {
res.cookie("authorization", "", {
httpOnly: true,
/* Expired */
expires: new Date(Date.now() - 604800000),
secure: req.secure || false,
sameSite: "Lax"
});
}
return res.send({ success: true });
}));
users.get("/:id/files", errorCatch(async function (req, res) {
let pageNo = 0;
if (req.query.page && !isNaN(req.query.page)) {
try {
const no = parseInt(req.query.page, 10);
if (typeof no === "number" && no >= 0 && no < BIG) {
pageNo = no;
} else {
return res.status(400).send(errorGenerator(400, "Invalid page number."));
}
} catch (err) {
return res.status(400).send(errorGenerator(400, "Invalid page number."));
}
}
const files = await db.getUserFiles(req.target.username, pageNo);
res.send({ success: true, files });
}));
users.get("/:id/links", errorCatch(async function (req, res) {
const links = await db.getLinks(req.target.username);
res.send({ success: true, links: links || [] });
}));
module.exports = users;