more cleanups
This commit is contained in:
parent
cd0ed07687
commit
5ea233f06b
10
jsconfig.json
Normal file
10
jsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"allowImportingTsExtensions": false
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/node_modules/*"
|
||||
]
|
||||
}
|
81
package-lock.json
generated
81
package-lock.json
generated
@ -10,7 +10,6 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"app-root-path": "3.1.0",
|
||||
"axios": "1.7.7",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bull": "4.16.2",
|
||||
"cookie-parser": "1.4.6",
|
||||
@ -41,7 +40,7 @@
|
||||
"pg-query-stream": "4.6.0",
|
||||
"signale": "1.4.0",
|
||||
"useragent": "2.3.0",
|
||||
"uuid": "^10.0.0",
|
||||
"uuid": "10.0.0",
|
||||
"winston": "3.3.3",
|
||||
"winston-daily-rotate-file": "4.7.1"
|
||||
},
|
||||
@ -1478,23 +1477,6 @@
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-styled-components": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz",
|
||||
@ -1907,18 +1889,6 @@
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||
@ -2146,15 +2116,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
@ -2595,26 +2556,6 @@
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/foreach": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz",
|
||||
@ -2628,20 +2569,6 @@
|
||||
"integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==",
|
||||
"license": "Apache2"
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -4600,12 +4527,6 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pseudomap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||
|
@ -27,7 +27,6 @@
|
||||
"homepage": "https://github.com/thedevs-network/kutt#readme",
|
||||
"dependencies": {
|
||||
"app-root-path": "3.1.0",
|
||||
"axios": "1.7.7",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bull": "4.16.2",
|
||||
"cookie-parser": "1.4.6",
|
||||
|
@ -23,6 +23,7 @@ const env = cleanEnv(process.env, {
|
||||
DEFAULT_MAX_STATS_PER_LINK: num({ default: 5000 }),
|
||||
DISALLOW_ANONYMOUS_LINKS: bool({ default: false }),
|
||||
DISALLOW_REGISTRATION: bool({ default: false }),
|
||||
SERVER_IP_ADDRESS: str({ default: "" }),
|
||||
CUSTOM_DOMAIN_USE_HTTPS: bool({ default: false }),
|
||||
JWT_SECRET: str(),
|
||||
ADMIN_EMAILS: str({ default: "" }),
|
||||
|
@ -3,36 +3,49 @@ const passport = require("passport");
|
||||
const { v4: uuid } = require("uuid");
|
||||
const bcrypt = require("bcryptjs");
|
||||
const nanoid = require("nanoid");
|
||||
const axios = require("axios");
|
||||
|
||||
const { CustomError } = require("../utils");
|
||||
const query = require("../queries");
|
||||
const utils = require("../utils");
|
||||
const redis = require("../redis");
|
||||
const mail = require("../mail");
|
||||
const env = require("../env");
|
||||
|
||||
function authenticate(type, error, isStrict) {
|
||||
const CustomError = utils.CustomError;
|
||||
|
||||
function authenticate(type, error, isStrict, redirect) {
|
||||
return function auth(req, res, next) {
|
||||
if (req.user) return next();
|
||||
|
||||
passport.authenticate(type, (err, user) => {
|
||||
if (err) return next(err);
|
||||
const accepts = req.accepts(["json", "html"]);
|
||||
|
||||
if (
|
||||
redirect &&
|
||||
((!user && isStrict) ||
|
||||
(user && isStrict && !user.verified) ||
|
||||
(user && user.banned))
|
||||
) {
|
||||
if (redirect === "page") {
|
||||
res.redirect("/login");
|
||||
return;
|
||||
}
|
||||
if (redirect === "header") {
|
||||
res.setHeader("HX-Redirect", "/login");
|
||||
res.send("NOT_AUTHENTICATED");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!user && isStrict) {
|
||||
req.viewTemplate = "partials/auth/form";
|
||||
throw new CustomError(error, 401);
|
||||
}
|
||||
|
||||
if (user && isStrict && !user.verified) {
|
||||
req.viewTemplate = "partials/auth/form";
|
||||
throw new CustomError("Your email address is not verified. " +
|
||||
"Sign up to get the verification link again.", 400);
|
||||
}
|
||||
|
||||
if (user && user.banned) {
|
||||
req.viewTemplate = "partials/auth/form";
|
||||
throw new CustomError("You're banned from using this website.", 403);
|
||||
}
|
||||
|
||||
@ -49,10 +62,11 @@ function authenticate(type, error, isStrict) {
|
||||
}
|
||||
}
|
||||
|
||||
const local = authenticate("local", "Login credentials are wrong.", true);
|
||||
const jwt = authenticate("jwt", "Unauthorized.", true);
|
||||
const jwtLoose = authenticate("jwt", "Unauthorized.", false);
|
||||
const apikey = authenticate("localapikey", "API key is not correct.", false);
|
||||
const local = authenticate("local", "Login credentials are wrong.", true, null);
|
||||
const jwt = authenticate("jwt", "Unauthorized.", true, "header");
|
||||
const jwtPage = authenticate("jwt", "Unauthorized.", true, "page");
|
||||
const jwtLoose = authenticate("jwt", "Unauthorized.", false, null);
|
||||
const apikey = authenticate("localapikey", "API key is not correct.", false, null);
|
||||
|
||||
async function cooldown(req, res, next) {
|
||||
if (env.DISALLOW_ANONYMOUS_LINKS) return next();
|
||||
@ -76,7 +90,6 @@ async function cooldown(req, res, next) {
|
||||
}
|
||||
|
||||
function admin(req, res, next) {
|
||||
// FIXME: attaching to req is risky, find another way
|
||||
if (req.user.admin) return next();
|
||||
throw new CustomError("Unauthorized", 401);
|
||||
}
|
||||
@ -104,11 +117,7 @@ function login(req, res) {
|
||||
const token = utils.signToken(req.user);
|
||||
|
||||
if (req.isHTML) {
|
||||
res.cookie("token", token, {
|
||||
maxAge: 1000 * 60 * 60 * 24 * 7, // expire after seven days
|
||||
httpOnly: true,
|
||||
secure: env.isProd
|
||||
});
|
||||
utils.setToken(res, token);
|
||||
res.render("partials/auth/welcome");
|
||||
return;
|
||||
}
|
||||
@ -133,12 +142,8 @@ async function verify(req, res, next) {
|
||||
|
||||
if (user) {
|
||||
const token = utils.signToken(user);
|
||||
res.clearCookie("token", { httpOnly: true, secure: env.isProd });
|
||||
res.cookie("token", token, {
|
||||
maxAge: 1000 * 60 * 60 * 24 * 7, // expire after seven days
|
||||
httpOnly: true,
|
||||
secure: env.isProd
|
||||
});
|
||||
utils.deleteCurrentToken(res);
|
||||
utils.setToken(res, token);
|
||||
res.locals.token_verified = true;
|
||||
req.cookies.token = token;
|
||||
}
|
||||
@ -208,7 +213,7 @@ async function resetPasswordRequest(req, res) {
|
||||
|
||||
if (user) {
|
||||
// TODO: handle error
|
||||
await mail.resetPasswordToken(user).catch(() => null);
|
||||
mail.resetPasswordToken(user).catch(() => null);
|
||||
}
|
||||
|
||||
if (req.isHTML) {
|
||||
@ -237,12 +242,8 @@ async function resetPassword(req, res, next) {
|
||||
|
||||
if (user) {
|
||||
const token = utils.signToken(user);
|
||||
res.clearCookie("token", { httpOnly: true, secure: env.isProd });
|
||||
res.cookie("token", token, {
|
||||
maxAge: 1000 * 60 * 60 * 24 * 7, // expire after seven days
|
||||
httpOnly: true,
|
||||
secure: env.isProd
|
||||
});
|
||||
utils.deleteCurrentToken(res);
|
||||
utils.setToken(res, token);
|
||||
res.locals.token_verified = true;
|
||||
req.cookies.token = token;
|
||||
}
|
||||
@ -306,8 +307,6 @@ async function changeEmailRequest(req, res) {
|
||||
async function changeEmail(req, res, next) {
|
||||
const changeEmailToken = req.params.changeEmailToken;
|
||||
|
||||
console.log("-", changeEmailToken, "-");
|
||||
|
||||
if (changeEmailToken) {
|
||||
const foundUser = await query.user.find({
|
||||
change_email_token: changeEmailToken
|
||||
@ -332,12 +331,8 @@ async function changeEmail(req, res, next) {
|
||||
|
||||
if (user) {
|
||||
const token = utils.signToken(user);
|
||||
res.clearCookie("token", { httpOnly: true, secure: env.isProd });
|
||||
res.cookie("token", token, {
|
||||
maxAge: 1000 * 60 * 60 * 24 * 7, // expire after seven days
|
||||
httpOnly: true,
|
||||
secure: env.isProd
|
||||
});
|
||||
utils.deleteCurrentToken(res);
|
||||
utils.setToken(res, token);
|
||||
res.locals.token_verified = true;
|
||||
req.cookies.token = token;
|
||||
}
|
||||
@ -355,6 +350,7 @@ module.exports = {
|
||||
generateApiKey,
|
||||
jwt,
|
||||
jwtLoose,
|
||||
jwtPage,
|
||||
local,
|
||||
login,
|
||||
resetPassword,
|
||||
|
@ -1,6 +1,6 @@
|
||||
const { Handler } = require("express");
|
||||
|
||||
const { CustomError, sanitize, sleep } = require("../utils");
|
||||
const { CustomError, sanitize } = require("../utils");
|
||||
const query = require("../queries");
|
||||
const redis = require("../redis");
|
||||
|
||||
|
@ -1,16 +1,14 @@
|
||||
const { validationResult } = require("express-validator");
|
||||
const signale = require("signale");
|
||||
|
||||
const { CustomError, sanitize } = require("../utils");
|
||||
const { logger } = require("../config/winston");
|
||||
const query = require("../queries")
|
||||
const { CustomError } = require("../utils");
|
||||
const env = require("../env");
|
||||
|
||||
// export const ip: Handler = (req, res, next) => {
|
||||
// req.realIP =
|
||||
// (req.headers["x-real-ip"] as string) || req.connection.remoteAddress || "";
|
||||
// return next();
|
||||
// };
|
||||
function ip(req, res, next) {
|
||||
req.realIP = req.headers["x-real-ip"] || req.connection.remoteAddress || "";
|
||||
return next();
|
||||
};
|
||||
|
||||
function error(error, req, res, _next) {
|
||||
if (env.isDev) {
|
||||
@ -42,7 +40,6 @@ function verify(req, res, next) {
|
||||
res.locals.errors[e.param] = e.msg;
|
||||
});
|
||||
|
||||
|
||||
throw new CustomError(error, 400);
|
||||
}
|
||||
|
||||
@ -71,11 +68,10 @@ function parseQuery(req, res, next) {
|
||||
}
|
||||
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const skip = parseInt(req.query.skip) || 0;
|
||||
|
||||
req.context = {
|
||||
limit: limit > 50 ? 50 : limit,
|
||||
skip,
|
||||
skip: parseInt(req.query.skip) || 0,
|
||||
all: admin ? req.query.all === "true" || req.query.all === "on" : false
|
||||
};
|
||||
|
||||
@ -84,6 +80,7 @@ function parseQuery(req, res, next) {
|
||||
|
||||
module.exports = {
|
||||
error,
|
||||
ip,
|
||||
parseQuery,
|
||||
verify,
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
const { differenceInSeconds } = require("date-fns");
|
||||
const promisify = require("util").promisify;
|
||||
const bcrypt = require("bcryptjs");
|
||||
const isbot = require("isbot");
|
||||
@ -11,7 +12,6 @@ const query = require("../queries");
|
||||
const queue = require("../queues");
|
||||
const utils = require("../utils");
|
||||
const env = require("../env");
|
||||
const { differenceInSeconds } = require("date-fns");
|
||||
|
||||
const CustomError = utils.CustomError;
|
||||
const dnsLookup = promisify(dns.lookup);
|
||||
@ -56,7 +56,7 @@ async function create(req, res) {
|
||||
|
||||
const targetDomain = utils.removeWww(URL.parse(target).hostname);
|
||||
|
||||
const queries = await Promise.all([
|
||||
const tasks = await Promise.all([
|
||||
validators.cooldown(req.user),
|
||||
validators.malware(req.user, target),
|
||||
validators.linksCount(req.user),
|
||||
@ -78,19 +78,19 @@ async function create(req, res) {
|
||||
|
||||
// if "reuse" is true, try to return
|
||||
// the existent URL without creating one
|
||||
if (queries[3]) {
|
||||
return res.json(utils.sanitize.link(queries[3]));
|
||||
if (tasks[3]) {
|
||||
return res.json(utils.sanitize.link(tasks[3]));
|
||||
}
|
||||
|
||||
// Check if custom link already exists
|
||||
if (queries[4]) {
|
||||
if (tasks[4]) {
|
||||
const error = "Custom URL is already in use.";
|
||||
res.locals.errors = { customurl: error };
|
||||
throw new CustomError(error);
|
||||
}
|
||||
|
||||
// Create new link
|
||||
const address = customurl || queries[5];
|
||||
const address = customurl || tasks[5];
|
||||
const link = await query.link.create({
|
||||
password,
|
||||
address,
|
||||
@ -122,7 +122,6 @@ async function create(req, res) {
|
||||
}
|
||||
|
||||
async function edit(req, res) {
|
||||
const { address, target, description, expire_in, password } = req.body;
|
||||
const link = await query.link.find({
|
||||
uuid: req.params.id,
|
||||
...(!req.user.admin && { user_id: req.user.id })
|
||||
@ -134,23 +133,32 @@ async function edit(req, res) {
|
||||
|
||||
let isChanged = false;
|
||||
[
|
||||
[address, "address"],
|
||||
[target, "target"],
|
||||
[description, "description"],
|
||||
[expire_in, "expire_in"],
|
||||
[password, "password"]
|
||||
[req.body.address, "address"],
|
||||
[req.body.target, "target"],
|
||||
[req.body.description, "description"],
|
||||
[req.body.expire_in, "expire_in"],
|
||||
[req.body.password, "password"]
|
||||
].forEach(([value, name]) => {
|
||||
if (!value) {
|
||||
if (name === "password" && link.password)
|
||||
req.body.password = null;
|
||||
else {
|
||||
delete req.body[name];
|
||||
return;
|
||||
}
|
||||
if (value === link[name]) {
|
||||
}
|
||||
if (value === link[name] && name !== "password") {
|
||||
delete req.body[name];
|
||||
return;
|
||||
}
|
||||
if (name === "expire_in")
|
||||
if (differenceInSeconds(new Date(value), new Date(link.expire_in)) <= 60)
|
||||
return;
|
||||
if (name === "password")
|
||||
if (value && value.replace(/•/ig, "").length === 0) {
|
||||
delete req.body.password;
|
||||
return;
|
||||
}
|
||||
isChanged = true;
|
||||
});
|
||||
|
||||
@ -158,23 +166,25 @@ async function edit(req, res) {
|
||||
throw new CustomError("Should at least update one field.");
|
||||
}
|
||||
|
||||
const targetDomain = utils.removeWww(URL.parse(target).hostname);
|
||||
const { address, target, description, expire_in, password } = req.body;
|
||||
|
||||
const targetDomain = target && utils.removeWww(URL.parse(target).hostname);
|
||||
const domain_id = link.domain_id || null;
|
||||
|
||||
const queries = await Promise.all([
|
||||
const tasks = await Promise.all([
|
||||
validators.cooldown(req.user),
|
||||
target && validators.malware(req.user, target),
|
||||
address && address !== link.address &&
|
||||
address &&
|
||||
query.link.find({
|
||||
address,
|
||||
domain_id
|
||||
}),
|
||||
validators.bannedDomain(targetDomain),
|
||||
validators.bannedHost(targetDomain)
|
||||
target && validators.bannedDomain(targetDomain),
|
||||
target && validators.bannedHost(targetDomain)
|
||||
]);
|
||||
|
||||
// Check if custom link already exists
|
||||
if (queries[2]) {
|
||||
if (tasks[2]) {
|
||||
const error = "Custom URL is already in use.";
|
||||
res.locals.errors = { address: error };
|
||||
throw new CustomError("Custom URL is already in use.");
|
||||
@ -190,7 +200,7 @@ async function edit(req, res) {
|
||||
...(description && { description }),
|
||||
...(target && { target }),
|
||||
...(expire_in && { expire_in }),
|
||||
...(password && { password })
|
||||
...((password || password === null) && { password })
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -8,7 +8,7 @@ function isHTML(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
function addNoLayoutLocals(req, res, next) {
|
||||
function noLayout(req, res, next) {
|
||||
res.locals.layout = null;
|
||||
next();
|
||||
}
|
||||
@ -20,13 +20,14 @@ function viewTemplate(template) {
|
||||
}
|
||||
}
|
||||
|
||||
function addConfigLocals(req, res, next) {
|
||||
function config(req, res, next) {
|
||||
res.locals.default_domain = env.DEFAULT_DOMAIN;
|
||||
res.locals.site_name = env.SITE_NAME;
|
||||
res.locals.server_ip_address = env.SERVER_IP_ADDRESS;
|
||||
next();
|
||||
}
|
||||
|
||||
async function addUserLocals(req, res, next) {
|
||||
async function user(req, res, next) {
|
||||
const user = req.user;
|
||||
res.locals.user = user;
|
||||
res.locals.domains = user && (await query.domain.get({ user_id: user.id })).map(utils.sanitize.domain);
|
||||
@ -50,12 +51,12 @@ function protected(req, res, next) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addConfigLocals,
|
||||
addNoLayoutLocals,
|
||||
addUserLocals,
|
||||
config,
|
||||
createLink,
|
||||
editLink,
|
||||
isHTML,
|
||||
noLayout,
|
||||
protected,
|
||||
user,
|
||||
viewTemplate,
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
const query = require("../queries");
|
||||
const utils = require("../utils");
|
||||
const query = require("../queries")
|
||||
const env = require("../env");
|
||||
|
||||
async function homepage(req, res) {
|
||||
@ -10,7 +10,8 @@ async function homepage(req, res) {
|
||||
|
||||
function login(req, res) {
|
||||
if (req.user) {
|
||||
return res.redirect("/");
|
||||
res.redirect("/");
|
||||
return;
|
||||
}
|
||||
res.render("login", {
|
||||
title: "Log in or sign up"
|
||||
@ -18,7 +19,7 @@ function login(req, res) {
|
||||
}
|
||||
|
||||
function logout(req, res) {
|
||||
res.clearCookie("token", { httpOnly: true, secure: env.isProd });
|
||||
utils.deleteCurrentToken(res);
|
||||
res.render("logout", {
|
||||
title: "Logging out.."
|
||||
});
|
||||
@ -31,21 +32,12 @@ function notFound(req, res) {
|
||||
}
|
||||
|
||||
function settings(req, res) {
|
||||
// TODO: make this a middelware function, apply it to where it's necessary
|
||||
if (!req.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
res.render("settings", {
|
||||
title: "Settings"
|
||||
});
|
||||
}
|
||||
|
||||
function stats(req, res) {
|
||||
// TODO: make this a middelware function, apply it to where it's necessary
|
||||
if (!req.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
const id = req.query.id;
|
||||
res.render("stats", {
|
||||
title: "Stats"
|
||||
});
|
||||
@ -154,21 +146,13 @@ async function getReportEmail(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function linkEdit(req, res) {
|
||||
const link = await query.link.find({
|
||||
uuid: req.params.id,
|
||||
...(!req.user.admin && { user_id: req.user.id })
|
||||
});
|
||||
// TODO: handle when no link
|
||||
// if (!link) {
|
||||
// return res.render("partials/links/dialog/message", {
|
||||
// layout: false,
|
||||
// message: "Could not find the link."
|
||||
// });
|
||||
// }
|
||||
res.render("partials/links/edit", {
|
||||
...utils.sanitize.link(link),
|
||||
...(!link && utils.sanitize.link(link)),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ async function remove(req, res) {
|
||||
await query.user.remove(req.user);
|
||||
|
||||
if (req.isHTML) {
|
||||
res.clearCookie("token", { httpOnly: true, secure: env.isProd });
|
||||
utils.deleteCurrentToken(res);
|
||||
res.setHeader("HX-Trigger-After-Swap", "redirectToHomepage");
|
||||
res.render("partials/settings/delete_account", {
|
||||
success: "Account has been deleted. Logging out..."
|
||||
|
@ -1,8 +1,7 @@
|
||||
const { body, param } = require("express-validator");
|
||||
const { isAfter, subDays, subHours, addMilliseconds } = require("date-fns");
|
||||
const { promisify } = require("util");
|
||||
const { body, param } = require("express-validator");
|
||||
const promisify = require("util").promisify;
|
||||
const bcrypt = require("bcryptjs");
|
||||
const axios = require("axios");
|
||||
const dns = require("dns");
|
||||
const URL = require("url");
|
||||
const ms = require("ms");
|
||||
@ -109,11 +108,7 @@ const editLink = [
|
||||
.isLength({ min: 1, max: 2040 })
|
||||
.withMessage("Maximum URL length is 2040.")
|
||||
.customSanitizer(utils.addProtocol)
|
||||
.custom(
|
||||
value =>
|
||||
urlRegex({ exact: true, strict: false }).test(value) ||
|
||||
/^(?!https?)(\w+):\/\//.test(value)
|
||||
)
|
||||
.custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\w+:|\/\/)/.test(value))
|
||||
.withMessage("URL is not valid.")
|
||||
.custom(value => utils.removeWww(URL.parse(value).host) !== env.DEFAULT_DOMAIN)
|
||||
.withMessage(`${env.DEFAULT_DOMAIN} URLs are not allowed.`),
|
||||
@ -176,11 +171,12 @@ const addDomain = [
|
||||
.isLength({ min: 3, max: 64 })
|
||||
.withMessage("Domain length must be between 3 and 64.")
|
||||
.trim()
|
||||
.customSanitizer(utils.addProtocol)
|
||||
.custom(value => utils.urlRegex.test(value))
|
||||
.customSanitizer(value => {
|
||||
const parsed = URL.parse(value);
|
||||
return utils.removeWww(parsed.hostname || parsed.href);
|
||||
})
|
||||
.custom(value => urlRegex({ exact: true, strict: false }).test(value))
|
||||
.custom(value => value !== env.DEFAULT_DOMAIN)
|
||||
.withMessage("You can't use the default domain.")
|
||||
.custom(async value => {
|
||||
@ -191,7 +187,7 @@ const addDomain = [
|
||||
body("homepage")
|
||||
.optional({ checkFalsy: true, nullable: true })
|
||||
.customSanitizer(utils.addProtocol)
|
||||
.custom(value => urlRegex({ exact: true, strict: false }).test(value))
|
||||
.custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\w+:|\/\/)/.test(value))
|
||||
.withMessage("Homepage is not valid.")
|
||||
];
|
||||
|
||||
@ -282,11 +278,11 @@ const signup = [
|
||||
.custom(async (value, { req }) => {
|
||||
const user = await query.user.find({ email: value });
|
||||
|
||||
if (user) {
|
||||
if (user)
|
||||
req.user = user;
|
||||
}
|
||||
|
||||
if (user?.verified) return Promise.reject();
|
||||
if (user?.verified)
|
||||
return Promise.reject();
|
||||
})
|
||||
.withMessage("You can't use this email address.")
|
||||
];
|
||||
@ -337,15 +333,6 @@ const resetPassword = [
|
||||
.withMessage("Email length must be max 255.")
|
||||
];
|
||||
|
||||
// export const resetEmailRequest = [
|
||||
// body("email", "Email is not valid.")
|
||||
// .exists({ checkFalsy: true, checkNull: true })
|
||||
// .trim()
|
||||
// .isEmail()
|
||||
// .isLength({ min: 0, max: 255 })
|
||||
// .withMessage("Email length must be max 255.")
|
||||
// ];
|
||||
|
||||
const deleteUser = [
|
||||
body("password", "Password is not valid.")
|
||||
.exists({ checkFalsy: true, checkNull: true })
|
||||
@ -375,9 +362,11 @@ function cooldown(user) {
|
||||
async function malware(user, target) {
|
||||
if (!env.GOOGLE_SAFE_BROWSING_KEY) return;
|
||||
|
||||
const isMalware = await axios.post(
|
||||
const isMalware = await fetch(
|
||||
`https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${env.GOOGLE_SAFE_BROWSING_KEY}`,
|
||||
{
|
||||
method: "post",
|
||||
body: JSON.stringify({
|
||||
client: {
|
||||
clientId: env.DEFAULT_DOMAIN.toLowerCase().replace(".", ""),
|
||||
clientVersion: "1.0.0"
|
||||
@ -398,8 +387,9 @@ async function malware(user, target) {
|
||||
],
|
||||
threatEntries: [{ url: target }]
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
).then(res => res.json());
|
||||
if (!isMalware.data || !isMalware.data.matches) return;
|
||||
|
||||
if (user) {
|
||||
|
@ -23,10 +23,7 @@ const transporter = nodemailer.createTransport(mailConfig);
|
||||
// Read email templates
|
||||
const resetEmailTemplatePath = path.join(__dirname, "template-reset.html");
|
||||
const verifyEmailTemplatePath = path.join(__dirname, "template-verify.html");
|
||||
const changeEmailTemplatePath = path.join(
|
||||
__dirname,
|
||||
"template-change-email.html"
|
||||
);
|
||||
const changeEmailTemplatePath = path.join(__dirname,"template-change-email.html");
|
||||
const resetEmailTemplate = fs
|
||||
.readFileSync(resetEmailTemplatePath, { encoding: "utf-8" })
|
||||
.replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
|
||||
|
@ -1,6 +1,5 @@
|
||||
const bcrypt = require("bcryptjs");
|
||||
|
||||
// FIXME: circular dependency
|
||||
const CustomError = require("../utils").CustomError;
|
||||
const redis = require("../redis");
|
||||
const knex = require("../knex");
|
||||
|
@ -52,11 +52,11 @@ async function add(params) {
|
||||
};
|
||||
|
||||
async function find(match, total) {
|
||||
// if (match.link_id) {
|
||||
// const key = redis.key.stats(match.link_id);
|
||||
// const cached = await redis.client.get(key);
|
||||
// if (cached) return JSON.parse(cached);
|
||||
// }
|
||||
if (match.link_id) {
|
||||
const key = redis.key.stats(match.link_id);
|
||||
const cached = await redis.client.get(key);
|
||||
if (cached) return JSON.parse(cached);
|
||||
}
|
||||
|
||||
const stats = {
|
||||
lastDay: {
|
||||
|
@ -12,7 +12,7 @@ const router = Router();
|
||||
router.get(
|
||||
"/",
|
||||
asyncHandler(auth.jwtLoose),
|
||||
asyncHandler(locals.addUserLocals),
|
||||
asyncHandler(locals.user),
|
||||
asyncHandler(renders.homepage)
|
||||
);
|
||||
|
||||
@ -31,35 +31,42 @@ router.get(
|
||||
router.get(
|
||||
"/404",
|
||||
asyncHandler(auth.jwtLoose),
|
||||
asyncHandler(locals.user),
|
||||
asyncHandler(renders.notFound)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/settings",
|
||||
asyncHandler(auth.jwtLoose),
|
||||
asyncHandler(locals.addUserLocals),
|
||||
asyncHandler(auth.jwtPage),
|
||||
asyncHandler(locals.user),
|
||||
asyncHandler(renders.settings)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/stats",
|
||||
asyncHandler(auth.jwtLoose),
|
||||
asyncHandler(locals.addUserLocals),
|
||||
asyncHandler(auth.jwtPage),
|
||||
asyncHandler(locals.user),
|
||||
asyncHandler(renders.stats)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/banned",
|
||||
asyncHandler(auth.jwtLoose),
|
||||
asyncHandler(locals.user),
|
||||
asyncHandler(renders.banned)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/report",
|
||||
asyncHandler(auth.jwtLoose),
|
||||
asyncHandler(locals.user),
|
||||
asyncHandler(renders.report)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/reset-password",
|
||||
asyncHandler(auth.jwtLoose),
|
||||
asyncHandler(locals.user),
|
||||
asyncHandler(renders.resetPassword)
|
||||
);
|
||||
|
||||
@ -67,7 +74,7 @@ router.get(
|
||||
"/reset-password/:resetPasswordToken",
|
||||
asyncHandler(auth.resetPassword),
|
||||
asyncHandler(auth.jwtLoose),
|
||||
asyncHandler(locals.addUserLocals),
|
||||
asyncHandler(locals.user),
|
||||
asyncHandler(renders.resetPasswordResult)
|
||||
);
|
||||
|
||||
@ -75,7 +82,7 @@ router.get(
|
||||
"/verify-email/:changeEmailToken",
|
||||
asyncHandler(auth.changeEmail),
|
||||
asyncHandler(auth.jwtLoose),
|
||||
asyncHandler(locals.addUserLocals),
|
||||
asyncHandler(locals.user),
|
||||
asyncHandler(renders.verifyChangeEmail)
|
||||
);
|
||||
|
||||
@ -83,26 +90,28 @@ router.get(
|
||||
"/verify/:verificationToken",
|
||||
asyncHandler(auth.verify),
|
||||
asyncHandler(auth.jwtLoose),
|
||||
asyncHandler(locals.addUserLocals),
|
||||
asyncHandler(locals.user),
|
||||
asyncHandler(renders.verify)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/terms",
|
||||
asyncHandler(auth.jwtLoose),
|
||||
asyncHandler(locals.user),
|
||||
asyncHandler(renders.terms)
|
||||
);
|
||||
|
||||
// partial renders
|
||||
router.get(
|
||||
"/confirm-link-delete",
|
||||
locals.addNoLayoutLocals,
|
||||
locals.noLayout,
|
||||
asyncHandler(auth.jwt),
|
||||
asyncHandler(renders.confirmLinkDelete)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/confirm-link-ban",
|
||||
locals.addNoLayoutLocals,
|
||||
locals.noLayout,
|
||||
locals.viewTemplate("partials/links/dialog/message"),
|
||||
asyncHandler(auth.jwt),
|
||||
asyncHandler(auth.admin),
|
||||
@ -111,21 +120,21 @@ router.get(
|
||||
|
||||
router.get(
|
||||
"/link/edit/:id",
|
||||
locals.addNoLayoutLocals,
|
||||
locals.noLayout,
|
||||
asyncHandler(auth.jwt),
|
||||
asyncHandler(renders.linkEdit)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/add-domain-form",
|
||||
locals.addNoLayoutLocals,
|
||||
locals.noLayout,
|
||||
asyncHandler(auth.jwt),
|
||||
asyncHandler(renders.addDomainForm)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/confirm-domain-delete",
|
||||
locals.addNoLayoutLocals,
|
||||
locals.noLayout,
|
||||
locals.viewTemplate("partials/settings/domain/delete"),
|
||||
asyncHandler(auth.jwt),
|
||||
asyncHandler(renders.confirmDomainDelete)
|
||||
@ -133,7 +142,7 @@ router.get(
|
||||
|
||||
router.get(
|
||||
"/get-report-email",
|
||||
locals.addNoLayoutLocals,
|
||||
locals.noLayout,
|
||||
locals.viewTemplate("partials/report/email"),
|
||||
asyncHandler(renders.getReportEmail)
|
||||
);
|
||||
|
@ -9,11 +9,11 @@ const link = require("./link.routes");
|
||||
const user = require("./user.routes");
|
||||
const auth = require("./auth.routes");
|
||||
|
||||
const apiRouter = Router();
|
||||
const renderRouter = Router();
|
||||
|
||||
renderRouter.use(renders);
|
||||
apiRouter.use(locals.addNoLayoutLocals);
|
||||
|
||||
const apiRouter = Router();
|
||||
apiRouter.use(locals.noLayout);
|
||||
apiRouter.use("/domains", domains);
|
||||
apiRouter.use("/health", health);
|
||||
apiRouter.use("/links", link);
|
||||
|
@ -22,7 +22,8 @@ require("./passport");
|
||||
// create express app
|
||||
const app = express();
|
||||
|
||||
// TODO: comments
|
||||
// stating that this app is running behind a proxy
|
||||
// and the express app should get the IP address from the proxy server
|
||||
app.set("trust proxy", true);
|
||||
|
||||
if (env.isDev) {
|
||||
@ -36,27 +37,29 @@ app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.static("static"));
|
||||
|
||||
app.use(passport.initialize());
|
||||
// app.use(helpers.ip);
|
||||
app.use(helpers.ip);
|
||||
app.use(locals.isHTML);
|
||||
app.use(locals.addConfigLocals);
|
||||
app.use(locals.config);
|
||||
|
||||
// template engine / serve html
|
||||
app.set("view engine", "hbs");
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
utils.registerHandlebarsHelpers();
|
||||
|
||||
// render html pages
|
||||
app.use("/", routes.render);
|
||||
|
||||
// if is custom domain, redirect to the set homepage
|
||||
app.use(asyncHandler(links.redirectCustomDomainHomepage));
|
||||
|
||||
// handle api requests
|
||||
app.use("/api/v2", routes.api);
|
||||
app.use("/api", routes.api);
|
||||
|
||||
// finally, redirect the short link to the target
|
||||
app.get("/:id", asyncHandler(links.redirect));
|
||||
|
||||
// Error handler
|
||||
// handle errors coming from above routes
|
||||
app.use(helpers.error);
|
||||
|
||||
app.listen(env.PORT, () => {
|
||||
|
@ -1,9 +1,9 @@
|
||||
const ms = require("ms");
|
||||
const path = require("path");
|
||||
const { differenceInDays, differenceInHours, differenceInMonths, differenceInMilliseconds, addDays, subHours, subDays, subMonths, subYears } = require("date-fns");
|
||||
const nanoid = require("nanoid/generate");
|
||||
const JWT = require("jsonwebtoken");
|
||||
const { differenceInDays, differenceInHours, differenceInMonths, differenceInMilliseconds, addDays, subHours, subDays, subMonths, subYears } = require("date-fns");
|
||||
const path = require("path");
|
||||
const hbs = require("hbs");
|
||||
const ms = require("ms");
|
||||
|
||||
const env = require("../env");
|
||||
|
||||
@ -37,6 +37,18 @@ function signToken(user) {
|
||||
)
|
||||
}
|
||||
|
||||
function setToken(res, token) {
|
||||
res.cookie("token", token, {
|
||||
maxAge: 1000 * 60 * 60 * 24 * 7, // expire after seven days
|
||||
httpOnly: true,
|
||||
secure: env.isProd
|
||||
});
|
||||
}
|
||||
|
||||
function deleteCurrentToken(res) {
|
||||
res.clearCookie("token", { httpOnly: true, secure: env.isProd });
|
||||
}
|
||||
|
||||
async function generateId(query, domain_id) {
|
||||
const address = nanoid(
|
||||
"abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789",
|
||||
@ -262,6 +274,7 @@ function registerHandlebarsHelpers() {
|
||||
module.exports = {
|
||||
addProtocol,
|
||||
CustomError,
|
||||
deleteCurrentToken,
|
||||
generateId,
|
||||
getDifferenceFunction,
|
||||
getInitStats,
|
||||
@ -276,6 +289,7 @@ module.exports = {
|
||||
registerHandlebarsHelpers,
|
||||
removeWww,
|
||||
sanitize,
|
||||
setToken,
|
||||
signToken,
|
||||
sleep,
|
||||
statsObjectToArray,
|
||||
|
@ -2,8 +2,7 @@
|
||||
<div class="logo-wrapper">
|
||||
<a class="logo nav" href="/" title="Kutt">
|
||||
<img src="/images/logo.svg" alt="kutt" width="18" height="24" />
|
||||
{{!-- TODO: configurable site name --}}
|
||||
Kutt
|
||||
{{site_name}}
|
||||
</a>
|
||||
<ul class="logo-links">
|
||||
<li>
|
||||
|
@ -1,4 +1,5 @@
|
||||
<td class="content">
|
||||
{{#if id}}
|
||||
<form
|
||||
id="edit-form-{{id}}"
|
||||
hx-patch="/api/links/{id}"
|
||||
@ -110,4 +111,7 @@
|
||||
{{> links/tr}}
|
||||
</template>
|
||||
</form>
|
||||
{{else}}
|
||||
<p class="no-links">No link was found.</p>
|
||||
{{/if}}
|
||||
</td>
|
@ -6,10 +6,17 @@
|
||||
<b>{{default_domain}}/shorturl</b> you can have
|
||||
<b>yoursite.com/shorturl.</b>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Point your domain's A record to <b>192.64.116.170</b> then add the domain
|
||||
via the form below:
|
||||
Point your domain's A record to
|
||||
{{#if server_ip_address}}
|
||||
<b>{{server_ip_address}}</b>
|
||||
{{else}}
|
||||
our <b>IP address</b>
|
||||
{{/if}}
|
||||
then add the domain via the form below:
|
||||
</p>
|
||||
|
||||
{{> settings/domain/table}}
|
||||
<div class="add-domain-wrapper">
|
||||
<button
|
||||
|
@ -1407,6 +1407,11 @@ main form label#advanced input {
|
||||
|
||||
#links-table-wrapper table tr.edit form .response p { margin: 2rem 0 0; }
|
||||
|
||||
#links-table-wrapper table tr.edit p.no-links {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dialog .ban-checklist {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -117,7 +117,6 @@ function handleShortURLCopyLink(element) {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// TODO: make it an extension
|
||||
// open and close dialog
|
||||
function openDialog(id, name) {
|
||||
const dialog = document.getElementById(id);
|
||||
|
Loading…
x
Reference in New Issue
Block a user