diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..b892d62 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "allowImportingTsExtensions": false + }, + "exclude": [ + "node_modules", + "**/node_modules/*" + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eb16264..3b64f1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 52a967c..abbfbf8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/env.js b/server/env.js index fa06040..8383dd7 100644 --- a/server/env.js +++ b/server/env.js @@ -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: "" }), diff --git a/server/handlers/auth.handler.js b/server/handlers/auth.handler.js index 43b84bd..2135664 100644 --- a/server/handlers/auth.handler.js +++ b/server/handlers/auth.handler.js @@ -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; } @@ -305,8 +306,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({ @@ -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, diff --git a/server/handlers/domains.handler.js b/server/handlers/domains.handler.js index 12bfa2f..359f538 100644 --- a/server/handlers/domains.handler.js +++ b/server/handlers/domains.handler.js @@ -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"); diff --git a/server/handlers/helpers.handler.js b/server/handlers/helpers.handler.js index 7cb37b0..0919e7b 100644 --- a/server/handlers/helpers.handler.js +++ b/server/handlers/helpers.handler.js @@ -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, } \ No newline at end of file diff --git a/server/handlers/links.handler.js b/server/handlers/links.handler.js index a0e8c13..59702ec 100644 --- a/server/handlers/links.handler.js +++ b/server/handlers/links.handler.js @@ -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) { - delete req.body[name]; - return; + 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 }) } ); diff --git a/server/handlers/locals.handler.js b/server/handlers/locals.handler.js index 1f95bdc..0a5e10b 100644 --- a/server/handlers/locals.handler.js +++ b/server/handlers/locals.handler.js @@ -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, } \ No newline at end of file diff --git a/server/handlers/renders.handler.js b/server/handlers/renders.handler.js index 04897a3..b569719 100644 --- a/server/handlers/renders.handler.js +++ b/server/handlers/renders.handler.js @@ -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)), }); } diff --git a/server/handlers/users.handler.js b/server/handlers/users.handler.js index d5f45f1..3bd4a69 100644 --- a/server/handlers/users.handler.js +++ b/server/handlers/users.handler.js @@ -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..." diff --git a/server/handlers/validators.handler.js b/server/handlers/validators.handler.js index 1e1c113..f30fc50 100644 --- a/server/handlers/validators.handler.js +++ b/server/handlers/validators.handler.js @@ -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,31 +362,34 @@ 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}`, { - client: { - clientId: env.DEFAULT_DOMAIN.toLowerCase().replace(".", ""), - clientVersion: "1.0.0" - }, - threatInfo: { - threatTypes: [ - "THREAT_TYPE_UNSPECIFIED", - "MALWARE", - "SOCIAL_ENGINEERING", - "UNWANTED_SOFTWARE", - "POTENTIALLY_HARMFUL_APPLICATION" - ], - platformTypes: ["ANY_PLATFORM", "PLATFORM_TYPE_UNSPECIFIED"], - threatEntryTypes: [ - "EXECUTABLE", - "URL", - "THREAT_ENTRY_TYPE_UNSPECIFIED" - ], - threatEntries: [{ url: target }] - } + method: "post", + body: JSON.stringify({ + client: { + clientId: env.DEFAULT_DOMAIN.toLowerCase().replace(".", ""), + clientVersion: "1.0.0" + }, + threatInfo: { + threatTypes: [ + "THREAT_TYPE_UNSPECIFIED", + "MALWARE", + "SOCIAL_ENGINEERING", + "UNWANTED_SOFTWARE", + "POTENTIALLY_HARMFUL_APPLICATION" + ], + platformTypes: ["ANY_PLATFORM", "PLATFORM_TYPE_UNSPECIFIED"], + threatEntryTypes: [ + "EXECUTABLE", + "URL", + "THREAT_ENTRY_TYPE_UNSPECIFIED" + ], + threatEntries: [{ url: target }] + } + }) } - ); + ).then(res => res.json()); if (!isMalware.data || !isMalware.data.matches) return; if (user) { diff --git a/server/mail/mail.js b/server/mail/mail.js index 038f6fa..015a158 100644 --- a/server/mail/mail.js +++ b/server/mail/mail.js @@ -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) diff --git a/server/queries/link.queries.js b/server/queries/link.queries.js index 427506b..5f54eba 100644 --- a/server/queries/link.queries.js +++ b/server/queries/link.queries.js @@ -1,6 +1,5 @@ const bcrypt = require("bcryptjs"); -// FIXME: circular dependency const CustomError = require("../utils").CustomError; const redis = require("../redis"); const knex = require("../knex"); diff --git a/server/queries/visit.queries.js b/server/queries/visit.queries.js index fe261b6..d126ec1 100644 --- a/server/queries/visit.queries.js +++ b/server/queries/visit.queries.js @@ -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: { diff --git a/server/routes/renders.routes.js b/server/routes/renders.routes.js index 22c37a7..aae0bbe 100644 --- a/server/routes/renders.routes.js +++ b/server/routes/renders.routes.js @@ -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) ); diff --git a/server/routes/routes.js b/server/routes/routes.js index 531303d..fe07c73 100644 --- a/server/routes/routes.js +++ b/server/routes/routes.js @@ -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); diff --git a/server/server.js b/server/server.js index 790f010..f80a638 100644 --- a/server/server.js +++ b/server/server.js @@ -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, () => { diff --git a/server/utils/utils.js b/server/utils/utils.js index cfc554c..a3690d6 100644 --- a/server/utils/utils.js +++ b/server/utils/utils.js @@ -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, diff --git a/server/views/partials/header.hbs b/server/views/partials/header.hbs index 6e2de86..55f2899 100644 --- a/server/views/partials/header.hbs +++ b/server/views/partials/header.hbs @@ -2,8 +2,7 @@