validators.handler.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. const { isAfter, subDays, subHours, addMilliseconds, differenceInHours } = require("date-fns");
  2. const { body, param, query: queryValidator } = require("express-validator");
  3. const promisify = require("util").promisify;
  4. const bcrypt = require("bcryptjs");
  5. const dns = require("dns");
  6. const URL = require("url");
  7. const ms = require("ms");
  8. const { ROLES } = require("../consts");
  9. const query = require("../queries");
  10. const utils = require("../utils");
  11. const knex = require("../knex");
  12. const env = require("../env");
  13. const dnsLookup = promisify(dns.lookup);
  14. const checkUser = (value, { req }) => !!req.user;
  15. const sanitizeCheckbox = value => value === true || value === "on" || value;
  16. const createLink = [
  17. body("target")
  18. .exists({ checkNull: true, checkFalsy: true })
  19. .withMessage("Target is missing.")
  20. .isString()
  21. .trim()
  22. .isLength({ min: 1, max: 2040 })
  23. .withMessage("Maximum URL length is 2040.")
  24. .customSanitizer(utils.addProtocol)
  25. .custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\w+:|\/\/)/.test(value))
  26. .withMessage("URL is not valid.")
  27. .custom(value => utils.removeWww(URL.parse(value).host) !== env.DEFAULT_DOMAIN)
  28. .withMessage(`${env.DEFAULT_DOMAIN} URLs are not allowed.`),
  29. body("password")
  30. .optional({ nullable: true, checkFalsy: true })
  31. .custom(checkUser)
  32. .withMessage("Only users can use this field.")
  33. .isString()
  34. .isLength({ min: 3, max: 64 })
  35. .withMessage("Password length must be between 3 and 64."),
  36. body("customurl")
  37. .optional({ nullable: true, checkFalsy: true })
  38. .custom(checkUser)
  39. .withMessage("Only users can use this field.")
  40. .isString()
  41. .trim()
  42. .isLength({ min: 1, max: 64 })
  43. .withMessage("Custom URL length must be between 1 and 64.")
  44. .custom(value => /^[a-zA-Z0-9-_]+$/g.test(value))
  45. .withMessage("Custom URL is not valid.")
  46. .custom(value => !utils.preservedURLs.some(url => url.toLowerCase() === value))
  47. .withMessage("You can't use this custom URL."),
  48. body("reuse")
  49. .optional({ nullable: true })
  50. .custom(checkUser)
  51. .withMessage("Only users can use this field.")
  52. .isBoolean()
  53. .withMessage("Reuse must be boolean."),
  54. body("description")
  55. .optional({ nullable: true, checkFalsy: true })
  56. .isString()
  57. .trim()
  58. .isLength({ min: 1, max: 2040 })
  59. .withMessage("Description length must be between 1 and 2040."),
  60. body("expire_in")
  61. .optional({ nullable: true, checkFalsy: true })
  62. .isString()
  63. .trim()
  64. .custom(value => {
  65. try {
  66. return !!ms(value);
  67. } catch {
  68. return false;
  69. }
  70. })
  71. .withMessage("Expire format is invalid. Valid examples: 1m, 8h, 42 days.")
  72. .customSanitizer(ms)
  73. .custom(value => value >= ms("1m"))
  74. .withMessage("Expire time should be more than 1 minute.")
  75. .customSanitizer(value => utils.dateToUTC(addMilliseconds(new Date(), value))),
  76. body("domain")
  77. .optional({ nullable: true, checkFalsy: true })
  78. .customSanitizer(value => value === env.DEFAULT_DOMAIN ? null : value)
  79. .custom(checkUser)
  80. .withMessage("Only users can use this field.")
  81. .isString()
  82. .withMessage("Domain should be string.")
  83. .customSanitizer(value => value.toLowerCase())
  84. .custom(async (address, { req }) => {
  85. const domain = await query.domain.find({
  86. address,
  87. user_id: req.user.id
  88. });
  89. req.body.fetched_domain = domain || null;
  90. if (!domain) return Promise.reject();
  91. })
  92. .withMessage("You can't use this domain.")
  93. ];
  94. const editLink = [
  95. body("target")
  96. .optional({ checkFalsy: true, nullable: true })
  97. .isString()
  98. .trim()
  99. .isLength({ min: 1, max: 2040 })
  100. .withMessage("Maximum URL length is 2040.")
  101. .customSanitizer(utils.addProtocol)
  102. .custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\w+:|\/\/)/.test(value))
  103. .withMessage("URL is not valid.")
  104. .custom(value => utils.removeWww(URL.parse(value).host) !== env.DEFAULT_DOMAIN)
  105. .withMessage(`${env.DEFAULT_DOMAIN} URLs are not allowed.`),
  106. body("password")
  107. .optional({ nullable: true, checkFalsy: true })
  108. .isString()
  109. .isLength({ min: 3, max: 64 })
  110. .withMessage("Password length must be between 3 and 64."),
  111. body("address")
  112. .optional({ checkFalsy: true, nullable: true })
  113. .isString()
  114. .trim()
  115. .isLength({ min: 1, max: 64 })
  116. .withMessage("Custom URL length must be between 1 and 64.")
  117. .custom(value => /^[a-zA-Z0-9-_]+$/g.test(value))
  118. .withMessage("Custom URL is not valid")
  119. .custom(value => !utils.preservedURLs.some(url => url.toLowerCase() === value))
  120. .withMessage("You can't use this custom URL."),
  121. body("expire_in")
  122. .optional({ nullable: true, checkFalsy: true })
  123. .isString()
  124. .trim()
  125. .custom(value => {
  126. try {
  127. return !!ms(value);
  128. } catch {
  129. return false;
  130. }
  131. })
  132. .withMessage("Expire format is invalid. Valid examples: 1m, 8h, 42 days.")
  133. .customSanitizer(ms)
  134. .custom(value => value >= ms("1m"))
  135. .withMessage("Expire time should be more than 1 minute.")
  136. .customSanitizer(value => utils.dateToUTC(addMilliseconds(new Date(), value))),
  137. body("description")
  138. .optional({ nullable: true, checkFalsy: true })
  139. .isString()
  140. .trim()
  141. .isLength({ min: 0, max: 2040 })
  142. .withMessage("Description length must be between 0 and 2040."),
  143. param("id", "ID is invalid.")
  144. .exists({ checkFalsy: true, checkNull: true })
  145. .isLength({ min: 36, max: 36 })
  146. ];
  147. const redirectProtected = [
  148. body("password", "Password is invalid.")
  149. .exists({ checkFalsy: true, checkNull: true })
  150. .isString()
  151. .isLength({ min: 3, max: 64 })
  152. .withMessage("Password length must be between 3 and 64."),
  153. param("id", "ID is invalid.")
  154. .exists({ checkFalsy: true, checkNull: true })
  155. .isLength({ min: 36, max: 36 })
  156. ];
  157. const addDomain = [
  158. body("address", "Domain is not valid.")
  159. .exists({ checkFalsy: true, checkNull: true })
  160. .isLength({ min: 3, max: 64 })
  161. .withMessage("Domain length must be between 3 and 64.")
  162. .trim()
  163. .customSanitizer(utils.addProtocol)
  164. .custom(value => utils.urlRegex.test(value))
  165. .customSanitizer(value => {
  166. const parsed = URL.parse(value);
  167. return utils.removeWww(parsed.hostname || parsed.href);
  168. })
  169. .custom(value => value !== env.DEFAULT_DOMAIN)
  170. .withMessage("You can't use the default domain.")
  171. .custom(async value => {
  172. const domain = await query.domain.find({ address: value });
  173. if (domain?.user_id || domain?.banned) return Promise.reject();
  174. })
  175. .withMessage("You can't add this domain."),
  176. body("homepage")
  177. .optional({ checkFalsy: true, nullable: true })
  178. .customSanitizer(utils.addProtocol)
  179. .custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\w+:|\/\/)/.test(value))
  180. .withMessage("Homepage is not valid.")
  181. ];
  182. const addDomainAdmin = [
  183. body("address", "Domain is not valid.")
  184. .exists({ checkFalsy: true, checkNull: true })
  185. .isLength({ min: 3, max: 64 })
  186. .withMessage("Domain length must be between 3 and 64.")
  187. .trim()
  188. .customSanitizer(utils.addProtocol)
  189. .custom(value => utils.urlRegex.test(value))
  190. .customSanitizer(value => {
  191. const parsed = URL.parse(value);
  192. return utils.removeWww(parsed.hostname || parsed.href);
  193. })
  194. .custom(value => value !== env.DEFAULT_DOMAIN)
  195. .withMessage("You can't add the default domain.")
  196. .custom(async value => {
  197. const domain = await query.domain.find({ address: value });
  198. if (domain) return Promise.reject();
  199. })
  200. .withMessage("Domain already exists."),
  201. body("homepage")
  202. .optional({ checkFalsy: true, nullable: true })
  203. .customSanitizer(utils.addProtocol)
  204. .custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\w+:|\/\/)/.test(value))
  205. .withMessage("Homepage is not valid."),
  206. body("banned")
  207. .optional({ nullable: true })
  208. .customSanitizer(sanitizeCheckbox)
  209. .isBoolean(),
  210. ]
  211. const removeDomain = [
  212. param("id", "ID is invalid.")
  213. .exists({
  214. checkFalsy: true,
  215. checkNull: true
  216. })
  217. .isLength({ min: 36, max: 36 })
  218. ];
  219. const removeDomainAdmin = [
  220. param("id", "ID is invalid.")
  221. .exists({
  222. checkFalsy: true,
  223. checkNull: true
  224. })
  225. .isNumeric(),
  226. queryValidator("links")
  227. .optional({ nullable: true })
  228. .customSanitizer(sanitizeCheckbox)
  229. .isBoolean(),
  230. ];
  231. const deleteLink = [
  232. param("id", "ID is invalid.")
  233. .exists({
  234. checkFalsy: true,
  235. checkNull: true
  236. })
  237. .isLength({ min: 36, max: 36 })
  238. ];
  239. const reportLink = [
  240. body("link", "No link has been provided.")
  241. .exists({
  242. checkFalsy: true,
  243. checkNull: true
  244. })
  245. .customSanitizer(utils.addProtocol)
  246. .custom(
  247. value => utils.removeWww(URL.parse(value).host) === env.DEFAULT_DOMAIN
  248. )
  249. .withMessage(`You can only report a ${env.DEFAULT_DOMAIN} link.`)
  250. ];
  251. const banLink = [
  252. param("id", "ID is invalid.")
  253. .exists({
  254. checkFalsy: true,
  255. checkNull: true
  256. })
  257. .isLength({ min: 36, max: 36 }),
  258. body("host", '"host" should be a boolean.')
  259. .optional({
  260. nullable: true
  261. })
  262. .customSanitizer(sanitizeCheckbox)
  263. .isBoolean(),
  264. body("user", '"user" should be a boolean.')
  265. .optional({
  266. nullable: true
  267. })
  268. .customSanitizer(sanitizeCheckbox)
  269. .isBoolean(),
  270. body("userLinks", '"userLinks" should be a boolean.')
  271. .optional({
  272. nullable: true
  273. })
  274. .customSanitizer(sanitizeCheckbox)
  275. .isBoolean(),
  276. body("domain", '"domain" should be a boolean.')
  277. .optional({
  278. nullable: true
  279. })
  280. .customSanitizer(sanitizeCheckbox)
  281. .isBoolean()
  282. ];
  283. const banUser = [
  284. param("id", "ID is invalid.")
  285. .exists({
  286. checkFalsy: true,
  287. checkNull: true
  288. })
  289. .isNumeric(),
  290. body("links", '"links" should be a boolean.')
  291. .optional({
  292. nullable: true
  293. })
  294. .customSanitizer(sanitizeCheckbox)
  295. .isBoolean(),
  296. body("domains", '"domains" should be a boolean.')
  297. .optional({
  298. nullable: true
  299. })
  300. .customSanitizer(sanitizeCheckbox)
  301. .isBoolean()
  302. ];
  303. const banDomain = [
  304. param("id", "ID is invalid.")
  305. .exists({
  306. checkFalsy: true,
  307. checkNull: true
  308. })
  309. .isNumeric(),
  310. body("links", '"links" should be a boolean.')
  311. .optional({
  312. nullable: true
  313. })
  314. .customSanitizer(sanitizeCheckbox)
  315. .isBoolean(),
  316. body("domains", '"domains" should be a boolean.')
  317. .optional({
  318. nullable: true
  319. })
  320. .customSanitizer(sanitizeCheckbox)
  321. .isBoolean()
  322. ];
  323. const createUser = [
  324. body("password", "Password is not valid.")
  325. .exists({ checkFalsy: true, checkNull: true })
  326. .isLength({ min: 8, max: 64 })
  327. .withMessage("Password length must be between 8 and 64."),
  328. body("email", "Email is not valid.")
  329. .exists({ checkFalsy: true, checkNull: true })
  330. .trim()
  331. .isLength({ min: 1, max: 255 })
  332. .withMessage("Email length must be max 255.")
  333. .isEmail()
  334. .custom(async (value, { req }) => {
  335. const user = await query.user.find({ email: value });
  336. if (user)
  337. return Promise.reject();
  338. })
  339. .withMessage("User already exists."),
  340. body("role", "Role is not valid.")
  341. .optional({ nullable: true, checkFalsy: true })
  342. .trim()
  343. .isIn([ROLES.USER, ROLES.ADMIN]),
  344. body("verified")
  345. .optional({ nullable: true })
  346. .customSanitizer(sanitizeCheckbox)
  347. .isBoolean(),
  348. body("banned")
  349. .optional({ nullable: true })
  350. .customSanitizer(sanitizeCheckbox)
  351. .isBoolean(),
  352. body("verification_email")
  353. .optional({ nullable: true })
  354. .customSanitizer(sanitizeCheckbox)
  355. .isBoolean(),
  356. ];
  357. const getStats = [
  358. param("id", "ID is invalid.")
  359. .exists({
  360. checkFalsy: true,
  361. checkNull: true
  362. })
  363. .isLength({ min: 36, max: 36 })
  364. ];
  365. const signup = [
  366. body("password", "Password is not valid.")
  367. .exists({ checkFalsy: true, checkNull: true })
  368. .isLength({ min: 8, max: 64 })
  369. .withMessage("Password length must be between 8 and 64."),
  370. body("email", "Email is not valid.")
  371. .exists({ checkFalsy: true, checkNull: true })
  372. .trim()
  373. .isLength({ min: 0, max: 255 })
  374. .withMessage("Email length must be max 255.")
  375. .isEmail()
  376. ];
  377. const signupEmailTaken = [
  378. body("email", "Email is not valid.")
  379. .custom(async (value, { req }) => {
  380. const user = await query.user.find({ email: value });
  381. if (user) {
  382. req.user = user;
  383. }
  384. if (user?.verified) {
  385. return Promise.reject();
  386. }
  387. })
  388. .withMessage("You can't use this email address.")
  389. ];
  390. const login = [
  391. body("password", "Password is not valid.")
  392. .exists({ checkFalsy: true, checkNull: true })
  393. .isLength({ min: 8, max: 64 })
  394. .withMessage("Password length must be between 8 and 64."),
  395. body("email", "Email is not valid.")
  396. .exists({ checkFalsy: true, checkNull: true })
  397. .trim()
  398. .isLength({ min: 1, max: 255 })
  399. .withMessage("Email length must be max 255.")
  400. .isEmail()
  401. ];
  402. const createAdmin = [
  403. body("password", "Password is not valid.")
  404. .exists({ checkFalsy: true, checkNull: true })
  405. .isLength({ min: 8, max: 64 })
  406. .withMessage("Password length must be between 8 and 64."),
  407. body("email", "Email is not valid.")
  408. .exists({ checkFalsy: true, checkNull: true })
  409. .trim()
  410. .isLength({ min: 0, max: 255 })
  411. .withMessage("Email length must be max 255.")
  412. .isEmail()
  413. ];
  414. const changePassword = [
  415. body("currentpassword", "Password is not valid.")
  416. .exists({ checkFalsy: true, checkNull: true })
  417. .isLength({ min: 8, max: 64 })
  418. .withMessage("Password length must be between 8 and 64."),
  419. body("newpassword", "Password is not valid.")
  420. .exists({ checkFalsy: true, checkNull: true })
  421. .isLength({ min: 8, max: 64 })
  422. .withMessage("Password length must be between 8 and 64.")
  423. ];
  424. const changeEmail = [
  425. body("password", "Password is not valid.")
  426. .exists({ checkFalsy: true, checkNull: true })
  427. .isLength({ min: 8, max: 64 })
  428. .withMessage("Password length must be between 8 and 64."),
  429. body("email", "Email address is not valid.")
  430. .exists({ checkFalsy: true, checkNull: true })
  431. .trim()
  432. .isLength({ min: 1, max: 255 })
  433. .withMessage("Email length must be max 255.")
  434. .isEmail()
  435. ];
  436. const resetPassword = [
  437. body("email", "Email is not valid.")
  438. .exists({ checkFalsy: true, checkNull: true })
  439. .trim()
  440. .isLength({ min: 0, max: 255 })
  441. .withMessage("Email length must be max 255.")
  442. .isEmail()
  443. ];
  444. const newPassword = [
  445. body("reset_password_token", "Reset password token is invalid.")
  446. .exists({ checkFalsy: true, checkNull: true })
  447. .isLength({ min: 36, max: 36 }),
  448. body("new_password", "Password is not valid.")
  449. .exists({ checkFalsy: true, checkNull: true })
  450. .isLength({ min: 8, max: 64 })
  451. .withMessage("Password length must be between 8 and 64."),
  452. body("repeat_password", "Password is not valid.")
  453. .custom((repeat_password, { req }) => {
  454. return repeat_password === req.body.new_password;
  455. })
  456. .withMessage("Passwords don't match."),
  457. ];
  458. const deleteUser = [
  459. body("password", "Password is not valid.")
  460. .exists({ checkFalsy: true, checkNull: true })
  461. .isLength({ min: 8, max: 64 })
  462. .custom(async (password, { req }) => {
  463. const isMatch = await bcrypt.compare(password, req.user.password);
  464. if (!isMatch) return Promise.reject();
  465. })
  466. .withMessage("Password is not correct.")
  467. ];
  468. const deleteUserByAdmin = [
  469. param("id", "ID is invalid.")
  470. .exists({ checkFalsy: true, checkNull: true })
  471. .isNumeric()
  472. ];
  473. async function bannedDomain(domain) {
  474. const isBanned = await query.domain.find({
  475. address: domain,
  476. banned: true
  477. });
  478. if (isBanned) {
  479. throw new utils.CustomError("Domain is banned.", 400);
  480. }
  481. };
  482. async function bannedHost(domain) {
  483. let isBanned;
  484. try {
  485. const dnsRes = await dnsLookup(domain);
  486. if (!dnsRes || !dnsRes.address) return;
  487. isBanned = await query.host.find({
  488. address: dnsRes.address,
  489. banned: true
  490. });
  491. } catch (error) {
  492. isBanned = null;
  493. }
  494. if (isBanned) {
  495. throw new utils.CustomError("URL is containing malware/scam.", 400);
  496. }
  497. };
  498. module.exports = {
  499. addDomain,
  500. addDomainAdmin,
  501. banDomain,
  502. banLink,
  503. banUser,
  504. bannedDomain,
  505. bannedHost,
  506. changeEmail,
  507. changePassword,
  508. checkUser,
  509. createAdmin,
  510. createLink,
  511. createUser,
  512. deleteLink,
  513. deleteUser,
  514. deleteUserByAdmin,
  515. editLink,
  516. getStats,
  517. login,
  518. newPassword,
  519. redirectProtected,
  520. removeDomain,
  521. removeDomainAdmin,
  522. reportLink,
  523. resetPassword,
  524. signup,
  525. signupEmailTaken,
  526. }