validators.handler.js 17 KB

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