Compare commits
675 Commits
Author | SHA1 | Date |
---|---|---|
Pitu | 9367eb5eb8 | |
Pitu | 6cf87b95ec | |
Andy Chan | fcd9b7797f | |
Pitu | f8d763dca3 | |
Brandon Dusseau | 556dfbd5f5 | |
Jason | 25897ba6d3 | |
Kana | 58864852d1 | |
Kana | f262fa8069 | |
Pitu | ff046169bf | |
Pitu | 63016a5b74 | |
Zephyrrus | a8b985240d | |
Kana | 0d36d03db6 | |
Devices | 45ae83d60c | |
Pitu | c9de92cc7f | |
Pitu | a4c7dc5cf3 | |
Pitu | 2efd58f1a4 | |
Pitu | c416e2a0bf | |
Pitu | d9e0537a1d | |
Pitu | 1c463ea81e | |
Pitu | 270b7acd4c | |
Pitu | a4c447bb8b | |
Pitu | 43ce7c7c32 | |
Pitu | 0917f6f91f | |
Pitu | aff22be1b3 | |
Pitu | 740666012c | |
Kana | 065c5221a0 | |
Pitu | 0ec31e2371 | |
Zephyrrus | 2b5857182f | |
Pitu | acc7da5310 | |
Pitu | 9a0e6f9640 | |
Pitu | 142028a058 | |
Pitu | 940ab07d35 | |
Pitu | f42c75c7f4 | |
Pitu | d4390cce40 | |
Pitu | c131c3a1fc | |
Pitu | ce893ebc15 | |
Pitu | ed9fa0fa72 | |
Zephyrrus | 0cae7e9eda | |
Zephyrrus | 6fe5055e9d | |
Pitu | 30808a3574 | |
Pitu | ff343727d2 | |
Pitu | 50d13e2ae7 | |
Pitu | 334c3d1c34 | |
Pitu | b2253c7f60 | |
Pitu | f45cb197e4 | |
Pitu | d3c80127ec | |
Pitu | 9b28e56e09 | |
Pitu | 3358cf6939 | |
Pitu | 44974f937a | |
Kana | 987078b9af | |
Pitu | 5f5716963d | |
Jeremy | 067140cf65 | |
iCrawl | 44cdb2a51e | |
Noel | cb76049e02 | |
Kana | 2bd32f927b | |
Sergi Ibànyez Peña | 2267e3ea55 | |
Kana | fd0a106cea | |
Pitu | 3f223a9dbf | |
Pitu | abd7a1ca2e | |
Pitu | ab211c8a9e | |
Pitu | c8456954b2 | |
Pitu | 541bfedb92 | |
Pitu | b24c0175f5 | |
Kana | 771d8494ef | |
Kana | 96ffed5cca | |
Shubham Parihar | 602f1572f4 | |
Pitu | 52a91f796e | |
Pitu | 6199cc0cb6 | |
Pitu | ea0d8aafcf | |
Pitu | e09fcf70d8 | |
Kana | 9200604adb | |
iCrawl | f7bcd6718d | |
Pitu | bbce774d6c | |
Pitu | 58e900b5ac | |
Pitu | 950a874acf | |
Pitu | ba409ef2fc | |
Pitu | f2ac2da642 | |
Kana | e779706ab7 | |
Pitu | 528425bcba | |
Pitu | 56f38f4494 | |
Pitu | a3bf693d30 | |
Pitu | 91a15f417e | |
Pitu | 68fcbcb545 | |
Pitu | eb82ecb077 | |
Pitu | 68fd5bd133 | |
Pitu | bf0b0f64bf | |
Kana | e1e1a8ba0c | |
Zephyrrus | cf513f2ebe | |
Kana | 38021d9599 | |
iCrawl | 63509cd2d3 | |
Zephyrrus | dc4f3a6557 | |
Zephyrrus | d69fcd856a | |
Zephyrrus | 46ef63fb9f | |
Zephyrrus | 3c303241e1 | |
Kana | 01e17ed856 | |
Zephyrrus | e962efd014 | |
Pitu | 81b722452c | |
Pitu | 0d18e7d2a2 | |
Pitu | 0484ea74a2 | |
Pitu | 523359ec32 | |
Pitu | 09c834e6ce | |
Kana | 3cfb2721ce | |
Kana | 0205300cb9 | |
Zephyrrus | b58e12cad8 | |
Zephyrrus | bf10242180 | |
Zephyrrus | 925080f6a0 | |
Zephyrrus | 34bd71948e | |
Zephyrrus | a838d024a7 | |
Zephyrrus | b3df1dd7a6 | |
Zephyrrus | d4b1550439 | |
Pitu | be6ce9ac5a | |
Pitu | 5823fa95cd | |
Pitu | f3dc0ffe75 | |
Zephyrrus | e0801f0c19 | |
Zephyrrus | f151a8ac3a | |
Zephyrrus | 9370c32182 | |
Zephyrrus | 53f5015c99 | |
Pitu | 46ec3f7168 | |
Pitu | 07a873c88b | |
Pitu | 9da45d6160 | |
Kana | f944469162 | |
Pitu | a0824b5a97 | |
Pitu | df4e4272f4 | |
Pitu | ac5b3f29fa | |
Pitu | 1eccbb3d30 | |
Pitu | 38ff50eb80 | |
Pitu | 28efb0d9ed | |
Pitu | d2bf8ce8c8 | |
Pitu | 153d764c8f | |
Pitu | 4bda02813f | |
Pitu | 92f38085b0 | |
Pitu | f125011413 | |
Pitu | cdeca073ba | |
Kana | 25723290b9 | |
Liam | 05137aad9c | |
Kana | 5e274c58ce | |
Liam | c2afd2c2dc | |
Liam | a8e37fa0c8 | |
Liam | 1516ec6281 | |
Pitu | f2e270d284 | |
Pitu | 5e219868b0 | |
Pitu | 3934621d25 | |
Pitu | d8af517adc | |
Pitu | fcd39dc550 | |
Pitu | f20963b4a1 | |
Pitu | 4ff204115c | |
Pitu | b77c0a57cc | |
Pitu | 7d5b3c4ac7 | |
Pitu | 72d3fad525 | |
Kana | 4334dda3f6 | |
Zephyrrus | 13058d99d6 | |
Pitu | edb3bed988 | |
Pitu | 24edf8f4fd | |
Pitu | b2ddfbc8a6 | |
Kana | 2737eb5c2c | |
iCrawl | 5d2d46d8dc | |
Kana | 35c14c2242 | |
Pitu | aa7d245317 | |
Pitu | e97fee4844 | |
Pitu | 68634418e1 | |
Pitu | 726f47f301 | |
Pitu | ec2f9e0d98 | |
Pitu | 7190e035b4 | |
Pitu | 5c2f6782dd | |
Pitu | 24647beb97 | |
Pitu | 4dfb087729 | |
Pitu | f73cde6bb5 | |
Pitu | 943a00827d | |
Pitu | 493e05df27 | |
Pitu | 047a6afce6 | |
Pitu | 3051fbe948 | |
Pitu | 09d8d02e6c | |
Pitu | 279cde7dd3 | |
Pitu | fb2c27086f | |
Kana | 2412a60bd4 | |
Zephyrrus | 587f7d69e8 | |
Zephyrrus | d2efb2707c | |
Zephyrrus | e6c3327b9c | |
Zephyrrus | 90001c2df5 | |
Zephyrrus | 13825ddae6 | |
Zephyrrus | d6813fa912 | |
Zephyrrus | 20ee770fd6 | |
Zephyrrus | 4a3fef2b99 | |
Zephyrrus | 443e63d05a | |
Zephyrrus | 151c360740 | |
Zephyrrus | c88d08330f | |
Zephyrrus | 39e9941ded | |
Zephyrrus | 279234a081 | |
Zephyrrus | 01c04298b0 | |
Zephyrrus | 1a4744de3c | |
Zephyrrus | 78c6fa14e6 | |
Zephyrrus | 93478a334b | |
Zephyrrus | 18bb451f79 | |
Zephyrrus | fe314a742f | |
Zephyrrus | 9de50b26f1 | |
Zephyrrus | c5b165b495 | |
Zephyrrus | 6fee07d9e1 | |
Zephyrrus | 04660dbce1 | |
Pitu | a891cbc1fc | |
Zephyrrus | 645b62b81d | |
Zephyrrus | c93ddb0900 | |
Zephyrrus | b49017aafd | |
Zephyrrus | 165731fae8 | |
Zephyrrus | ef255587b1 | |
Pitu | bf63bc5e2e | |
Pitu | 37a5a8cce2 | |
Pitu | 0b6867f1b1 | |
Pitu | aea442a956 | |
Pitu | d3b7321bf6 | |
Pitu | 91507249c0 | |
Pitu | 8e3c3841a4 | |
Pitu | 898a2dde78 | |
Pitu | d644b21d43 | |
Pitu | 609ff7ceb4 | |
Pitu | 5e07436f0d | |
Pitu | 8ffa0ba075 | |
Pitu | ec51cc803e | |
Pitu | b70a75da1a | |
Pitu | 6dd7500084 | |
Pitu | a057f26896 | |
Pitu | 407fb8bcc3 | |
Pitu | 5f58431409 | |
Pitu | 4dafc79cb7 | |
Pitu | 2d06d918a1 | |
Zephyrrus | a721681944 | |
Zephyrrus | 0f66d80703 | |
Zephyrrus | 7e78a03931 | |
Zephyrrus | fd3f6de51a | |
Zephyrrus | da703de1d0 | |
Zephyrrus | 5ded974ef9 | |
Zephyrrus | 704578e964 | |
Zephyrrus | 746a454612 | |
Zephyrrus | 495a23c3a5 | |
Zephyrrus | 6713eca9d4 | |
Zephyrrus | ad852de51a | |
Zephyrrus | b519b6ccb4 | |
Zephyrrus | 49d3e3b203 | |
Zephyrrus | 1526637881 | |
Zephyrrus | 1a8b6602e0 | |
Zephyrrus | eccbb1ca93 | |
Zephyrrus | 5d61b4d000 | |
Zephyrrus | fb0bc57542 | |
Zephyrrus | 15f296a780 | |
Zephyrrus | 766e74cc51 | |
Zephyrrus | 04fdd63cee | |
Zephyrrus | ba5a740885 | |
Zephyrrus | 1e1f3fbb27 | |
Zephyrrus | b4603fd64e | |
Zephyrrus | 39ed9d336e | |
Zephyrrus | ac1d6eec5b | |
Zephyrrus | 92be4504cc | |
Zephyrrus | 836a01327d | |
Zephyrrus | b620aa9815 | |
Zephyrrus | 7581d13d1c | |
Zephyrrus | 22f9eb4dff | |
Zephyrrus | c2dbbe6396 | |
Zephyrrus | dd46f79550 | |
Zephyrrus | 24157dd1b9 | |
Zephyrrus | a790d7749e | |
Zephyrrus | 42f1a1003a | |
Zephyrrus | 720ffaf008 | |
Zephyrrus | f0753e1551 | |
Zephyrrus | 1576a67bc6 | |
Zephyrrus | 6688587eb6 | |
Zephyrrus | e9ef148d74 | |
Zephyrrus | 261d0f4781 | |
Zephyrrus | 44e8175e96 | |
Zephyrrus | 8416dc81a3 | |
Zephyrrus | 3e1677c18a | |
Zephyrrus | 520062508c | |
Zephyrrus | c8ad345d12 | |
Zephyrrus | 20a782fefe | |
Zephyrrus | 7e4dbdbd2b | |
Zephyrrus | ac037c773e | |
Zephyrrus | 27b0fdafb2 | |
Zephyrrus | 50d233cd66 | |
Zephyrrus | 0a50ef771e | |
Zephyrrus | 048e5d9325 | |
Zephyrrus | 0e1ae73855 | |
Zephyrrus | 4df3976ded | |
Pitu | 695d9a74ef | |
Pitu | 04319acf20 | |
Pitu | 207fc916d9 | |
Pitu | d1340c26b5 | |
Pitu | f189ddf9e6 | |
Pitu | a9fe08f9e5 | |
Pitu | b526d88036 | |
Pitu | b8fc0cc858 | |
Pitu | e0c35a7d74 | |
Pitu | 496477ebda | |
Pitu | b886fda079 | |
Pitu | ec67bb8087 | |
Pitu | cd6170b939 | |
Pitu | 5df8485c5e | |
Pitu | 124ff68f06 | |
Pitu | b27b4c47f7 | |
Pitu | 1836c8c93a | |
Pitu | 4c52932426 | |
Pitu | d4ac722f58 | |
Pitu | aadb01bcff | |
Pitu | 6da29eb7c1 | |
Pitu | 432d86022c | |
Pitu | 97243c7087 | |
Pitu | de54e19d3a | |
Pitu | a639b85734 | |
Pitu | c114e59be3 | |
Pitu | d63f1f57e9 | |
Pitu | bf37e04cb6 | |
Pitu | 759832cd78 | |
Pitu | 05c129ec32 | |
Pitu | 3fb303380e | |
Pitu | a957713e81 | |
Kana | 78c5b73b70 | |
Kana | be3f3c7d79 | |
Pitu | cba7bf8586 | |
Pitu | 4e4b4ea468 | |
Pitu | bca8fbcd83 | |
Pitu | 5ca3c35381 | |
Pitu | 1177de9b04 | |
Pitu | 43eb86c77f | |
Pitu | 4683848108 | |
Pitu | 2695d192ba | |
Pitu | e6eb13e5cd | |
Pitu | c385f8b30a | |
Pitu | 391ee68e4a | |
Pitu | 459ab5433b | |
Pitu | dd9c9ac11f | |
Pitu | c121bd42f3 | |
Pitu | 3680432bdf | |
Pitu | 579e1e754a | |
Pitu | a552aca8ab | |
Pitu | b1f56ef9b9 | |
Pitu | 4db167ec43 | |
Pitu | 0d36f0d69a | |
Pitu | 8e4f1b7838 | |
Kana | 84f1feff43 | |
dependabot[bot] | 0ba2a828a0 | |
Kana | 4d3b35072a | |
Kana | 9f037eb6d1 | |
Kana | 84a44c7743 | |
dependabot[bot] | 9c617c82b3 | |
dependabot[bot] | a8a7e06b63 | |
dependabot[bot] | 4617a6a60c | |
Kana | 60c9f36374 | |
dependabot[bot] | 677aa8f68e | |
Pitu | ca9e1c859d | |
Pitu | b5a04f21f9 | |
Pitu | c074b5e197 | |
Pitu | fec273b23b | |
Pitu | ac36cdc143 | |
Pitu | a7c1855ce5 | |
Pitu | ab0839f1f5 | |
Pitu | 3bd8d119ba | |
Pitu | 9609279554 | |
Pitu | d44154d379 | |
Pitu | 5dc7eda038 | |
Pitu | 4b0966f857 | |
Pitu | a248f69947 | |
Pitu | e9ce158e36 | |
Pitu | c4d803c4f6 | |
Pitu | 0f4d196c8c | |
Pitu | b12cc4c289 | |
Pitu | 9aba5cd221 | |
Pitu | 5df5751736 | |
Pitu | ea3e503d13 | |
Pitu | 8905f2e7a7 | |
Kana | fac19ac31d | |
Robin B | cff0ab7ccb | |
Pitu | 107d1f4750 | |
Pitu | 1790a84430 | |
Pitu | 19e79365ad | |
Pitu | d7745aff40 | |
Pitu | a142078f64 | |
Pitu | dfaeb1051f | |
Pitu | 897e22c936 | |
Pitu | 85ed48219e | |
Pitu | 088ffe175e | |
Pitu | f06c8c9d33 | |
Pitu | 497a961a38 | |
Pitu | 79eb00f71c | |
Pitu | af9d752eaf | |
Kana | 94bc88191e | |
Pitu | b1e7511593 | |
Pitu | e04833dff1 | |
Pitu | e7d27844d0 | |
Pitu | 455c9ee25c | |
Pitu | f11cb56db8 | |
Pitu | dc4e6bf907 | |
Pitu | 87ee815517 | |
Pitu | 771a5c2bf7 | |
Pitu | 4ab3796fbd | |
Pitu | 4277db90f6 | |
Pitu | 69dd014f49 | |
Pitu | 00058e9915 | |
Pitu | 197e69f2f2 | |
Pitu | 4a2ed9f0f4 | |
Pitu | 2f063735d7 | |
Pitu | dd98cecfeb | |
Pitu | 85ac744837 | |
Pitu | c6b0f62d13 | |
Pitu | 3653884560 | |
Pitu | 390eeb9b07 | |
Kana | 28bca3b195 | |
Pitu | 4d6cc7460d | |
Pitu | 6aee90e375 | |
Pitu | 71f2450431 | |
Kana | 77a5b856fe | |
Pitu | 3ce7657871 | |
Pitu | 99bc74875e | |
Pitu | 789f5fc259 | |
Pitu | 70301ca368 | |
Pitu | ce76a8ec7b | |
Pitu | 0639c4e1bb | |
Pitu | 9edc0d0508 | |
Pitu | 33dded09a7 | |
Pitu | d821ebf4ac | |
Pitu | 0ed4624dd0 | |
Pitu | 73d85e8c79 | |
Kana | e8f70f5170 | |
Kana | 04ee40e673 | |
Kana | 47ca404b6b | |
Pitu | 5a701536cf | |
Kana | 36b11f0027 | |
Pitu | a9e9373c9c | |
Kana | a86e9f8323 | |
Pitu | 9cba85c47c | |
Pitu | 9f5a3d15f5 | |
Pitu | c169ab6dc1 | |
Pitu | f37d206943 | |
Pitu | 7ad463469b | |
Pitu | 7a74647d3e | |
Pitu | 62c0c1db20 | |
Pitu | 6cd31674d5 | |
Pitu | ab66e095a8 | |
Pitu | 80732ff90a | |
Pitu | 8be134c8d8 | |
Pitu | ee601d3524 | |
Pitu | c8e0ebd8ff | |
Pitu | fc95cb7b0f | |
Kana | df90d3157a | |
Kana | 80a76d868f | |
Kana | 79bb01e720 | |
Kana | 9c81720c30 | |
Kana | 5b7dcc7576 | |
Kana | 165f891686 | |
Pitu | 500eb4f9a0 | |
Pitu | a284a9a064 | |
Pitu | 84c4b442cf | |
Pitu | c7a4a39de4 | |
Pitu | 352d84ea3a | |
Pitu | 48d1859723 | |
Pitu | 44e6fd31d2 | |
Pitu | 25c5a06ec3 | |
Kana | 3fe0b274b7 | |
Pitu | 55e6c1f9cd | |
Pitu | 1b3a3de252 | |
Pitu | 89a271818e | |
Kana | 2e0cbd3ea7 | |
Kana | 064d6a6be9 | |
Nathan DECHER | 69e3c6e5f5 | |
Pitu | a857b8910b | |
Pitu | 6fa1dbd99e | |
Pitu | e33cf30449 | |
Pitu | 63f327e49d | |
Pitu | 4c241dc1d1 | |
Kana | c9fd7acf39 | |
hyperdefined | cc8948eb7e | |
Kana | 5b697fd4e6 | |
Kana | b5e2f09a1d | |
Kana | 436cbe4009 | |
天使アシュリー | 11881780d7 | |
天使アシュリー | 87b7a2b50a | |
Kayo | fe1f0ed65e | |
Kana | 40b2e3b3c1 | |
Kana | 3d5d540c0e | |
Leo MG Nesfield (LMGN) | 06c69559f6 | |
Kana | bb722776c2 | |
Pascal Temel | cb320e4354 | |
Kana | ea575898f3 | |
Pascal Temel | fa8ef06764 | |
Pascal Temel | 05c17f2dc9 | |
Kana | ee6e9875db | |
Kana | 4e2eb60613 | |
Kana | cca2ec5ecf | |
Kana | 1ccc45c6ec | |
hyperdefined | 029736fe72 | |
hyperdefined | 0dd87f204f | |
Calvin_c64 | b3ec39dbe8 | |
Calvin_c64 | 40fa3bd5df | |
TRASHER | a1d5e43961 | |
Kana | 5fe57f6dd1 | |
Kana | b105ff273c | |
Kosemii | ec630b2c24 | |
Daniel Odendahl Jr | 69dfaa6c27 | |
Kana | 558abcca90 | |
Alpha | 304d8c5775 | |
Kana | b18828d814 | |
Kayo | 3e0be53c62 | |
Kana | de7e639c75 | |
Kana | dddeff0187 | |
Alpha | b353d009db | |
Pitu | 430af8306b | |
Pitu | 8e1711ed6c | |
Pitu | 04fb2218cd | |
Pitu | 108e5d5d2f | |
Pitu | c3fde6d5a6 | |
Pitu | 8ca6784eec | |
Pitu | b75023114a | |
Pitu | 4b2b02110b | |
Pitu | e8bb2c5a7f | |
Pitu | 1fe6f579f9 | |
Pitu | 41cb8ff986 | |
Pitu | f2c885b718 | |
Pitu | 9f03bc6d4a | |
Pitu | 455ca39886 | |
Pitu | c2c6e99878 | |
Pitu | 46ed1c6a82 | |
Pitu | 77dd0c70f7 | |
Pitu | 977d3c6d3a | |
Pitu | d777439c7b | |
Pitu | 7df56eb91c | |
Pitu | e073fb4317 | |
Pitu | 9001133414 | |
Pitu | 5560f69896 | |
Pitu | 22b511cf25 | |
Pitu | 072fec199d | |
Pitu | c7af18e730 | |
Pitu | 65a5967058 | |
Pitu | b0e5dd4539 | |
Pitu | 0dbc9ca7ba | |
Pitu | 37c7596ac3 | |
Pitu | 44e54187c6 | |
Pitu | 04cb6dcce5 | |
Pitu | fe10a00ba9 | |
Pitu | 3243d85b59 | |
Pitu | e7767ac709 | |
Pitu | a42cf4400e | |
Pitu | 7268d24143 | |
Pitu | 3f0bdd7a28 | |
Pitu | 868f4a64ec | |
Kana | 51e4d6182a | |
Lukas | 8427372059 | |
Kana | cc2318b22a | |
Uriel | 4e7a9b9fe6 | |
Kana | bb3761c23e | |
Kana | 0d40f21ce8 | |
Sanchez | 9a4047afa5 | |
Kosemii | 20984be41f | |
Kana | 6847cbb346 | |
Tony Langhammer | a6b0a827cf | |
Kana | a59396aa6d | |
hyperdefined | 073f14a7c0 | |
hyperdefined | d3b13af53b | |
Kana | 7ca08ceade | |
Brayden | ea65e4b5ea | |
Brayden | 3b75760886 | |
Kana | 78c184562a | |
Shumatsu | aa8266087f | |
Shumatsu | b078041503 | |
Shumatsu | 68032b62b9 | |
Kana | 7711f0d45a | |
hyperdefined | 39b3c69bd9 | |
hyperdefined | f4d6e4bd64 | |
Kana | 82e8e7167b | |
VVLNT | 5ca83af4a4 | |
Kana | c46f5d9b31 | |
Josh | 67d7bf18b3 | |
Kana | 210967396e | |
Uriel | 1bc9c1ed7f | |
Uriel | 953981f48d | |
Kana | cbe73ed1ec | |
Shumatsu | a9ac6b1574 | |
Shumatsu | df6d5459e0 | |
Kana | 41fd4fb81d | |
Kana | 3bb212cfda | |
Nadya | 55e64b1f36 | |
Kosemii | f28fcf4388 | |
Kana | 0994355c7e | |
Kana | 7655d87066 | |
Nadya | 18b5d8eed9 | |
Kosemii | e0911494e6 | |
Kana | 0f66d74683 | |
Nadya | 30e460b7f8 | |
Kana | e07e87e061 | |
Nadya | ba6b3764dc | |
Nadya | 8758ccac82 | |
Nadya | ef4e7755ae | |
Nadya | 92efc7bf17 | |
Kana | 7f488f0ac9 | |
RyoshiKayo | 94e24f3c5d | |
Kana | e6467ab4f1 | |
Kayo | b08cc2c6bb | |
Kana | 4d883de6ac | |
Kayo | c4993e5882 | |
Shumatsu | 44ca2dd53d | |
Kosinus | 717367320e | |
Bobby Wibowo | 08637b9ea9 | |
Bobby Wibowo | 19e965a77a | |
Bobby Wibowo | 5a4bec6b00 | |
Bobby Wibowo | fa6c33e2e9 | |
Kana | 9d17bb284a | |
Kana | 91331e7947 | |
Kana | 917cdabcb0 | |
Kayo | 5715a2d1f3 | |
Kayo | 35da812a01 | |
Kayo | d7c792fa8a | |
Kayo | 535f12b70a | |
Kayo | 6f7ec5d282 | |
Kana | 40bfa143c2 | |
Kayo | 46bb4079c6 | |
Kayo | a9d0e0a85c | |
Kana | 0157388217 | |
Bobby Wibowo | 5be27c129d | |
Bobby Wibowo | dcb72734fe | |
Kana | 46bf0da5ee | |
Unknown | 56e2f3ff5c | |
Unknown | ba8500144b | |
Kana | 496575dea0 | |
iCrawl | 8a75ab91a6 | |
Kana | 1a77649ce3 | |
pyra | b9cad8e4d5 | |
Kana | 939b5c52f7 | |
RyoshiKayo | d009c2dcf6 | |
RyoshiKayo | 47821474a5 | |
Kana | 48ec9d9559 | |
Kana | f1cc65a55e | |
RyoshiKayo | a9232b905c | |
RyoshiKayo | 01f1c600ed | |
RyoshiKayo | 9465cce88a | |
RyoshiKayo | 465607cd5b | |
Bobby Wibowo | 5052cd2651 | |
Bobby Wibowo | 7de25210ce | |
Bobby Wibowo | 38d77fdfbb | |
Bobby Wibowo | f42bd7d011 | |
Kana | 5db6a92334 | |
vistafan12 | e64417d0cb | |
Kana | ef153d0cc1 | |
Kana | 1c3ac828ed | |
EpikPhailure | 4ef19ed027 | |
Kana | 0a4729d2bd | |
Kana | eb01ab06a2 | |
ScruffyRules | 9c07dda317 | |
ScruffyRules | d367bc27fa | |
Kana | 48883d7728 | |
Pitu | 3c2ba4868a | |
Pitu | 759943f798 | |
Pitu | 76d48602c6 | |
Pitu | 09a5c6bf1e | |
Pitu | 149742ab61 | |
Pitu | 5ff33ba930 | |
Pitu | 992b632d1a | |
Pitu | 8d8dbc7078 | |
Pitu | e56ef97975 | |
Pitu | 54a262ef95 | |
Pitu | 4b0b015c41 | |
Pitu | f5d0d7271a | |
Pitu | 702075b66d | |
Pitu | 128b7113bf | |
Pitu | 07bc306506 | |
Pitu | 3c5303e505 | |
Pitu | c1963b2809 | |
Pitu | d1260563d4 | |
Pitu | 052440328e | |
Pitu | 6aa57285d4 | |
Kana | a2cd342720 | |
Kana | 84a8449d37 | |
Kel | 8427d23807 | |
Kana | 50abd8bdc5 | |
Kana | fffb317b9d | |
Kana | bdc14f9d3a | |
Kana | 9ab59d47de | |
Jason Etcovitch | 11cd00c804 | |
Kana | 1544f1ca76 | |
Pascal Temel | b05dac6743 | |
Pascal Temel | 4b63ea2d1b |
|
@ -0,0 +1,46 @@
|
|||
# Packages
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Log files
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
|
||||
# Docker (experimental)
|
||||
docker/
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
docker-compose.config.yml
|
||||
docker-compose.config.example.yml
|
||||
|
||||
# Tests
|
||||
coverage/
|
||||
jest-setup.ts
|
||||
jest.config.js
|
||||
|
||||
# Linting
|
||||
.eslingignore
|
||||
.eslintrc.json
|
||||
tsconfig.eslint.json
|
||||
|
||||
# Miscellaneous
|
||||
.tmp
|
||||
.vscode
|
||||
.git
|
||||
.gitattributes
|
||||
.gitignore
|
||||
README.md
|
||||
chibi.ps1
|
||||
chibi.sh
|
||||
dist
|
||||
.nuxt
|
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
130
.eslintrc.json
|
@ -1,130 +0,0 @@
|
|||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"plugins": [
|
||||
"html"
|
||||
],
|
||||
"settings": {
|
||||
"html/html-extensions": [".vue"]
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"no-extra-parens": ["warn", "all", { "nestedBinaryExpressions": false }],
|
||||
"accessor-pairs": "warn",
|
||||
"array-callback-return": "error",
|
||||
"complexity": "warn",
|
||||
"consistent-return": "error",
|
||||
"curly": ["error", "multi-line", "consistent"],
|
||||
"dot-location": ["error", "property"],
|
||||
"dot-notation": "error",
|
||||
"eqeqeq": "error",
|
||||
"no-empty-function": "error",
|
||||
"no-floating-decimal": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-invalid-this": "error",
|
||||
"no-lone-blocks": "error",
|
||||
"no-multi-spaces": "error",
|
||||
"no-new-func": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-new": "error",
|
||||
"no-octal-escape": "error",
|
||||
"no-return-assign": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-sequences": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-unmodified-loop-condition": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"no-useless-call": "error",
|
||||
"no-useless-concat": "error",
|
||||
"no-useless-escape": "error",
|
||||
"no-void": "error",
|
||||
"no-warning-comments": "warn",
|
||||
"wrap-iife": "error",
|
||||
"yoda": "error",
|
||||
"no-label-var": "error",
|
||||
"no-shadow": "error",
|
||||
"no-undef-init": "error",
|
||||
"handle-callback-err": "error",
|
||||
"no-mixed-requires": "error",
|
||||
"no-new-require": "error",
|
||||
"no-path-concat": "error",
|
||||
"array-bracket-spacing": "error",
|
||||
"block-spacing": "error",
|
||||
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
|
||||
"camelcase": "error",
|
||||
"comma-dangle": "error",
|
||||
"comma-spacing": "error",
|
||||
"comma-style": "error",
|
||||
"computed-property-spacing": "error",
|
||||
"consistent-this": "error",
|
||||
"eol-last": "error",
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"id-length": ["error", { "exceptions": ["i", "j", "a", "b", "_"] }],
|
||||
"indent": ["error", "tab", { "SwitchCase": 1 }],
|
||||
"key-spacing": "error",
|
||||
"keyword-spacing": ["error", {
|
||||
"overrides": {
|
||||
"if": { "after": true },
|
||||
"for": { "after": true },
|
||||
"while": { "after": true },
|
||||
"catch": { "after": true },
|
||||
"switch": { "after": true }
|
||||
}
|
||||
}],
|
||||
"max-depth": "error",
|
||||
"max-len": ["error", 120, 2],
|
||||
"max-nested-callbacks": ["error", { "max": 4 }],
|
||||
"max-statements-per-line": ["error", { "max": 2 }],
|
||||
"new-cap": "error",
|
||||
"newline-per-chained-call": ["error", { "ignoreChainWithDepth": 3 }],
|
||||
"no-array-constructor": "error",
|
||||
"no-bitwise": "warn",
|
||||
"no-inline-comments": "error",
|
||||
"no-lonely-if": "error",
|
||||
"no-mixed-operators": "error",
|
||||
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
|
||||
"no-new-object": "error",
|
||||
"no-spaced-func": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-whitespace-before-property": "error",
|
||||
"object-curly-newline": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"operator-assignment": "error",
|
||||
"operator-linebreak": ["error", "before"],
|
||||
"padded-blocks": ["error", "never"],
|
||||
"quote-props": ["error", "as-needed"],
|
||||
"quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
|
||||
"semi-spacing": "error",
|
||||
"semi": "off",
|
||||
"space-before-blocks": "error",
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"space-in-parens": "error",
|
||||
"space-infix-ops": "error",
|
||||
"space-unary-ops": "error",
|
||||
"spaced-comment": "error",
|
||||
"unicode-bom": "error",
|
||||
"arrow-spacing": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-useless-computed-key": "error",
|
||||
"no-useless-constructor": "error",
|
||||
"prefer-arrow-callback": "error",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error",
|
||||
"prefer-template": "error",
|
||||
"rest-spread-spacing": "error",
|
||||
"template-curly-spacing": "error",
|
||||
"yield-star-spacing": "error",
|
||||
"no-unused-vars": "warn"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
|
@ -0,0 +1,12 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: pitu
|
||||
patreon: pitu
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE REQUEST]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
|
@ -1,11 +1,16 @@
|
|||
.DS_Store
|
||||
!.gitkeep
|
||||
# Packages
|
||||
node_modules/
|
||||
uploads/
|
||||
dist/
|
||||
.nuxt/
|
||||
logs/
|
||||
database/db
|
||||
config.js
|
||||
start.json
|
||||
npm-debug.log
|
||||
pages/custom/**
|
||||
migrate.js
|
||||
# Chibisafe specifics
|
||||
database.sqlite
|
||||
uploads/
|
||||
.env
|
||||
!src/api/routes/uploads
|
||||
db
|
||||
database.sqlite-journal
|
||||
docker/nginx/chibisafe.moe.conf
|
||||
docker-compose.config.yml
|
||||
/coverage
|
||||
local/
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
audit=false
|
||||
fund=false
|
||||
node-version=false
|
||||
legacy-peer-deps=true
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "[DEV] Launch API",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}\\src\\api\\structures\\Server"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "[DEV] Launch ThumbGen",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}\\src\\api\\utils\\generateThumbs"
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "client: chrome",
|
||||
"url": "http://localhost:8070",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "server: nuxt",
|
||||
"args": ["dev"],
|
||||
"osx": {
|
||||
"program": "${workspaceFolder}/node_modules/.bin/nuxt"
|
||||
},
|
||||
"linux": {
|
||||
"program": "${workspaceFolder}/node_modules/.bin/nuxt"
|
||||
},
|
||||
"windows": {
|
||||
"program": "${workspaceFolder}/node_modules/nuxt/bin/nuxt.js"
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "fullstack: nuxt",
|
||||
"configurations": ["server: nuxt", "client: chrome"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"editor.detectIndentation": false,
|
||||
"editor.insertSpaces": false,
|
||||
"files.insertFinalNewline": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.formatOnSave": true,
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"prettier.disableLanguages": ["vue"],
|
||||
"vetur.format.enable": true,
|
||||
"files.eol": "\n",
|
||||
"eslint.alwaysShowStatus": true,
|
||||
"eslint.format.enable": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
}
|
21
LICENSE
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 Pitu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
100
README.md
|
@ -1,70 +1,68 @@
|
|||
![loli-safe](https://a.safe.moe/jcutlz.png)
|
||||
# lolisafe, a small safe worth protecting.
|
||||
<p align="center">
|
||||
<img width="234" height="376" src="https://lolisafe.moe/xjoghu.png">
|
||||
</p>
|
||||
|
||||
## Sites using loli-safe
|
||||
- [lolisafe.moe](https://lolisafe.moe): A small safe worth protecting.
|
||||
- [safe.moe](https://safe.moe): The world most ~~un~~safe pomf clone
|
||||
- Feel free to add yours here.
|
||||
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://raw.githubusercontent.com/kanadeko/Kuro/master/LICENSE)
|
||||
[![Chat / Support](https://img.shields.io/badge/Chat%20%2F%20Support-discord-7289DA.svg?style=flat-square)](https://discord.gg/5g6vgwn)
|
||||
[![Support me](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dpitu%26type%3Dpledges&style=flat-square)](https://www.patreon.com/pitu)
|
||||
[![Support me](https://img.shields.io/badge/Support-Buy%20me%20a%20coffee-yellow.svg?style=flat-square)](https://www.buymeacoffee.com/kana)
|
||||
|
||||
## What's new in v2.2.0
|
||||
- Creation of public link for sharing a gallery
|
||||
- Ability to add your own html files without making git dirty (Check [this commit](https://github.com/WeebDev/loli-safe/commit/18c66d27fb580ed0f847f11525d2d2dca0fda2f4))
|
||||
- Thumbnail creation for .webm and .mp4 (Thanks to [PascalTemel](https://github.com/PascalTemel))
|
||||
- Changed how duplicate files work (Check [this issue for more info](https://github.com/WeebDev/loli-safe/issues/8))
|
||||
### Attention
|
||||
If you are upgrading from `v3.x` to `v4.0.0` (lolisafe to chibisafe) and you want to keep your files and relations please read the [migration guide](docs/migrating.md). Keep in mind the migration is a best-effort script and it's recommended to start from scratch. That being said the migration does work but it's up to you to make a backup beforehand in case something goes wrong.
|
||||
|
||||
If you're upgrading from a previous version, create a `migrate.js` file on the root folder with the following code and run it only once:
|
||||
`v4.0.1` changed the hashing algorithm for a better, faster one. So if you are currently running v4.0.0 and decide to update to v4.0.1+ it's in your best interest to rehash all the files your instance is serving. To do this go to the chibisafe root folder and run `node src/api/utils/rehashDatabase.js`. Depending on how many files you have it can take a few minutes or hours, there's a progress bar that will give you an idea.
|
||||
|
||||
```js
|
||||
const config = require('./config.js')
|
||||
const db = require('knex')(config.database)
|
||||
const randomstring = require('randomstring')
|
||||
## What is Chibisafe?
|
||||
Chibisafe is a file uploader service written in node that aims to to be easy to use and easy to set up. It's mainly intended for images and videos, but it accepts anything you throw at it.
|
||||
- You can run it in public or private mode, making it so only people with user accounts can upload files as well as controlling if user signup is enabled or not.
|
||||
- Out of the box support for ShareX configuration letting you upload screenshots and screenrecordings directly to your chibisafe instance.
|
||||
- Browser extension to be able to right click any image/video from any website and upload it directly to your chibisafe instance.
|
||||
- Chunk uploads enabled by default to be able to handle big boi files.
|
||||
- API Key support so you can integrate the service with whatever you desire.
|
||||
- Albums, tags and Discord-like search function
|
||||
- User list and control panel
|
||||
|
||||
db.schema.table('albums', function (table) {
|
||||
table.string('identifier')
|
||||
}).then(() => {
|
||||
db.table('albums').then((albums) => {
|
||||
for(let album of albums)
|
||||
db.table('albums').where('id', album.id).update('identifier', randomstring.generate(8)).then(() => {})
|
||||
})
|
||||
})
|
||||
```
|
||||
### Docker
|
||||
If you want to deploy a docker instance instead of manually setting the service up, you can use `docker-composer` with our scripts. [Please refer to the docs here](docs/docker.md)
|
||||
|
||||
## Running
|
||||
1. Clone
|
||||
2. Rename `config.sample.js` to `config.js`
|
||||
4. Modify port, basedomain and privacy options if desired
|
||||
3. run `npm install` to install all dependencies
|
||||
5. run `pm2 start lolisafe.js` or `node lolisafe.js` to start the service
|
||||
### Pre-requisites
|
||||
This guide asumes a whole lot of things, including that you know your way around linux, nginx and internet in general.
|
||||
|
||||
## Getting started
|
||||
This service supports running both as public and private. The only difference is that one needs a token to upload and the other one doesn't. If you want it to be public so anyone can upload files either from the website or API, just set the option `private: false` in the `config.js` file. In case you want to run it privately, you should set `private: true`.
|
||||
- Decently updated version of linux (we recommend Debian)
|
||||
- `node` version 12.18.2+ (we recommend using [volta.sh](https://volta.sh/) or [n](https://github.com/tj/n))
|
||||
- `build-essential` package installed in your system to build dependencies
|
||||
- `ffmpeg` package installed
|
||||
- `pm2` globally installed (`npm i -g pm2`) to keep the service alive at all times.
|
||||
- Alternatively you can use tmux, forever, or whatever you are most familiar with
|
||||
- `nginx` installed and running
|
||||
|
||||
Upon running the service for the first time, it's gonna create a user account with the username `root` and password `root`. This is your admin account and you should change the password immediately. This account will let you manage all uploaded files and remove any if necessary.
|
||||
> Note: while Chibisafe does work on Windows, setting it up is not covered in this readme. It's up to you to install the neccessary dependencies
|
||||
|
||||
If you set `enableUserAccounts: true`, people will be able to create accounts on the service to keep track of their uploaded files and create albums to upload stuff to, pretty much like imgur does, but only through the API. Every user account has a token that the user can use to upload stuff through the API. You can find this token on the section called `Change your token` on the administration dashboard, and if it gets leaked or compromised you can renew it by clicking the button titled `Request new token`.
|
||||
### Installing
|
||||
|
||||
## Using loli-safe
|
||||
Once the service starts you can start hitting the upload endpoint at `/api/upload` with any file. If you're using the frontend to do so then you are pretty much set, but if using the API to upload make sure the form name is set to `files[]` and the form type to `multipart/form-data`. If the service is running in private mode, dont forget to send a header of type `token: YOUR-CLIENT-TOKEN` to validate the request.
|
||||
1. Clone the repository and `cd` into it
|
||||
2. Run `npm i`
|
||||
3. Run `npm run setup`
|
||||
|
||||
A sample of the returning json from the endpoint can be seen below:
|
||||
```json
|
||||
{
|
||||
"name": "EW7C.png",
|
||||
"size": "71400",
|
||||
"url": "https://i.kanacchi.moe/EW7C.png"
|
||||
}
|
||||
```
|
||||
Chibisafe is now installed, configured and ready. Now you need to serve it to the public by using a domain name.
|
||||
|
||||
To make it easier and better than any other service, you can download [our Chrome extension](https://chrome.google.com/webstore/detail/loli-safe-uploader/enkkmplljfjppcdaancckgilmgoiofnj) that will let you configure your hostname and tokens, so that you can simply `right click` -> `send to loli-safe` to any image/audio/video file on the web.
|
||||
4. Check the [nginx](docs/nginx.md) file for a sample configuration that has every step to run chibisafe securely on production.
|
||||
|
||||
Because of how nodejs apps work, if you want it attached to a domain name you will need to make a reverse proxy for it. Here is a tutorial [on how to do this with nginx](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-16-04). Keep in mind that this is only a requirement if you want to access your loli-safe service by using a domain name (ex: https://i.kanacchi.moe), otherwise you can use the service just fine by accessing it from your server's IP.
|
||||
After you finish setting up nginx, you need to start chibisafe by using pm2. If you want to use something else like forever, ensure that the process spawned from `npm run start` never dies.
|
||||
|
||||
If you choose to use a domain name and thus nginx, you should add the following directive into your location block with the limit you want to set on uploaded file's size:
|
||||
`client_max_body_size 512M;`
|
||||
5. Run `pm2 start pm2.json`:
|
||||
6. Profit
|
||||
|
||||
### Screenshots
|
||||
<p align="center">
|
||||
<img src="https://lolisafe.moe/73up1d.png">
|
||||
<img src="https://lolisafe.moe/q0uctp.png">
|
||||
<img src="https://lolisafe.moe/8fi2x6.png">
|
||||
</p>
|
||||
|
||||
## Author
|
||||
|
||||
**loli-safe** © [Pitu](https://github.com/Pitu), Released under the [MIT](https://github.com/WeebDev/loli-safe/blob/master/LICENSE) License.<br>
|
||||
**Chibisafe** © [Pitu](https://github.com/Pitu), Released under the [MIT](https://github.com/WeebDev/chibisafe/blob/master/LICENSE) License.<br>
|
||||
Authored and maintained by Pitu.
|
||||
|
||||
> [lolisafe.moe](https://lolisafe.moe) · GitHub [@Pitu](https://github.com/Pitu)
|
||||
> [chibisafe.moe](https://chibisafe.moe) · GitHub [@Pitu](https://github.com/Pitu)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
function isBabelLoader(caller) {
|
||||
return caller && caller.name === 'babel-loader';
|
||||
}
|
||||
|
||||
module.exports = api => {
|
||||
if (api.env('test') && !api.caller(isBabelLoader)) {
|
||||
return {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
node: 'current'
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
|
@ -1,77 +0,0 @@
|
|||
module.exports = {
|
||||
|
||||
/*
|
||||
If set to true the user will need to specify the auto-generated token
|
||||
on each API call, meaning random strangers wont be able to use the service
|
||||
unless they have the token loli-safe provides you with.
|
||||
If it's set to false, then upload will be public for anyone to use.
|
||||
*/
|
||||
private: true,
|
||||
|
||||
// If true, users will be able to create accounts and access their uploaded files
|
||||
enableUserAccounts: true,
|
||||
|
||||
// The registered domain where you will be serving the app. Use IP if none.
|
||||
domains: [
|
||||
|
||||
/*
|
||||
You need to specify the base domain where loli-self is running
|
||||
and how should it resolve the URL for uploaded files. For example:
|
||||
*/
|
||||
|
||||
// Files will be served at http(s)://i.kanacchi.moe/Fxt0.png
|
||||
{ host: 'kanacchi.moe', resolve: 'https://i.kanacchi.moe'},
|
||||
|
||||
// Files will be served at https://my.kanacchi.moe/loli-self/files/Fxt0.png
|
||||
{ host: 'kanacchi.moe', resolve: 'https://my.kanacchi.moe/loli-self/files' }
|
||||
|
||||
],
|
||||
|
||||
// Port on which to run the server
|
||||
port: 9999,
|
||||
|
||||
// Pages to process for the frontend
|
||||
pages: ['home', 'auth', 'dashboard', 'faq'],
|
||||
|
||||
// Add file extensions here which should be blocked
|
||||
blockedExtensions: [
|
||||
'.exe',
|
||||
'.bat',
|
||||
'.cmd',
|
||||
'.msi',
|
||||
'.sh'
|
||||
],
|
||||
|
||||
// Uploads config
|
||||
uploads: {
|
||||
|
||||
// Folder where images should be stored
|
||||
folder: 'uploads',
|
||||
|
||||
// Max file size allowed. Needs to be in MB
|
||||
// Note: When maxSize is greater than 1 MiB,
|
||||
// you must set the client_max_body_size
|
||||
// to the same as maxSize.
|
||||
maxSize: '512MB',
|
||||
|
||||
// The length of the random generated name for the uploaded files
|
||||
fileLength: 32,
|
||||
|
||||
// NOTE: Thumbnails are only for the admin panel and they require you
|
||||
// to install a separate binary called graphicsmagick (http://www.graphicsmagick.org)
|
||||
// for images and FFmpeg (https://ffmpeg.org/) for video files
|
||||
generateThumbnails: false
|
||||
},
|
||||
|
||||
// Folder where to store logs
|
||||
logsFolder: 'logs',
|
||||
|
||||
// The following values shouldn't be touched
|
||||
database: {
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
filename: './database/db'
|
||||
},
|
||||
useNullAsDefault: true
|
||||
}
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
const config = require('../config.js')
|
||||
const db = require('knex')(config.database)
|
||||
const randomstring = require('randomstring')
|
||||
const utils = require('./utilsController.js')
|
||||
const path = require('path')
|
||||
|
||||
let albumsController = {}
|
||||
|
||||
albumsController.list = function(req, res, next) {
|
||||
|
||||
let token = req.headers.token
|
||||
if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
|
||||
|
||||
db.table('users').where('token', token).then((user) => {
|
||||
if (user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token' })
|
||||
|
||||
let fields = ['id', 'name']
|
||||
|
||||
if (req.params.sidebar === undefined) {
|
||||
fields.push('timestamp')
|
||||
fields.push('identifier')
|
||||
}
|
||||
|
||||
db.table('albums').select(fields).where({ enabled: 1, userid: user[0].id }).then((albums) => {
|
||||
|
||||
if (req.params.sidebar !== undefined)
|
||||
return res.json({ success: true, albums })
|
||||
|
||||
let ids = []
|
||||
for (let album of albums) {
|
||||
album.date = new Date(album.timestamp * 1000)
|
||||
album.date = utils.getPrettyDate(album.date) // album.date.getFullYear() + '-' + (album.date.getMonth() + 1) + '-' + album.date.getDate() + ' ' + (album.date.getHours() < 10 ? '0' : '') + album.date.getHours() + ':' + (album.date.getMinutes() < 10 ? '0' : '') + album.date.getMinutes() + ':' + (album.date.getSeconds() < 10 ? '0' : '') + album.date.getSeconds()
|
||||
|
||||
let basedomain = req.get('host')
|
||||
for (let domain of config.domains)
|
||||
if (domain.host === req.get('host'))
|
||||
if (domain.hasOwnProperty('resolve'))
|
||||
basedomain = domain.resolve
|
||||
|
||||
album.identifier = basedomain + '/a/' + album.identifier
|
||||
|
||||
ids.push(album.id)
|
||||
}
|
||||
|
||||
db.table('files').whereIn('albumid', ids).select('albumid').then((files) => {
|
||||
|
||||
let albumsCount = {}
|
||||
|
||||
for (let id of ids) albumsCount[id] = 0
|
||||
for (let file of files) albumsCount[file.albumid] += 1
|
||||
for (let album of albums) album.files = albumsCount[album.id]
|
||||
|
||||
return res.json({ success: true, albums })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
|
||||
}
|
||||
|
||||
albumsController.create = function(req, res, next) {
|
||||
let token = req.headers.token
|
||||
if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
|
||||
|
||||
db.table('users').where('token', token).then((user) => {
|
||||
if (user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token' })
|
||||
|
||||
let name = req.body.name
|
||||
if (name === undefined || name === '')
|
||||
return res.json({ success: false, description: 'No album name specified' })
|
||||
|
||||
db.table('albums').where({
|
||||
name: name,
|
||||
enabled: 1,
|
||||
userid: user[0].id
|
||||
}).then((album) => {
|
||||
if (album.length !== 0) return res.json({ success: false, description: 'There\'s already an album with that name' })
|
||||
|
||||
db.table('albums').insert({
|
||||
name: name,
|
||||
enabled: 1,
|
||||
userid: user[0].id,
|
||||
identifier: randomstring.generate(8),
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}).then(() => {
|
||||
return res.json({ success: true })
|
||||
})
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
|
||||
}
|
||||
|
||||
albumsController.delete = function(req, res, next) {
|
||||
let token = req.headers.token
|
||||
if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
|
||||
|
||||
db.table('users').where('token', token).then((user) => {
|
||||
if (user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token'})
|
||||
|
||||
let id = req.body.id
|
||||
if (id === undefined || id === ''){
|
||||
return res.json({ success: false, description: 'No album specified' })
|
||||
}
|
||||
|
||||
db.table('albums').where({ id: id, userid: user[0].id }).update({ enabled: 0 }).then(() => {
|
||||
return res.json({ success: true })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}
|
||||
|
||||
albumsController.rename = function(req, res, next) {
|
||||
let token = req.headers.token
|
||||
if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
|
||||
|
||||
db.table('users').where('token', token).then((user) => {
|
||||
if (user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token'})
|
||||
|
||||
let id = req.body.id
|
||||
if (id === undefined || id === '')
|
||||
return res.json({ success: false, description: 'No album specified' })
|
||||
|
||||
let name = req.body.name
|
||||
if (name === undefined || name === '')
|
||||
return res.json({ success: false, description: 'No name specified' })
|
||||
|
||||
db.table('albums').where({ name: name, userid: user[0].id }).then((results) => {
|
||||
if (results.length !== 0) return res.json({ success: false, description: 'Name already in use' })
|
||||
|
||||
db.table('albums').where({ id: id, userid: user[0].id }).update({ name: name }).then(() => {
|
||||
return res.json({ success: true })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
|
||||
}
|
||||
|
||||
albumsController.get = function(req, res, next) {
|
||||
let identifier = req.params.identifier
|
||||
if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided' })
|
||||
|
||||
db.table('albums')
|
||||
.where('identifier', identifier)
|
||||
.then((albums) => {
|
||||
if (albums.length === 0) return res.json({ success: false, description: 'Album not found' })
|
||||
|
||||
let title = albums[0].name
|
||||
db.table('files').select('name').where('albumid', albums[0].id).orderBy('id', 'DESC').then((files) => {
|
||||
|
||||
let basedomain = req.get('host')
|
||||
for (let domain of config.domains)
|
||||
if (domain.host === req.get('host'))
|
||||
if (domain.hasOwnProperty('resolve'))
|
||||
basedomain = domain.resolve
|
||||
|
||||
for (let file of files) {
|
||||
file.file = basedomain + '/' + file.name
|
||||
|
||||
let ext = path.extname(file.name).toLowerCase()
|
||||
if (utils.extensions.includes(ext)) {
|
||||
file.thumb = basedomain + '/thumbs/' + file.name.slice(0, -ext.length) + '.png'
|
||||
utils.generateThumbs(file)
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
title: title,
|
||||
count: files.length,
|
||||
files
|
||||
})
|
||||
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}
|
||||
|
||||
module.exports = albumsController
|
|
@ -1,88 +0,0 @@
|
|||
const config = require('../config.js')
|
||||
const db = require('knex')(config.database)
|
||||
const bcrypt = require('bcrypt')
|
||||
const saltRounds = 10
|
||||
const randomstring = require('randomstring')
|
||||
|
||||
let authController = {}
|
||||
|
||||
authController.verify = function(req, res, next) {
|
||||
|
||||
let username = req.body.username
|
||||
let password = req.body.password
|
||||
|
||||
if (username === undefined) return res.json({ success: false, description: 'No username provided' })
|
||||
if (password === undefined) return res.json({ success: false, description: 'No password provided' })
|
||||
|
||||
db.table('users').where('username', username).then((user) => {
|
||||
if (user.length === 0) return res.json({ success: false, description: 'Username doesn\'t exist' })
|
||||
|
||||
bcrypt.compare(password, user[0].password, function(err, result) {
|
||||
if (result === false) return res.json({ success: false, description: 'Wrong password' })
|
||||
return res.json({ success: true, token: user[0].token })
|
||||
})
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
|
||||
}
|
||||
|
||||
authController.register = function(req, res, next) {
|
||||
|
||||
if (config.enableUserAccounts === false)
|
||||
return res.json({ success: false, description: 'Register is disabled at the moment' })
|
||||
|
||||
let username = req.body.username
|
||||
let password = req.body.password
|
||||
|
||||
if (username === undefined) return res.json({ success: false, description: 'No username provided' })
|
||||
if (password === undefined) return res.json({ success: false, description: 'No password provided' })
|
||||
|
||||
if (username.length < 4 || username.length > 32)
|
||||
return res.json({ success: false, description: 'Username must have 4-32 characters' })
|
||||
if (password.length < 6 || password.length > 64)
|
||||
return res.json({ success: false, description: 'Password must have 6-64 characters' })
|
||||
|
||||
db.table('users').where('username', username).then((user) => {
|
||||
if (user.length !== 0) return res.json({ success: false, description: 'Username already exists' })
|
||||
|
||||
bcrypt.hash(password, saltRounds, function(err, hash) {
|
||||
if (err) return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻' })
|
||||
|
||||
let token = randomstring.generate(64)
|
||||
|
||||
db.table('users').insert({
|
||||
username: username,
|
||||
password: hash,
|
||||
token: token
|
||||
}).then(() => {
|
||||
return res.json({ success: true, token: token })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
})
|
||||
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
|
||||
}
|
||||
|
||||
authController.changePassword = function(req, res, next) {
|
||||
|
||||
let token = req.headers.token
|
||||
if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
|
||||
|
||||
db.table('users').where('token', token).then((user) => {
|
||||
if (user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token'})
|
||||
|
||||
let password = req.body.password
|
||||
if (password === undefined) return res.json({ success: false, description: 'No password provided' })
|
||||
if (password.length < 6 || password.length > 64)
|
||||
return res.json({ success: false, description: 'Password must have 6-64 characters' })
|
||||
|
||||
bcrypt.hash(password, saltRounds, function(err, hash) {
|
||||
if (err) return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻' })
|
||||
|
||||
db.table('users').where('id', user[0].id).update({ password: hash }).then(() => {
|
||||
return res.json({ success: true })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
})
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}
|
||||
|
||||
module.exports = authController
|
|
@ -1,46 +0,0 @@
|
|||
const config = require('../config.js')
|
||||
const db = require('knex')(config.database)
|
||||
const randomstring = require('randomstring')
|
||||
|
||||
let tokenController = {}
|
||||
|
||||
tokenController.verify = function(req, res, next) {
|
||||
|
||||
if (req.body.token === undefined) return res.json({ success: false, description: 'No token provided' })
|
||||
let token = req.body.token
|
||||
|
||||
db.table('users').where('token', token).then((user) => {
|
||||
if (user.length === 0) return res.json({ success: false, description: 'Token mismatch' })
|
||||
return res.json({ success: true, username: user[0].username })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
|
||||
}
|
||||
|
||||
tokenController.list = function(req, res, next) {
|
||||
|
||||
let token = req.headers.token
|
||||
if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
|
||||
|
||||
db.table('users').where('token', token).then((user) => {
|
||||
if (user.length === 0) return res.json({ success: false, description: 'Token mismatch' })
|
||||
return res.json({ success: true, token: token })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
|
||||
}
|
||||
|
||||
tokenController.change = function(req, res, next) {
|
||||
|
||||
let token = req.headers.token
|
||||
if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
|
||||
|
||||
let newtoken = randomstring.generate(64)
|
||||
|
||||
db.table('users').where('token', token).update({
|
||||
token: newtoken,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}).then(() => {
|
||||
res.json({ success: true, token: newtoken })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}
|
||||
|
||||
module.exports = tokenController
|
|
@ -1,328 +0,0 @@
|
|||
const config = require('../config.js')
|
||||
const path = require('path')
|
||||
const multer = require('multer')
|
||||
const randomstring = require('randomstring')
|
||||
const db = require('knex')(config.database)
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs')
|
||||
const utils = require('./utilsController.js')
|
||||
|
||||
let uploadsController = {}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: function(req, file, cb) {
|
||||
cb(null, path.join(__dirname, '..', config.uploads.folder))
|
||||
},
|
||||
filename: function(req, file, cb) {
|
||||
cb(null, randomstring.generate(config.uploads.fileLength) + path.extname(file.originalname))
|
||||
}
|
||||
})
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: { fileSize: config.uploads.maxSize },
|
||||
fileFilter: function(req, file, cb) {
|
||||
if (config.blockedExtensions !== undefined) {
|
||||
if (config.blockedExtensions.some(extension => path.extname(file.originalname).toLowerCase() === extension)) {
|
||||
return cb('This file extension is not allowed');
|
||||
}
|
||||
return cb(null, true);
|
||||
}
|
||||
return cb(null, true);
|
||||
}
|
||||
}).array('files[]')
|
||||
|
||||
uploadsController.upload = function(req, res, next) {
|
||||
|
||||
// Get the token
|
||||
let token = req.headers.token
|
||||
|
||||
// If we're running in private and there's no token, error
|
||||
if (config.private === true)
|
||||
if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
|
||||
|
||||
// If there is no token then just leave it blank so the query fails
|
||||
if (token === undefined) token = ''
|
||||
|
||||
db.table('users').where('token', token).then((user) => {
|
||||
|
||||
if(user.length === 0)
|
||||
if(config.private === true)
|
||||
return res.status(401).json({ success: false, description: 'Invalid token provided' })
|
||||
|
||||
let userid
|
||||
if(user.length > 0)
|
||||
userid = user[0].id
|
||||
|
||||
// Check if user is trying to upload to an album
|
||||
let album
|
||||
if (userid !== undefined) {
|
||||
album = req.headers.albumid
|
||||
if (album === undefined)
|
||||
album = req.params.albumid
|
||||
}
|
||||
|
||||
/*
|
||||
A rewrite is due so might as well do awful things here and fix them later :bloblul:
|
||||
*/
|
||||
|
||||
if (album !== undefined && userid !== undefined) {
|
||||
// If both values are present, check if the album owner is the user uploading
|
||||
db.table('albums').where({ id: album, userid: userid }).then((albums) => {
|
||||
if (albums.length === 0) {
|
||||
return res.json({
|
||||
success: false,
|
||||
description: 'Album doesn\'t exist or it doesn\'t belong to the user'
|
||||
})
|
||||
}
|
||||
uploadsController.actuallyUpload(req, res, userid, album);
|
||||
})
|
||||
} else {
|
||||
uploadsController.actuallyUpload(req, res, userid, album);
|
||||
}
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}
|
||||
|
||||
uploadsController.actuallyUpload = function(req, res, userid, album) {
|
||||
upload(req, res, function (err) {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
return res.json({
|
||||
success: false,
|
||||
description: err
|
||||
})
|
||||
}
|
||||
|
||||
if (req.files.length === 0) return res.json({ success: false, description: 'no-files' })
|
||||
|
||||
let files = []
|
||||
let existingFiles = []
|
||||
let iteration = 1
|
||||
|
||||
req.files.forEach(function(file) {
|
||||
|
||||
// Check if the file exists by checking hash and size
|
||||
let hash = crypto.createHash('md5')
|
||||
let stream = fs.createReadStream(path.join(__dirname, '..', config.uploads.folder, file.filename))
|
||||
|
||||
stream.on('data', function (data) {
|
||||
hash.update(data, 'utf8')
|
||||
})
|
||||
|
||||
stream.on('end', function () {
|
||||
let fileHash = hash.digest('hex')
|
||||
|
||||
db.table('files')
|
||||
.where(function() {
|
||||
if (userid === undefined)
|
||||
this.whereNull('userid')
|
||||
else
|
||||
this.where('userid', userid)
|
||||
})
|
||||
.where({
|
||||
hash: fileHash,
|
||||
size: file.size
|
||||
}).then((dbfile) => {
|
||||
|
||||
if (dbfile.length !== 0) {
|
||||
uploadsController.deleteFile(file.filename).then(() => {}).catch((e) => console.error(e))
|
||||
existingFiles.push(dbfile[0])
|
||||
} else {
|
||||
files.push({
|
||||
name: file.filename,
|
||||
original: file.originalname,
|
||||
type: file.mimetype,
|
||||
size: file.size,
|
||||
hash: fileHash,
|
||||
ip: req.ip,
|
||||
albumid: album,
|
||||
userid: userid,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
}
|
||||
|
||||
if (iteration === req.files.length)
|
||||
return uploadsController.processFilesForDisplay(req, res, files, existingFiles)
|
||||
iteration++
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
uploadsController.processFilesForDisplay = function(req, res, files, existingFiles) {
|
||||
|
||||
let basedomain = req.get('host')
|
||||
for (let domain of config.domains)
|
||||
if (domain.host === req.get('host'))
|
||||
if (domain.hasOwnProperty('resolve'))
|
||||
basedomain = domain.resolve
|
||||
|
||||
if (files.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
files: existingFiles.map(file => {
|
||||
return {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
url: basedomain + '/' + file.name
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
db.table('files').insert(files).then(() => {
|
||||
|
||||
for (let efile of existingFiles) files.push(efile)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
files: files.map(file => {
|
||||
return {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
url: basedomain + '/' + file.name
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
for (let file of files) {
|
||||
let ext = path.extname(file.name).toLowerCase()
|
||||
if (utils.extensions.includes(ext)) {
|
||||
file.thumb = basedomain + '/thumbs/' + file.name.slice(0, -ext.length) + '.png'
|
||||
utils.generateThumbs(file)
|
||||
}
|
||||
}
|
||||
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}
|
||||
|
||||
uploadsController.delete = function(req, res) {
|
||||
|
||||
let token = req.headers.token
|
||||
if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
|
||||
|
||||
let id = req.body.id
|
||||
if (id === undefined || id === '')
|
||||
return res.json({ success: false, description: 'No file specified' })
|
||||
|
||||
db.table('users').where('token', token).then((user) => {
|
||||
if (user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token' })
|
||||
|
||||
db.table('files')
|
||||
.where('id', id)
|
||||
.where(function() {
|
||||
if (user[0].username !== 'root')
|
||||
this.where('userid', user[0].id)
|
||||
})
|
||||
.then((file) => {
|
||||
|
||||
uploadsController.deleteFile(file[0].name).then(() => {
|
||||
db.table('files').where('id', id).del().then(() => {
|
||||
return res.json({ success: true })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}).catch((e) => {
|
||||
console.log(e.toString())
|
||||
db.table('files').where('id', id).del().then(() => {
|
||||
return res.json({ success: true })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
})
|
||||
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}
|
||||
|
||||
uploadsController.deleteFile = function(file) {
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
fs.stat(path.join(__dirname, '..', config.uploads.folder, file), function(err, stats) {
|
||||
if (err) { return reject(err) }
|
||||
fs.unlink(path.join(__dirname, '..', config.uploads.folder, file), function(err) {
|
||||
if (err) { return reject(err) }
|
||||
return resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
uploadsController.list = function(req, res) {
|
||||
|
||||
let token = req.headers.token
|
||||
if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
|
||||
|
||||
db.table('users').where('token', token).then((user) => {
|
||||
if (user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token'})
|
||||
|
||||
let offset = req.params.page
|
||||
if (offset === undefined) offset = 0
|
||||
|
||||
db.table('files')
|
||||
.where(function() {
|
||||
if (req.params.id === undefined)
|
||||
this.where('id', '<>', '')
|
||||
else
|
||||
this.where('albumid', req.params.id)
|
||||
})
|
||||
.where(function() {
|
||||
if (user[0].username !== 'root')
|
||||
this.where('userid', user[0].id)
|
||||
})
|
||||
.orderBy('id', 'DESC')
|
||||
.limit(25)
|
||||
.offset(25 * offset)
|
||||
.select('id', 'albumid', 'timestamp', 'name', 'userid')
|
||||
.then((files) => {
|
||||
db.table('albums').then((albums) => {
|
||||
|
||||
let basedomain = req.get('host')
|
||||
for (let domain of config.domains)
|
||||
if (domain.host === req.get('host'))
|
||||
if (domain.hasOwnProperty('resolve'))
|
||||
basedomain = domain.resolve
|
||||
|
||||
let userids = []
|
||||
|
||||
for (let file of files) {
|
||||
file.file = basedomain + '/' + file.name
|
||||
file.date = new Date(file.timestamp * 1000)
|
||||
file.date = utils.getPrettyDate(file.date) // file.date.getFullYear() + '-' + (file.date.getMonth() + 1) + '-' + file.date.getDate() + ' ' + (file.date.getHours() < 10 ? '0' : '') + file.date.getHours() + ':' + (file.date.getMinutes() < 10 ? '0' : '') + file.date.getMinutes() + ':' + (file.date.getSeconds() < 10 ? '0' : '') + file.date.getSeconds()
|
||||
|
||||
file.album = ''
|
||||
|
||||
if (file.albumid !== undefined)
|
||||
for (let album of albums)
|
||||
if (file.albumid === album.id)
|
||||
file.album = album.name
|
||||
|
||||
// Only push usernames if we are root
|
||||
if (user[0].username === 'root')
|
||||
if (file.userid !== undefined && file.userid !== null && file.userid !== '')
|
||||
userids.push(file.userid)
|
||||
|
||||
let ext = path.extname(file.name).toLowerCase()
|
||||
if (utils.extensions.includes(ext)) {
|
||||
file.thumb = basedomain + '/thumbs/' + file.name.slice(0, -ext.length) + '.png'
|
||||
utils.generateThumbs(file)
|
||||
}
|
||||
}
|
||||
|
||||
// If we are a normal user, send response
|
||||
if (user[0].username !== 'root') return res.json({ success: true, files })
|
||||
|
||||
// If we are root but there are no uploads attached to a user, send response
|
||||
if (userids.length === 0) return res.json({ success: true, files })
|
||||
|
||||
db.table('users').whereIn('id', userids).then((users) => {
|
||||
for (let user of users)
|
||||
for (let file of files)
|
||||
if (file.userid === user.id)
|
||||
file.username = user.username
|
||||
|
||||
return res.json({ success: true, files })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = uploadsController
|
|
@ -1,58 +0,0 @@
|
|||
const path = require('path')
|
||||
const config = require('../config.js')
|
||||
const fs = require('fs')
|
||||
const gm = require('gm')
|
||||
const ffmpeg = require('fluent-ffmpeg')
|
||||
|
||||
const utilsController = {}
|
||||
utilsController.extensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webm', '.mp4']
|
||||
|
||||
utilsController.getPrettyDate = function(date) {
|
||||
return date.getFullYear() + '-'
|
||||
+ (date.getMonth() + 1) + '-'
|
||||
+ date.getDate() + ' '
|
||||
+ (date.getHours() < 10 ? '0' : '')
|
||||
+ date.getHours() + ':'
|
||||
+ (date.getMinutes() < 10 ? '0' : '')
|
||||
+ date.getMinutes() + ':'
|
||||
+ (date.getSeconds() < 10 ? '0' : '')
|
||||
+ date.getSeconds()
|
||||
}
|
||||
|
||||
utilsController.generateThumbs = function(file, basedomain) {
|
||||
if (config.uploads.generateThumbnails !== true) return
|
||||
const ext = path.extname(file.name).toLowerCase()
|
||||
|
||||
let thumbname = path.join(__dirname, '..', config.uploads.folder, 'thumbs', file.name.slice(0, -ext.length) + '.png')
|
||||
fs.access(thumbname, (err) => {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
if (ext === '.webm' || ext === '.mp4') {
|
||||
ffmpeg(path.join(__dirname, '..', config.uploads.folder, file.name))
|
||||
.thumbnail({
|
||||
timestamps: [0],
|
||||
filename: '%b.png',
|
||||
folder: path.join(__dirname, '..', config.uploads.folder, 'thumbs'),
|
||||
size: '200x?'
|
||||
})
|
||||
.on('error', (error) => {
|
||||
console.log('Error - ', error.message)
|
||||
})
|
||||
} else {
|
||||
let size = {
|
||||
width: 200,
|
||||
height: 200
|
||||
}
|
||||
gm(path.join(__dirname, '..', config.uploads.folder, file.name))
|
||||
.resize(size.width, size.height + '>')
|
||||
.gravity('Center')
|
||||
.extent(size.width, size.height)
|
||||
.background('transparent')
|
||||
.write(thumbname, (error) => {
|
||||
if (error) console.log('Error - ', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = utilsController
|
|
@ -1,50 +0,0 @@
|
|||
let init = function(db){
|
||||
|
||||
// Create the tables we need to store galleries and files
|
||||
db.schema.createTableIfNotExists('albums', function (table) {
|
||||
table.increments()
|
||||
table.integer('userid')
|
||||
table.string('name')
|
||||
table.string('identifier')
|
||||
table.integer('enabled')
|
||||
table.integer('timestamp')
|
||||
}).then(() => {})
|
||||
|
||||
db.schema.createTableIfNotExists('files', function (table) {
|
||||
table.increments()
|
||||
table.integer('userid')
|
||||
table.string('name')
|
||||
table.string('original')
|
||||
table.string('type')
|
||||
table.string('size')
|
||||
table.string('hash')
|
||||
table.string('ip')
|
||||
table.integer('albumid')
|
||||
table.integer('timestamp')
|
||||
}).then(() => {})
|
||||
|
||||
db.schema.createTableIfNotExists('users', function (table) {
|
||||
table.increments()
|
||||
table.string('username')
|
||||
table.string('password')
|
||||
table.string('token')
|
||||
table.integer('timestamp')
|
||||
}).then(() => {
|
||||
db.table('users').where({username: 'root'}).then((user) => {
|
||||
if(user.length > 0) return
|
||||
|
||||
require('bcrypt').hash('root', 10, function(err, hash) {
|
||||
if(err) console.error('Error generating password hash for root')
|
||||
|
||||
db.table('users').insert({
|
||||
username: 'root',
|
||||
password: hash,
|
||||
token: require('randomstring').generate(64),
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}).then(() => {})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = init
|
|
@ -0,0 +1,3 @@
|
|||
$env = $args[0]
|
||||
$cmd = $args | Select-Object -Skip 1
|
||||
docker-compose -f docker-compose.yml -f docker-compose.$env.yml -f docker-compose.config.yml $cmd
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/bash
|
||||
docker-compose -f docker-compose.yml -f docker-compose.$1.yml -f docker-compose.config.yml ${@%$1}
|
|
@ -0,0 +1,20 @@
|
|||
FROM jrottenberg/ffmpeg:4.3-alpine312 as ffmpeg
|
||||
|
||||
FROM node:14-alpine
|
||||
|
||||
WORKDIR /usr/chibisafe
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
RUN apk add --update \
|
||||
&& apk add --no-cache ca-certificates libwebp libwebp-tools expat \
|
||||
&& apk add --no-cache vidstab-dev --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community \
|
||||
&& apk add --no-cache --virtual .build-deps git curl build-base python3 g++ make \
|
||||
&& npm ci \
|
||||
&& apk del .build-deps
|
||||
|
||||
COPY --from=ffmpeg /usr/local /usr/local
|
||||
|
||||
COPY . .
|
||||
RUN mkdir uploads && mkdir database
|
||||
|
||||
CMD ["sh", "-c", "npm run migrate && npm run seed && npm start"]
|
|
@ -0,0 +1,35 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
chibisafe:
|
||||
environment:
|
||||
CHUNK_SIZE: 90
|
||||
SECRET: "wowfcgMHqZHwOIMLadWrKu3liyqPOOILpDLSDvuxq3YGhJmiZXJCVpnF96l11WfR"
|
||||
ADMIN_ACCOUNT: "admin"
|
||||
ADMIN_PASSWORD: "admin"
|
||||
# OVERWRITE_SETTINGS: 'false'
|
||||
# ROUTE_PREFIX: /api
|
||||
# RATE_LIMIT_WINDOW: 2
|
||||
# RATE_LIMIT_MAX: 5
|
||||
# BLOCKED_EXTENSIONS: '.jar,.exe,.msi,.com,.bat,.cmd,.scr,.ps1,.sh'
|
||||
# META_THEME_COLOR: '#20222b'
|
||||
# META_DESCRIPTION: 'Blazing fast file uploader and bunker written in node! 🚀'
|
||||
# META_KEYWORDS: 'chibisafe,upload,uploader,file,vue,images,ssr,file uploader,free'
|
||||
# META_TWITTER_HANDLE: ''
|
||||
# SERVER_PORT: 5000
|
||||
# DOMAIN: 'http://chibisafe.moe'
|
||||
# SERVICE_NAME: chibisafe
|
||||
# MAX_SIZE: 5000
|
||||
# GENERATE_THUMBNAILS: 'true'
|
||||
# GENERATE_ZIPS: 'true'
|
||||
# STRIP_EXIF: 'true'
|
||||
# SERVE_WITH_NODE: 'true'
|
||||
# GENERATED_FILENAME_LENGTH: 6
|
||||
# GENERATED_ALBUM_LENGTH: 4
|
||||
# PUBLIC_MODE: 'false'
|
||||
# USER_ACCOUNTS: 'true'
|
||||
# DB_CLIENT: 'sqlite3'
|
||||
# DB_HOST: ''
|
||||
# DB_USER: ''
|
||||
# DB_PASSWORD: ''
|
||||
# DB_DATABASE: ''
|
|
@ -0,0 +1,15 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
chibisafe:
|
||||
volumes:
|
||||
- chibisafe-data:/usr/chibisafe/uploads
|
||||
- chibisafe-database:/usr/chibisafe/database
|
||||
|
||||
volumes:
|
||||
nginx-data:
|
||||
name: "nginx-data"
|
||||
chibisafe-data:
|
||||
name: "chibisafe-data"
|
||||
chibisafe-database:
|
||||
name: "chibisafe-database"
|
|
@ -0,0 +1,7 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
chibisafe:
|
||||
volumes:
|
||||
- ./chibisafe-data:/usr/chibisafe/uploads
|
||||
- ./chibisafe-database:/usr/chibisafe/database
|
|
@ -0,0 +1,59 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
nginx:
|
||||
build:
|
||||
context: ./nginx
|
||||
dockerfile: Dockerfile
|
||||
expose:
|
||||
- "80"
|
||||
- "443"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "service", "nginx", "status"]
|
||||
interval: 60s
|
||||
timeout: 5s
|
||||
|
||||
chibisafe:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: ./docker/chibisafe/Dockerfile
|
||||
expose:
|
||||
- "5000"
|
||||
- "5001"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
OVERWRITE_SETTINGS: "false"
|
||||
CHUNK_SIZE: 90
|
||||
ROUTE_PREFIX: /api
|
||||
RATE_LIMIT_WINDOW: 2
|
||||
RATE_LIMIT_MAX: 5
|
||||
BLOCKED_EXTENSIONS: ".jar,.exe,.msi,.com,.bat,.cmd,.scr,.ps1,.sh"
|
||||
SECRET: ""
|
||||
MAX_LINKS_PER_ALBUM: 5
|
||||
META_THEME_COLOR: "#20222b"
|
||||
META_DESCRIPTION: "Blazing fast file uploader and bunker written in node! 🚀"
|
||||
META_KEYWORDS: "chibisafe,upload,uploader,file,vue,images,ssr,file uploader,free"
|
||||
META_TWITTER_HANDLE: ""
|
||||
SERVER_PORT: 5000
|
||||
DOMAIN: "http://localhost:5000"
|
||||
SERVICE_NAME: chibisafe
|
||||
MAX_SIZE: 5000
|
||||
GENERATE_THUMBNAILS: "true"
|
||||
GENERATE_ZIPS: "true"
|
||||
STRIP_EXIF: "true"
|
||||
SERVE_WITH_NODE: "true"
|
||||
GENERATED_FILENAME_LENGTH: 6
|
||||
GENERATED_ALBUM_LENGTH: 4
|
||||
PUBLIC_MODE: "false"
|
||||
USER_ACCOUNTS: "true"
|
||||
ADMIN_ACCOUNT: ""
|
||||
ADMIN_PASSWORD: ""
|
||||
DB_CLIENT: "sqlite3"
|
||||
DB_HOST: ""
|
||||
DB_USER: ""
|
||||
DB_PASSWORD: ""
|
||||
DB_DATABASE: ""
|
|
@ -0,0 +1,6 @@
|
|||
FROM nginx
|
||||
|
||||
COPY nginxconfig.io /etc/nginx/nginxconfig.io
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY chibisafe.moe.conf /etc/nginx/conf.d/chibisafe.moe.conf
|
||||
COPY ssl /etc/nginx/ssl
|
|
@ -0,0 +1,21 @@
|
|||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name chibisafe.moe;
|
||||
|
||||
# security
|
||||
include nginxconfig.io/security.conf;
|
||||
|
||||
# logging
|
||||
access_log /var/log/nginx/chibisafe.moe.access.log;
|
||||
error_log /var/log/nginx/chibisafe.moe.error.log warn;
|
||||
|
||||
# reverse proxy
|
||||
location / {
|
||||
proxy_pass http://chibisafe:5000;
|
||||
include nginxconfig.io/proxy.conf;
|
||||
}
|
||||
|
||||
# additional config
|
||||
include nginxconfig.io/general.conf;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name chibisafe.moe;
|
||||
|
||||
# SSL
|
||||
ssl_certificate /etc/nginx/ssl/chibisafe.moe.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/chibisafe.moe.key;
|
||||
|
||||
# security
|
||||
include nginxconfig.io/security.conf;
|
||||
|
||||
# logging
|
||||
access_log /var/log/nginx/chibisafe.moe.access.log;
|
||||
error_log /var/log/nginx/chibisafe.moe.error.log warn;
|
||||
|
||||
# reverse proxy
|
||||
location / {
|
||||
proxy_pass http://chibisafe:5000;
|
||||
include nginxconfig.io/proxy.conf;
|
||||
}
|
||||
|
||||
# additional config
|
||||
include nginxconfig.io/general.conf;
|
||||
}
|
||||
|
||||
# HTTP redirect
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
# Generated by nginxconfig.io
|
||||
# https://www.digitalocean.com/community/tools/nginx?domains.0.server.domain=tourneys.naval-base.com&domains.0.server.documentRoot=&domains.0.https.certType=custom&domains.0.php.php=false&domains.0.reverseProxy.reverseProxy=true&domains.0.reverseProxy.proxyPass=http%3A%2F%2F127.0.0.1%3A3001&domains.0.routing.root=false&domains.0.logging.accessLog=true&domains.0.logging.errorLog=true
|
||||
|
||||
user www-data;
|
||||
pid /run/nginx.pid;
|
||||
worker_processes auto;
|
||||
worker_rlimit_nofile 65535;
|
||||
|
||||
events {
|
||||
multi_accept on;
|
||||
worker_connections 65535;
|
||||
}
|
||||
|
||||
http {
|
||||
charset utf-8;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
server_tokens off;
|
||||
log_not_found off;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# MIME
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Logging
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
# SSL
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# Diffie-Hellman parameter for DHE ciphersuites
|
||||
# ssl_dhparam /etc/nginx/dhparam.pem;
|
||||
|
||||
# Mozilla Intermediate configuration
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
|
||||
# OCSP Stapling
|
||||
ssl_stapling off;
|
||||
ssl_stapling_verify off;
|
||||
resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=60s;
|
||||
resolver_timeout 2s;
|
||||
|
||||
# Upload size limit
|
||||
client_max_body_size 100M;
|
||||
client_body_timeout 600s;
|
||||
|
||||
# Load configs
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
# include /etc/nginx/sites-enabled/*;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
# favicon.ico
|
||||
location = /favicon.ico {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# robots.txt
|
||||
location = /robots.txt {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# gzip
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
|
@ -0,0 +1,18 @@
|
|||
proxy_http_version 1.1;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
# Proxy headers
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
|
||||
# Proxy timeouts
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
|
@ -0,0 +1,12 @@
|
|||
# security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline' 'unsafe-eval'" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
# . files
|
||||
location ~ /\.(?!well-known) {
|
||||
deny all;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
### Using Docker
|
||||
|
||||
If you want to avoid all the hassle of installing the dependencies, configuring nginx and so on you can try our docker image which makes things a bit simpler.
|
||||
|
||||
First make sure you have docker and docker composer installed, so please follow the install instructions for your OS/Distro:
|
||||
- https://docs.docker.com/engine/install/debian/
|
||||
- https://docs.docker.com/compose/install/
|
||||
|
||||
After that:
|
||||
- Copy the config file called `docker-compose.config.example.yml` and name it `docker-compose.config.yml` with the values you want. Those that are left commented will use the default values.
|
||||
- Copy either `chibisafe.moe.http.example.conf` or `chibisafe.moe.https.example.conf` and name it `chibisafe.moe.conf` for either HTTP or HTTPS
|
||||
- - If using HTTPS make sure to put your certificates into the `ssl` folder and name them accordingly:
|
||||
- - - `chibisafe.moe.crt` for the certificate
|
||||
- - - `chibisafe.moe.key` for the certificate key
|
||||
|
||||
Once you are done run the following commands:
|
||||
|
||||
- `cd docker`
|
||||
- `./chibisafe prod pull`
|
||||
- `./chibisafe prod build`
|
||||
- `./chibisafe prod up -d`
|
||||
|
||||
Congrats, your chibisafe instance is now running.
|
|
@ -0,0 +1,18 @@
|
|||
### Migrate from v3 to v4
|
||||
This version introduces a few breaking changes and updating requires some manual work.
|
||||
For starters we recommend cloning the new version somewhere else instead of `git pull` on your v3 version.
|
||||
|
||||
- After cloning move your `uploads` folder from the v3 folder to the new v4 folder.
|
||||
- Then copy your `database/db` file from your v3 folder to the root of your v4 folder.
|
||||
- Make sure to install the dependencies by running `npm i`
|
||||
- You then need to run `npm run setup` from the v4 folder and finish the setup process.
|
||||
- Once that's done you need to manually run `node src/api/scripts/databaseMigration.js` from the root folder of v4.
|
||||
- This will migrate the v3 database to v4 and regenerate every single thumbnail in webp to save bandwidth.
|
||||
- After the migration finishes, the last step is to update your nginx config with the [newly provided script](./nginx.md).
|
||||
- Restart nginx with `sudo nginx -s reload`.
|
||||
- And lastly start your chibisafe instance with `pm2 start pm2.json`.
|
||||
|
||||
### Breaking changes
|
||||
- If you are using the chibisafe extension from one of the stores, the new version has been submitted already. You can also load the unpacked extension by cloning [this repo](https://github.com/WeebDev/chibisafe-extension).
|
||||
- The chibisafe browser extension needs your new token. Instead of pasting your jwt token into it like before, you need to log in to chibisafe, go to your user settings and generate an `API KEY`, which you will use to access the service from 3rd party apps like the browser extension, ShareX, etc.
|
||||
- To upload a file to an album directly users used to use the endpoint `/api/upload/${albumId}`. This is no longer the case. To upload directly to an album now it's necessary to pass a header called `albumid` with an integer as the value of the album to which you want to upload the file to.
|
|
@ -0,0 +1,66 @@
|
|||
### Nginx config for SSL
|
||||
Make sure that:
|
||||
- `backend` port matches your wizard config
|
||||
- `client_max_body_size` matches your wizard config
|
||||
- You replace `your.domain` where pertinent
|
||||
|
||||
|
||||
```nginx
|
||||
upstream backend {
|
||||
server 127.0.0.1:5000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name your.domain;
|
||||
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
|
||||
server_name your.domain;
|
||||
|
||||
ssl_certificate /path/to/certificate.pem;
|
||||
ssl_certificate_key /path/to/certificate.key;
|
||||
ssl_trusted_certificate /path/to/certificate.pem;
|
||||
|
||||
access_log /var/log/nginx/your.domain.access.log;
|
||||
error_log /var/log/nginx/your.domain.error.log;
|
||||
|
||||
# Security
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_session_tickets off;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-
|
||||
GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SH
|
||||
A:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM
|
||||
-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
|
||||
ssl_prefer_server_ciphers on;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
resolver 8.8.8.8 8.8.4.4 valid=300s;
|
||||
resolver_timeout 5s;
|
||||
|
||||
client_max_body_size 100M;
|
||||
client_body_timeout 600s;
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://backend;
|
||||
proxy_redirect off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
### Sites using chibisafe
|
||||
|
||||
As you are aware by now chibisafe is a project meant to be self-hosted and so we've compiled the list below of sites using it. Some of them are open for registration and some might even offer anonymous uploads.
|
||||
|
||||
- [chibisafe.moe](https://chibisafe.moe): A small safe worth protecting.
|
||||
- [dmca.gripe](https://dmca.gripe): a dmca-resistant, permanent file hosting service. *(v3)*
|
||||
- [safe.succmy.wang](https://safe.succmy.wang): A private clone with a ~~funny~~ bad name
|
||||
- [discordjs.moe](https://discordjs.moe): A まじ卍 as fuck copy of lolisafe.moe
|
||||
- Feel free to make a PR to add your site here.
|
|
@ -0,0 +1,30 @@
|
|||
### Service config for systemd
|
||||
If you want to keep Chibisafe running by using systemd you can copy the example code shown below and create the file `/etc/systemd/system/chibisafe.service` with it.
|
||||
|
||||
You will need to edit the parameters:
|
||||
- `User` to be the username/uid of your chibisafe instance
|
||||
- `WorkingDirectory` to the **FULL** path to your chibisafe, `/home/chibisafe/chibisafe` for example.
|
||||
- `EnvironmentFile` the same as the above, with the addition of `/.env`, `/home/chibisafe/chibisafe/.env`
|
||||
|
||||
### If you are using n/nvm you will also need to update the path to npm in `ExecStart`
|
||||
- For n this will likely be `/home/username/n/bin/npm`
|
||||
- You can also find this by running `whereis npm` in your terminal and copy the path from the output.
|
||||
|
||||
Example below.
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=chibisafe, easy to use file uploader
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=chibisafe
|
||||
WorkingDirectory=/home/chibisafe/chibisafe
|
||||
EnvironmentFile=/home/chibisafe/chibisafe/.env
|
||||
ExecStart=/usr/bin/npm run start
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
moduleFileExtensions: ['js', 'json', 'vue'],
|
||||
moduleDirectories: ['node_modules'],
|
||||
transform: {
|
||||
'^.+\\.js$': '<rootDir>/node_modules/babel-jest',
|
||||
'.*\\.(vue)$': '<rootDir>/node_modules/vue-jest'
|
||||
},
|
||||
transformIgnorePatterns: ['/node_modules/(?!vue)']
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
require('dotenv').config();
|
||||
|
||||
module.exports = {
|
||||
client: process.env.DB_CLIENT,
|
||||
connection: {
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_DATABASE,
|
||||
filename: 'database/database.sqlite'
|
||||
},
|
||||
pool: {
|
||||
min: process.env.DATABASE_POOL_MIN || 2,
|
||||
max: process.env.DATABASE_POOL_MAX || 10
|
||||
},
|
||||
migrations: {
|
||||
directory: 'src/api/database/migrations'
|
||||
},
|
||||
seeds: {
|
||||
directory: 'src/api/database/seeds'
|
||||
},
|
||||
useNullAsDefault: process.env.DB_CLIENT === 'sqlite3'
|
||||
};
|
55
lolisafe.js
|
@ -1,55 +0,0 @@
|
|||
const config = require('./config.js')
|
||||
const api = require('./routes/api.js')
|
||||
const album = require('./routes/album.js')
|
||||
const express = require('express')
|
||||
const helmet = require('helmet')
|
||||
const bodyParser = require('body-parser')
|
||||
const RateLimit = require('express-rate-limit')
|
||||
const db = require('knex')(config.database)
|
||||
const fs = require('fs')
|
||||
const exphbs = require('express-handlebars')
|
||||
const safe = express()
|
||||
|
||||
require('./database/db.js')(db)
|
||||
|
||||
fs.existsSync('./pages/custom' ) || fs.mkdirSync('./pages/custom')
|
||||
fs.existsSync('./' + config.logsFolder) || fs.mkdirSync('./' + config.logsFolder)
|
||||
fs.existsSync('./' + config.uploads.folder) || fs.mkdirSync('./' + config.uploads.folder)
|
||||
fs.existsSync('./' + config.uploads.folder + '/thumbs') || fs.mkdirSync('./' + config.uploads.folder + '/thumbs')
|
||||
|
||||
safe.use(helmet())
|
||||
safe.set('trust proxy', 1)
|
||||
|
||||
safe.engine('handlebars', exphbs({defaultLayout: 'main'}))
|
||||
safe.set('view engine', 'handlebars')
|
||||
safe.enable('view cache')
|
||||
|
||||
let limiter = new RateLimit({ windowMs: 5000, max: 2 })
|
||||
safe.use('/api/login/', limiter)
|
||||
safe.use('/api/register/', limiter)
|
||||
|
||||
safe.use(bodyParser.urlencoded({ extended: true }))
|
||||
safe.use(bodyParser.json())
|
||||
|
||||
safe.use('/', express.static('./uploads'))
|
||||
safe.use('/', express.static('./public'))
|
||||
safe.use('/', album)
|
||||
safe.use('/api', api)
|
||||
|
||||
|
||||
for (let page of config.pages) {
|
||||
let root = './pages/'
|
||||
if (fs.existsSync(`./pages/custom/${page}.html`)) {
|
||||
root = './pages/custom/'
|
||||
}
|
||||
if (page === 'home') {
|
||||
safe.get('/', (req, res, next) => res.sendFile(`${page}.html`, { root: root }))
|
||||
} else {
|
||||
safe.get(`/${page}`, (req, res, next) => res.sendFile(`${page}.html`, { root: root }))
|
||||
}
|
||||
}
|
||||
|
||||
safe.use((req, res, next) => res.status(404).sendFile('404.html', { root: './pages/error/' }))
|
||||
safe.use((req, res, next) => res.status(500).sendFile('500.html', { root: './pages/error/' }))
|
||||
|
||||
safe.listen(config.port, () => console.log(`loli-safe started on port ${config.port}`))
|
|
@ -0,0 +1,92 @@
|
|||
import dotenv from 'dotenv/config';
|
||||
import autoprefixer from 'autoprefixer';
|
||||
|
||||
const Util = require('./src/api/utils/Util');
|
||||
|
||||
export default async () => {
|
||||
/*
|
||||
FIXME:
|
||||
Since Util.config is not populated during production env because it needs to grab the values from the db
|
||||
we need to use this hack to populate it before we can access the properties without await like we do in the export below.
|
||||
This will be solved once the TypeScript rewrite is complete as we can can simply pass a config object to express
|
||||
and build from there, but for now the build needs to be triggered before the API is started.
|
||||
*/
|
||||
await Util.config;
|
||||
return {
|
||||
ssr: true,
|
||||
srcDir: 'src/site/',
|
||||
head: {
|
||||
title: Util.config.serviceName,
|
||||
titleTemplate: `%s | ${Util.config.serviceName}`,
|
||||
// TODO: Add the directory with pictures for favicon and stuff
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ hid: 'theme-color', name: 'theme-color', content: `${Util.config.metaThemeColor}` },
|
||||
{ hid: 'description', name: 'description', content: `${Util.config.metaDescription}` },
|
||||
{ hid: 'keywords', name: 'keywords', content: `${Util.config.metaKeywords}` },
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: `${Util.config.serviceName}`
|
||||
},
|
||||
{ hid: 'application-name', name: 'application-name', content: `${Util.config.serviceName}` },
|
||||
{ hid: 'twitter:card', name: 'twitter:card', content: 'summary' },
|
||||
{ hid: 'twitter:site', name: 'twitter:site', content: `${Util.config.metaTwitterHandle}` },
|
||||
{ hid: 'twitter:creator', name: 'twitter:creator', content: `${Util.config.metaTwitterHandle}` },
|
||||
{ hid: 'twitter:title', name: 'twitter:title', content: `${Util.config.serviceName}` },
|
||||
{ hid: 'twitter:description', name: 'twitter:description', content: `${Util.config.metaDescription}` },
|
||||
{ hid: 'twitter:image', name: 'twitter:image', content: `/logo.png` },
|
||||
{ hid: 'og:url', property: 'og:url', content: `/` },
|
||||
{ hid: 'og:type', property: 'og:type', content: 'website' },
|
||||
{ hid: 'og:title', property: 'og:title', content: `${Util.config.serviceName}` },
|
||||
{ hid: 'og:description', property: 'og:description', content: `${Util.config.metaDescription}` },
|
||||
{ hid: 'og:image', property: 'og:image', content: `/logo.png` },
|
||||
{ hid: 'og:image:secure_url', property: 'og:image:secure_url', content: `/logo.png` },
|
||||
{ hid: 'og:site_name', property: 'og:site_name', content: `${Util.config.serviceName}` }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Nunito:300,400,600,700' },
|
||||
|
||||
// This one is a pain in the ass to make it customizable, so you should edit it manually
|
||||
{ type: 'application/json+oembed', href: `/oembed.json` }
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
'~/plugins/axios',
|
||||
'~/plugins/buefy',
|
||||
'~/plugins/v-clipboard',
|
||||
'~/plugins/vue-isyourpasswordsafe',
|
||||
'~/plugins/vue-timeago',
|
||||
'~/plugins/vuebar',
|
||||
'~/plugins/notifier',
|
||||
'~/plugins/handler'
|
||||
],
|
||||
css: [],
|
||||
modules: ['@nuxtjs/axios', 'cookie-universal-nuxt'],
|
||||
router: {
|
||||
linkActiveClass: 'is-active',
|
||||
linkExactActiveClass: 'is-active'
|
||||
},
|
||||
env: {
|
||||
development: process.env.NODE_ENV !== 'production'
|
||||
},
|
||||
axios: {
|
||||
baseURL: `${process.env.NODE_ENV === 'production' ? process.env.DOMAIN : 'http://localhost:5000'}/api`
|
||||
},
|
||||
build: {
|
||||
extractCSS: process.env.NODE_ENV === 'production',
|
||||
postcss: {
|
||||
preset: {
|
||||
autoprefixer
|
||||
}
|
||||
},
|
||||
extend(config, { isDev }) {
|
||||
// Extend only webpack config for client-bundle
|
||||
if (isDev) {
|
||||
config.devtool = 'source-map';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
181
package.json
|
@ -1,31 +1,154 @@
|
|||
{
|
||||
"name": "loli-safe",
|
||||
"version": "2.2.0",
|
||||
"description": "Pomf-like uploading service, written in node",
|
||||
"author": "kanadeko",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kanadeko/loli-safe"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/kanadeko/loli-safe/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bcrypt": "^1.0.2",
|
||||
"body-parser": "^1.16.0",
|
||||
"express": "^4.14.0",
|
||||
"express-handlebars": "^3.0.0",
|
||||
"express-rate-limit": "^2.6.0",
|
||||
"fluent-ffmpeg": "^2.1.0",
|
||||
"gm": "^1.23.0",
|
||||
"helmet": "^3.5.0",
|
||||
"knex": "^0.12.6",
|
||||
"multer": "^1.2.1",
|
||||
"randomstring": "^1.1.5",
|
||||
"sqlite3": "^3.1.11"
|
||||
}
|
||||
"name": "chibisafe",
|
||||
"version": "4.0.2",
|
||||
"description": "Blazing fast file uploader and bunker written in node! 🚀",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Pitu",
|
||||
"email": "heyitspitu@gmail.com",
|
||||
"url": "https://github.com/Pitu"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "node src/setup.js && npm run migrate && npm run seed",
|
||||
"start": "npm run migrate && nuxt build && cross-env NODE_ENV=production node src/api/structures/Server",
|
||||
"dev": "nodemon src/api/structures/Server",
|
||||
"migrate": "knex migrate:latest",
|
||||
"seed": "knex seed:run",
|
||||
"restart": "pm2 restart chibisafe",
|
||||
"overwrite-config": "cross-env OVERWRITE_SETTINGS=true",
|
||||
"test:vue": "jest --testPathPattern=src/site",
|
||||
"test:api": "jest --testPathPattern=src/tests/api",
|
||||
"test:e2e": "jest --testPathPattern=src/tests/e2e",
|
||||
"tests": "npm run test:api && npm run test:vue && npm run test:e2e",
|
||||
"sqlite": "sqlite_web -p 5001 database/database.sqlite"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/WeebDev/chibisafe"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/WeebDev/chibisafe/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^5.8.55",
|
||||
"@nuxtjs/axios": "^5.12.5",
|
||||
"adm-zip": "^0.4.13",
|
||||
"bcrypt": "^5.0.1",
|
||||
"blake3": "^2.1.4",
|
||||
"body-parser": "^1.18.3",
|
||||
"buefy": "^0.9.4",
|
||||
"busboy": "^0.2.14",
|
||||
"chalk": "^2.4.1",
|
||||
"chrono-node": "^2.1.4",
|
||||
"compression": "^1.7.2",
|
||||
"cookie-universal-nuxt": "^2.0.14",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^1.8.2",
|
||||
"dotenv": "^6.2.0",
|
||||
"dumper.js": "^1.3.1",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^3.4.0",
|
||||
"ffmpeg-probe": "^1.0.6",
|
||||
"file-saver": "^2.0.1",
|
||||
"file-type": "^16.1.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-jetpack": "^2.2.2",
|
||||
"helmet": "^3.15.1",
|
||||
"imagesloaded": "^4.1.4",
|
||||
"joi": "^17.3.0",
|
||||
"jsonwebtoken": "^8.5.0",
|
||||
"knex": "^0.21.15",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"moment": "^2.24.0",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.1",
|
||||
"mysql": "^2.16.0",
|
||||
"nuxt": "^2.14.12",
|
||||
"nuxt-dropzone": "^0.2.8",
|
||||
"pg": "^7.8.1",
|
||||
"qoa": "^0.2.0",
|
||||
"randomstring": "^1.1.5",
|
||||
"rotating-file-stream": "^2.1.3",
|
||||
"search-query-parser": "^1.5.5",
|
||||
"serve-static": "^1.13.2",
|
||||
"sharp": "^0.29.0",
|
||||
"sqlite3": "^5.0.0",
|
||||
"systeminformation": "^4.34.5",
|
||||
"uuid": "^3.3.2",
|
||||
"v-clipboard": "^2.2.1",
|
||||
"vue-axios": "^2.1.4",
|
||||
"vue-isyourpasswordsafe": "^1.0.2",
|
||||
"vue-plyr": "^5.1.0",
|
||||
"vue-timeago": "^3.4.4",
|
||||
"vue2-transitions": "^0.2.3",
|
||||
"vuebar": "^0.0.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@vue/test-utils": "^1.1.2",
|
||||
"autoprefixer": "^9.4.7",
|
||||
"axios": "^0.21.1",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^26.6.3",
|
||||
"cross-env": "^5.2.0",
|
||||
"eslint": "^7.17.0",
|
||||
"eslint-config-aqua": "^7.3.0",
|
||||
"eslint-import-resolver-nuxt": "^1.0.1",
|
||||
"eslint-plugin-vue": "^5.2.1",
|
||||
"jest": "^26.6.3",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"nodemon": "^1.19.4",
|
||||
"postcss-css-variables": "^0.11.0",
|
||||
"postcss-nested": "^3.0.0",
|
||||
"puppeteer": "^5.5.0",
|
||||
"sass": "^1.48.0",
|
||||
"sass-loader": "^10.1.0",
|
||||
"vue-jest": "^3.0.7"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"aqua/node",
|
||||
"aqua/vue"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"nuxt": {
|
||||
"nuxtSrcDir": "./src/site",
|
||||
"extensions": [
|
||||
".js",
|
||||
".vue"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"watch": [
|
||||
"src/api/*"
|
||||
],
|
||||
"delay": 2500
|
||||
},
|
||||
"keywords": [
|
||||
"chibisafe",
|
||||
"lolisafe",
|
||||
"upload",
|
||||
"uploader",
|
||||
"file",
|
||||
"vue",
|
||||
"ssr",
|
||||
"file uploader",
|
||||
"images"
|
||||
],
|
||||
"volta": {
|
||||
"node": "14.17.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
|
||||
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
|
||||
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
|
||||
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
|
||||
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
|
||||
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
|
||||
<meta name="apple-mobile-web-app-title" content="lolisafe">
|
||||
<meta name="application-name" content="lolisafe">
|
||||
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<meta property="og:url" content="https://lolisafe.moe" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
|
||||
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
|
||||
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
|
||||
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
|
||||
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
|
||||
<meta name="twitter:image" content="https://listen.moe/files/images/logo_square.png">
|
||||
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
|
||||
|
||||
<title>lolisafe - A small safe worth protecting.</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.0/css/bulma.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/style.css">
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js"></script>
|
||||
<script type="text/javascript" src="/js/album.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<section class="hero is-fullheight">
|
||||
<div class="hero-head">
|
||||
<div class="container">
|
||||
<h1 class="title" id='title' style='margin-top: 1.5rem;'></h1>
|
||||
<h1 class="subtitle" id='count'></h1>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-body">
|
||||
<div class="container" id='container'>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,87 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
|
||||
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
|
||||
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
|
||||
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
|
||||
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
|
||||
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
|
||||
<meta name="apple-mobile-web-app-title" content="lolisafe">
|
||||
<meta name="application-name" content="lolisafe">
|
||||
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<meta property="og:url" content="https://lolisafe.moe" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
|
||||
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
|
||||
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
|
||||
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
|
||||
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
|
||||
<meta name="twitter:image" content="https://listen.moe/files/images/logo_square.png">
|
||||
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
|
||||
|
||||
<title>lolisafe - A small safe worth protecting.</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.0/css/bulma.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/style.css">
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js"></script>
|
||||
<script type="text/javascript" src="https://use.fontawesome.com/cd26baa9bd.js"></script>
|
||||
<script type="text/javascript" src="/js/auth.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<style type="text/css">
|
||||
section#login {
|
||||
background-color: #f5f6f8;
|
||||
}
|
||||
</style>
|
||||
|
||||
<section id='login' class="hero is-fullheight">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
Dashboard Access
|
||||
</h1>
|
||||
<h2 class="subtitle">
|
||||
Login or register
|
||||
</h2>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<p class="control">
|
||||
<input id='user' class="input" type="text" placeholder="Your username">
|
||||
</p>
|
||||
<p class="control">
|
||||
<input id='pass' class="input" type="password" placeholder="Your password">
|
||||
</p>
|
||||
|
||||
<p class="control has-addons is-pulled-right">
|
||||
<a class="button" id='registerBtn' onclick="page.do('register')">
|
||||
<span>Register</span>
|
||||
</a>
|
||||
<a class="button" id='loginBtn' onclick="page.do('login')">
|
||||
<span>Log in</span>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="column is-hidden-mobile"></div>
|
||||
<div class="column is-hidden-mobile"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,100 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
|
||||
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
|
||||
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
|
||||
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
|
||||
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
|
||||
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
|
||||
<meta name="apple-mobile-web-app-title" content="lolisafe">
|
||||
<meta name="application-name" content="lolisafe">
|
||||
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<meta property="og:url" content="https://lolisafe.moe" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
|
||||
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
|
||||
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
|
||||
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
|
||||
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
|
||||
<meta name="twitter:image" content="https://listen.moe/files/images/logo_square.png">
|
||||
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
|
||||
|
||||
<title>lolisafe - A small safe worth protecting.</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.0/css/bulma.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/style.css">
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js"></script>
|
||||
<script type="text/javascript" src="https://use.fontawesome.com/cd26baa9bd.js"></script>
|
||||
<script type="text/javascript" src="/js/dashboard.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section id='auth' class="hero is-light is-fullheight">
|
||||
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
Admin dashboard
|
||||
</h1>
|
||||
<h2 class="subtitle">
|
||||
<p class="control has-addons">
|
||||
<input id='token' class="input is-danger" type="text" placeholder="Your admin token">
|
||||
<a id='tokenSubmit' class="button is-danger is-outlined">Check</a>
|
||||
</p>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section id='dashboard' class="section">
|
||||
|
||||
<div id="panel" class="container">
|
||||
<h1 class="title">Dashboard</h1>
|
||||
<h2 class="subtitle">A simple <strong>dashboard</strong>, to sort your uploaded stuff</h2>
|
||||
<hr>
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<aside class="menu" id="menu">
|
||||
<p class="menu-label">General</p>
|
||||
<ul class="menu-list">
|
||||
<li><a href="/">Frontpage</a></li>
|
||||
<li><a id="itemUploads" onclick="panel.getUploads()">Uploads</a></li>
|
||||
</ul>
|
||||
<p class="menu-label">Albums</p>
|
||||
<ul class="menu-list">
|
||||
<li><a id="itemManageGallery" onclick="panel.getAlbums()">Manage your albums</a></li>
|
||||
<li>
|
||||
<ul id='albumsContainer'></ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="menu-label">Administration</p>
|
||||
<ul class="menu-list">
|
||||
<li><a id="itemTokens" onclick="panel.changeToken()">Change your token</a></li>
|
||||
<li><a id="itemPassword" onclick="panel.changePassword()">Change your password</a></li>
|
||||
<li><a id="itemLogout"onclick="panel.logout()">Logout</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="column has-text-centered" id='page'>
|
||||
<img src="/images/logo.png">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
|
@ -1,47 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>loli-safe</title>
|
||||
|
||||
<link href='//fonts.googleapis.com/css?family=Lato:100' rel='stylesheet' type='text/css'>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
color: #B0BEC5;
|
||||
display: table;
|
||||
font-weight: 100;
|
||||
font-family: 'Lato';
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 72px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<div class="title">Page not found.</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,47 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>loli-safe</title>
|
||||
|
||||
<link href='//fonts.googleapis.com/css?family=Lato:100' rel='stylesheet' type='text/css'>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
color: #B0BEC5;
|
||||
display: table;
|
||||
font-weight: 100;
|
||||
font-family: 'Lato';
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 72px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<div class="title">Internal server error.</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,83 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
|
||||
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
|
||||
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
|
||||
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
|
||||
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
|
||||
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
|
||||
<meta name="apple-mobile-web-app-title" content="lolisafe">
|
||||
<meta name="application-name" content="lolisafe">
|
||||
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<meta property="og:url" content="https://lolisafe.moe" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
|
||||
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
|
||||
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
|
||||
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
|
||||
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
|
||||
<meta name="twitter:image" content="https://listen.moe/files/images/logo_square.png">
|
||||
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
|
||||
|
||||
<title>lolisafe - A small safe worth protecting.</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.0/css/bulma.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section class="hero is-fullheight has-text-centered" id="home">
|
||||
<div class="hero-body">
|
||||
<div class="container has-text-left">
|
||||
|
||||
<h2 class='subtitle'>What is lolisafe?</h2>
|
||||
<article class="message">
|
||||
<div class="message-body">
|
||||
lolisafe is an easy to use, open source and completely free file upload service. We accept your files, photos, documents, anything, and give you back a shareable link for you to send to others.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<h2 class='subtitle'>Will you keep my files forever?</h2>
|
||||
<article class="message">
|
||||
<div class="message-body">
|
||||
Unless we receive a copyright complain or some other bullshit, we will.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<h2 class='subtitle'>How can I keep track of my uploads?</h2>
|
||||
<article class="message">
|
||||
<div class="message-body">
|
||||
Simply create a user on the site and every upload will be associated with your account, granting you access to your uploaded files through our dashboard.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<h2 class='subtitle'>What are albums?</h2>
|
||||
<article class="message">
|
||||
<div class="message-body">
|
||||
Albums are a simple way of sorting uploads together. Right now you can create albums through the dashboard and use them only with <a target="_blank" href="https://chrome.google.com/webstore/detail/loli-safe-uploader/enkkmplljfjppcdaancckgilmgoiofnj">our chrome extension</a> which will enable you to <strong>right click -> send to lolisafe</strong> or to a desired album if you have any.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<h2 class='subtitle'>Why should I use this?</h2>
|
||||
<article class="message">
|
||||
<div class="message-body">
|
||||
There are too many file upload services out there, and a lot of them rely on the foundations of pomf which is ancient. In a desperate and unsuccessful attempt of finding a good file uploader that's easily extendable, lolisafe was born. We give you control over your files, we give you a way to sort your uploads into albums for ease of access and we give you an api to use with ShareX or any other thing that let's you make POST requests. Awesome isn't it? Just like you.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,93 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
|
||||
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
|
||||
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
|
||||
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
|
||||
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
|
||||
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
|
||||
<meta name="apple-mobile-web-app-title" content="lolisafe">
|
||||
<meta name="application-name" content="lolisafe">
|
||||
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<meta property="og:url" content="https://lolisafe.moe" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
|
||||
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
|
||||
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
|
||||
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
|
||||
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
|
||||
<meta name="twitter:image" content="https://listen.moe/files/images/logo_square.png">
|
||||
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
|
||||
|
||||
<title>lolisafe - A small safe worth protecting.</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.0/css/bulma.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/style.css">
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/4.3.0/min/dropzone.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js"></script>
|
||||
<script type="text/javascript" src="/js/home.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section class="hero is-fullheight has-text-centered" id="home">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<p id="b">
|
||||
<img class="logo" src="/images/logo_smol.png">
|
||||
</p>
|
||||
<h1 class="title">loli-safe</h1>
|
||||
<h2 class="subtitle">A <strong>modern</strong> self-hosted file upload service</h2>
|
||||
|
||||
<h3 class="subtitle" id="maxFileSize"></h3>
|
||||
<div class="columns">
|
||||
<div class="column is-hidden-mobile"></div>
|
||||
<div class="column" id="uploadContainer">
|
||||
<a id="loginToUpload" href="/auth" class="button is-danger">Running in private mode. Log in to upload.</a>
|
||||
<div class="field" id="albumDiv" style="display: none">
|
||||
<p class="control select-wrapper">
|
||||
<span class="select">
|
||||
<select id="albumSelect">
|
||||
<option value="">Upload to album</option>
|
||||
</select>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-hidden-mobile"></div>
|
||||
</div>
|
||||
|
||||
<div id="uploads">
|
||||
<div id="template" class="columns">
|
||||
<div class="column is-hidden-mobile"></div>
|
||||
<div class="column">
|
||||
<progress class="progress is-small is-danger" value="0" max="100" data-dz-uploadprogress></progress>
|
||||
<p data-dz-errormessage></p>
|
||||
<p class="link"></p>
|
||||
</div>
|
||||
<div class="column is-hidden-mobile"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="subtitle"><a href="/auth" id="loginLinkText"></a></h3>
|
||||
<h3 id="links">
|
||||
<a href="https://github.com/kanadeko/loli-safe" target="_blank" class="is-danger">View on Github</a><span>|</span><a href="https://lolisafe.moe/sharex.txt">ShareX</a><span>|</span><a href="https://chrome.google.com/webstore/detail/loli-safe-uploader/enkkmplljfjppcdaancckgilmgoiofnj" target="_blank" class="is-danger">Chrome extension</a><span>|</span><a href="/faq" class="is-danger">FAQ</a><span>|</span><a href="/auth" target="_blank" class="is-danger">Dashboard</a>
|
||||
</h3>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"apps": [
|
||||
{
|
||||
"name": "chibisafe",
|
||||
"script": "npm",
|
||||
"args": "run start",
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
},
|
||||
"env_production": {
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
/* ------------------
|
||||
HOME
|
||||
------------------ */
|
||||
|
||||
section#home #b {
|
||||
-webkit-animation-delay: 0.5s;
|
||||
animation-delay: 0.5s;
|
||||
-webkit-animation-duration: 1.5s;
|
||||
animation-duration: 1.5s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
-webkit-animation-name: floatUp;
|
||||
animation-name: floatUp;
|
||||
-webkit-animation-timing-function: cubic-bezier(0, 0.71, 0.29, 1);
|
||||
animation-timing-function: cubic-bezier(0, 0.71, 0.29, 1);
|
||||
border-radius: 24px;
|
||||
display: inline-block;
|
||||
height: 240px;
|
||||
margin-bottom: 40px;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
width: 240px;
|
||||
box-shadow: 0 20px 60px rgba(10, 10, 10, 0.05), 0 5px 10px rgba(10, 10, 10, 0.1), 0 1px 1px rgba(10, 10, 10, 0.2);
|
||||
}
|
||||
|
||||
section#home div#dropzone {
|
||||
border: 1px solid #dbdbdb;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border-color: #ff3860;
|
||||
color: #ff3860;
|
||||
display: none;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
box-shadow: none;
|
||||
height: 2.5em;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
justify-content: center;
|
||||
padding-left: .75em;
|
||||
padding-right: .75em;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
section#home div#uploads, section#home p#tokenContainer, section#home a#panel { display: none; }
|
||||
section#home div#dropzone:hover { background-color: #ff3860; border-color: #ff3860; color: #fff; }
|
||||
section#home h3#maxFileSize { font-size: 14px; }
|
||||
section#home h3#links span { padding-left: 5px; padding-right: 5px; }
|
||||
section#home img.logo { height: 200px; margin-top: 20px; }
|
||||
section#home .dz-preview .dz-details { display: flex; }
|
||||
section#home .dz-preview .dz-details .dz-size, section#home .dz-preview .dz-details .dz-filename { flex: 1; }
|
||||
section#home .dz-preview img, section#home .dz-preview .dz-success-mark, section#home .dz-preview .dz-error-mark { display: none; }
|
||||
section#home div#uploads { margin-bottom: 25px; }
|
||||
|
||||
@keyframes floatUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
box-shadow: 0 0 0 rgba(10, 10, 10, 0), 0 0 0 rgba(10, 10, 10, 0), 0 0 0 rgba(10, 10, 10, 0);
|
||||
-webkit-transform: scale(0.86);
|
||||
transform: scale(0.86);
|
||||
}
|
||||
25% { opacity: 100; }
|
||||
67% {
|
||||
box-shadow: 0 0 0 rgba(10, 10, 10, 0), 0 5px 10px rgba(10, 10, 10, 0.1), 0 1px 1px rgba(10, 10, 10, 0.2);
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 20px 60px rgba(10, 10, 10, 0.05), 0 5px 10px rgba(10, 10, 10, 0.1), 0 1px 1px rgba(10, 10, 10, 0.2);
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------
|
||||
PANEL
|
||||
------------------ */
|
||||
|
||||
section#login input, section#login p.control a.button {
|
||||
border-left: 0px;
|
||||
border-top: 0px;
|
||||
border-right: 0px;
|
||||
border-radius: 0px;
|
||||
box-shadow: 0 0 0;
|
||||
}
|
||||
|
||||
section#login p.control a.button { margin-left: 10px; }
|
||||
section#login p.control a#loginBtn { border-right: 0px; }
|
||||
section#login p.control a#registerBtn { border-left: 0px; }
|
||||
|
||||
|
||||
section#auth, section#dashboard { display: none }
|
||||
section#auth input { background: rgba(0, 0, 0, 0); }
|
||||
section#auth input, section#auth a {
|
||||
border-left: 0px;
|
||||
border-top: 0px;
|
||||
border-right: 0px;
|
||||
border-radius: 0px;
|
||||
box-shadow: 0 0 0;
|
||||
}
|
||||
|
||||
section#dashboard .table { font-size: 12px }
|
||||
section#dashboard div#table div.column { display:flex; width: 200px; height: 200px; margin: 9px; background: #f9f9f9; overflow: hidden; align-items: center; }
|
||||
section#dashboard div#table div.column a { width: 100%; }
|
||||
section#dashboard div#table div.column a img { width:200px; }
|
||||
|
||||
.select-wrapper {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
Before Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 7.9 KiB |
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/images/icons/mstile-150x150.png?v=XBreOJMe24"/>
|
||||
<TileColor>#00aba9</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
Before Width: | Height: | Size: 920 B |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 15 KiB |
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name": "lolisafe",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/icons/android-chrome-192x192.png?v=XBreOJMe24",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/images/icons/android-chrome-384x384.png?v=XBreOJMe24",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
Before Width: | Height: | Size: 6.1 KiB |
|
@ -1,47 +0,0 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,16.000000) scale(0.003765,-0.003765)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2397 4143 c-3 -3 -19 -7 -34 -8 -162 -16 -259 -56 -351 -144 -65
|
||||
-64 -123 -152 -156 -239 -22 -61 -27 -80 -40 -148 -6 -29 -18 -161 -20 -209
|
||||
-1 -40 -2 -40 -81 -80 l-81 -40 58 -50 c32 -28 70 -55 85 -62 l28 -11 -25 -10
|
||||
-25 -10 29 -13 28 -13 -6 -106 c-3 -58 -7 -114 -9 -125 -3 -11 -8 -45 -11 -76
|
||||
-4 -31 -9 -67 -11 -80 -3 -13 -12 -60 -20 -104 -21 -108 -99 -327 -177 -497
|
||||
-29 -64 -32 -67 -62 -64 -17 2 -36 4 -42 5 -25 2 -95 171 -108 259 -9 62 -14
|
||||
55 -73 -98 -35 -89 -48 -133 -59 -190 -2 -14 -6 -34 -9 -45 -21 -99 -22 -255
|
||||
-2 -370 8 -44 14 -81 13 -82 -5 -6 -97 76 -131 117 -47 57 -97 151 -111 205
|
||||
-30 123 -36 217 -19 300 8 41 -6 24 -48 -60 -36 -72 -72 -169 -81 -221 -3 -16
|
||||
-10 -44 -16 -63 -10 -36 -7 -255 4 -304 4 -16 15 -51 26 -79 11 -28 17 -54 15
|
||||
-58 -14 -22 -177 11 -235 48 l-25 16 16 -25 c27 -42 141 -127 204 -153 33 -14
|
||||
86 -32 117 -41 48 -14 58 -20 63 -44 3 -15 31 -65 61 -112 31 -46 68 -109 84
|
||||
-139 15 -31 41 -67 57 -80 16 -14 45 -45 64 -70 19 -25 37 -47 40 -50 3 -3 34
|
||||
-43 69 -90 109 -146 240 -259 412 -353 25 -14 65 -38 89 -53 48 -30 175 -79
|
||||
229 -88 117 -19 211 -37 250 -49 47 -14 85 -14 160 -3 131 21 171 87 171 281
|
||||
-1 73 -3 93 -12 110 -4 6 -9 22 -13 37 -6 26 -54 109 -159 278 -26 41 -55 85
|
||||
-63 97 -14 20 -14 23 -2 23 9 0 32 -21 52 -47 99 -128 366 -377 506 -473 12
|
||||
-8 43 -30 68 -49 38 -29 57 -35 116 -41 128 -13 166 5 214 103 81 161 76 270
|
||||
-19 438 -23 41 -45 76 -48 79 -3 3 -31 34 -61 70 -30 36 -57 67 -61 70 -15 13
|
||||
-169 222 -169 231 0 6 7 23 15 38 26 51 56 185 61 271 3 47 7 93 9 103 5 27
|
||||
-8 20 -38 -22 -15 -22 -45 -54 -67 -71 l-40 -32 -1 89 c0 48 -3 81 -7 73 -11
|
||||
-25 -41 -64 -46 -59 -2 2 -7 33 -11 69 -7 65 -7 66 22 80 15 7 30 14 33 14 3
|
||||
1 26 13 52 28 56 34 121 97 157 153 22 35 30 41 47 34 47 -17 103 -115 91
|
||||
-160 -6 -22 8 -26 17 -4 3 8 8 36 12 63 10 79 -28 143 -98 165 -15 5 -15 10
|
||||
-4 49 31 104 15 279 -35 384 -19 40 -29 78 -29 107 0 48 -12 61 -43 45 -9 -5
|
||||
-23 -5 -31 -1 -31 19 -74 61 -91 91 -13 21 -35 38 -62 48 -24 10 -42 22 -40
|
||||
28 3 6 9 10 15 9 9 -2 52 84 67 137 4 14 25 42 47 62 22 20 37 42 33 48 -3 5
|
||||
-11 7 -17 4 -6 -4 -6 1 1 14 6 12 16 24 22 27 6 4 23 25 39 47 29 40 29 41 10
|
||||
57 -17 13 -18 22 -11 54 5 21 7 50 6 65 -2 15 -6 52 -9 82 -10 114 -77 279
|
||||
-158 387 -46 63 -167 162 -247 202 -97 49 -263 91 -276 70 -4 -5 -13 -5 -24 1
|
||||
-10 5 -21 7 -24 3z"/>
|
||||
<path d="M3111 3011 c-13 -13 -21 -35 -22 -58 0 -21 -1 -53 -2 -71 -1 -17 2
|
||||
-32 6 -32 4 0 29 11 55 24 74 37 87 86 27 98 -12 2 -19 10 -17 20 1 8 -4 21
|
||||
-12 27 -12 10 -19 8 -35 -8z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 154 KiB |
Before Width: | Height: | Size: 4.0 MiB |
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 136 KiB |
|
@ -1,56 +0,0 @@
|
|||
var page = {};
|
||||
|
||||
page.do = function(dest){
|
||||
|
||||
var user = document.getElementById('user').value;
|
||||
var pass = document.getElementById('pass').value;
|
||||
|
||||
if(user === undefined || user === null || user === '')
|
||||
return swal('Error', 'You need to specify a username', 'error');
|
||||
if(pass === undefined || pass === null || pass === '')
|
||||
return swal('Error', 'You need to specify a username', 'error');
|
||||
|
||||
axios.post('/api/' + dest, {
|
||||
username: user,
|
||||
password: pass
|
||||
})
|
||||
.then(function (response) {
|
||||
|
||||
if(response.data.success === false)
|
||||
return swal('Error', response.data.description, 'error');
|
||||
|
||||
localStorage.token = response.data.token;
|
||||
window.location = '/dashboard';
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
return swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error');
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
page.verify = function(){
|
||||
page.token = localStorage.token;
|
||||
if(page.token === undefined) return;
|
||||
|
||||
axios.post('/api/tokens/verify', {
|
||||
token: page.token
|
||||
})
|
||||
.then(function (response) {
|
||||
|
||||
if(response.data.success === false)
|
||||
return swal('Error', response.data.description, 'error');
|
||||
|
||||
window.location = '/dashboard';
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
return swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error');
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
page.verify();
|
||||
}
|
|
@ -1,624 +0,0 @@
|
|||
let panel = {}
|
||||
|
||||
panel.page;
|
||||
panel.username;
|
||||
panel.token = localStorage.token;
|
||||
panel.filesView = localStorage.filesView;
|
||||
|
||||
panel.preparePage = function(){
|
||||
if(!panel.token) return window.location = '/auth';
|
||||
panel.verifyToken(panel.token, true);
|
||||
}
|
||||
|
||||
panel.verifyToken = function(token, reloadOnError){
|
||||
if(reloadOnError === undefined)
|
||||
reloadOnError = false;
|
||||
|
||||
axios.post('/api/tokens/verify', {
|
||||
token: token
|
||||
})
|
||||
.then(function (response) {
|
||||
|
||||
if(response.data.success === false){
|
||||
swal({
|
||||
title: "An error ocurred",
|
||||
text: response.data.description,
|
||||
type: "error"
|
||||
}, function(){
|
||||
if(reloadOnError){
|
||||
localStorage.removeItem("token");
|
||||
location.location = '/auth';
|
||||
}
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
axios.defaults.headers.common['token'] = token;
|
||||
localStorage.token = token;
|
||||
panel.token = token;
|
||||
panel.username = response.data.username;
|
||||
return panel.prepareDashboard();
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
panel.prepareDashboard = function(){
|
||||
panel.page = document.getElementById('page');
|
||||
document.getElementById('auth').style.display = 'none';
|
||||
document.getElementById('dashboard').style.display = 'block';
|
||||
|
||||
document.getElementById('itemUploads').addEventListener('click', function(){
|
||||
panel.setActiveMenu(this);
|
||||
});
|
||||
|
||||
document.getElementById('itemManageGallery').addEventListener('click', function(){
|
||||
panel.setActiveMenu(this);
|
||||
});
|
||||
|
||||
document.getElementById('itemTokens').addEventListener('click', function(){
|
||||
panel.setActiveMenu(this);
|
||||
});
|
||||
|
||||
document.getElementById('itemPassword').addEventListener('click', function(){
|
||||
panel.setActiveMenu(this);
|
||||
});
|
||||
|
||||
document.getElementById('itemLogout').innerHTML = `Logout ( ${panel.username} )`;
|
||||
|
||||
panel.getAlbumsSidebar();
|
||||
}
|
||||
|
||||
panel.logout = function(){
|
||||
localStorage.removeItem("token");
|
||||
location.reload('/');
|
||||
}
|
||||
|
||||
panel.getUploads = function(album = undefined, page = undefined){
|
||||
|
||||
if(page === undefined) page = 0;
|
||||
|
||||
let url = '/api/uploads/' + page
|
||||
if(album !== undefined)
|
||||
url = '/api/album/' + album + '/' + page
|
||||
|
||||
axios.get(url).then(function (response) {
|
||||
if(response.data.success === false){
|
||||
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
|
||||
else return swal("An error ocurred", response.data.description, "error");
|
||||
}
|
||||
|
||||
var prevPage = 0;
|
||||
var nextPage = page + 1;
|
||||
|
||||
if(response.data.files.length < 25)
|
||||
nextPage = page;
|
||||
|
||||
if(page > 0) prevPage = page - 1;
|
||||
|
||||
panel.page.innerHTML = '';
|
||||
var container = document.createElement('div');
|
||||
var pagination = `<nav class="pagination is-centered">
|
||||
<a class="pagination-previous" onclick="panel.getUploads(${album}, ${prevPage} )">Previous</a>
|
||||
<a class="pagination-next" onclick="panel.getUploads(${album}, ${nextPage} )">Next page</a>
|
||||
</nav>`;
|
||||
var listType = `
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<a class="button is-small is-outlined is-danger" title="List view" onclick="panel.setFilesView('list', ${album}, ${page})">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-list-ul"></i>
|
||||
</span>
|
||||
</a>
|
||||
<a class="button is-small is-outlined is-danger" title="List view" onclick="panel.setFilesView('thumbs', ${album}, ${page})">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-th-large"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
if(panel.filesView === 'thumbs'){
|
||||
|
||||
container.innerHTML = `
|
||||
${pagination}
|
||||
<hr>
|
||||
${listType}
|
||||
<div class="columns is-multiline is-mobile" id="table">
|
||||
|
||||
</div>
|
||||
${pagination}
|
||||
`;
|
||||
|
||||
panel.page.appendChild(container);
|
||||
var table = document.getElementById('table');
|
||||
|
||||
for(var item of response.data.files){
|
||||
|
||||
var div = document.createElement('div');
|
||||
div.className = "column is-2";
|
||||
if(item.thumb !== undefined)
|
||||
div.innerHTML = `<a href="${item.file}" target="_blank"><img src="${item.thumb}"/></a>`;
|
||||
else
|
||||
div.innerHTML = `<a href="${item.file}" target="_blank"><h1 class="title">.${item.file.split('.').pop()}</h1></a>`;
|
||||
table.appendChild(div);
|
||||
|
||||
}
|
||||
|
||||
}else{
|
||||
|
||||
var albumOrUser = 'Album';
|
||||
if(panel.username === 'root')
|
||||
albumOrUser = 'User';
|
||||
|
||||
container.innerHTML = `
|
||||
${pagination}
|
||||
<hr>
|
||||
${listType}
|
||||
<table class="table is-striped is-narrow is-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File</th>
|
||||
<th>${albumOrUser}</th>
|
||||
<th>Date</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table">
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
${pagination}
|
||||
`;
|
||||
|
||||
panel.page.appendChild(container);
|
||||
var table = document.getElementById('table');
|
||||
|
||||
for(var item of response.data.files){
|
||||
|
||||
var tr = document.createElement('tr');
|
||||
|
||||
var displayAlbumOrUser = item.album;
|
||||
if(panel.username === 'root'){
|
||||
displayAlbumOrUser = '';
|
||||
if(item.username !== undefined)
|
||||
displayAlbumOrUser = item.username;
|
||||
}
|
||||
|
||||
tr.innerHTML = `
|
||||
<tr>
|
||||
<th><a href="${item.file}" target="_blank">${item.file}</a></th>
|
||||
<th>${displayAlbumOrUser}</th>
|
||||
<td>${item.date}</td>
|
||||
<td>
|
||||
<a class="button is-small is-danger is-outlined" title="Delete album" onclick="panel.deleteFile(${item.id})">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-trash-o"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
table.appendChild(tr);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
panel.setFilesView = function(view, album, page){
|
||||
localStorage.filesView = view;
|
||||
panel.filesView = view;
|
||||
panel.getUploads(album, page);
|
||||
}
|
||||
|
||||
panel.deleteFile = function(id){
|
||||
swal({
|
||||
title: "Are you sure?",
|
||||
text: "You wont be able to recover the file!",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#ff3860",
|
||||
confirmButtonText: "Yes, delete it!",
|
||||
closeOnConfirm: false
|
||||
},
|
||||
function(){
|
||||
|
||||
axios.post('/api/upload/delete', {
|
||||
id: id
|
||||
})
|
||||
.then(function (response) {
|
||||
|
||||
if(response.data.success === false){
|
||||
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
|
||||
else return swal("An error ocurred", response.data.description, "error");
|
||||
}
|
||||
|
||||
swal("Deleted!", "The file has been deleted.", "success");
|
||||
panel.getUploads();
|
||||
return;
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
panel.getAlbums = function(){
|
||||
|
||||
axios.get('/api/albums').then(function (response) {
|
||||
if(response.data.success === false){
|
||||
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
|
||||
else return swal("An error ocurred", response.data.description, "error");
|
||||
}
|
||||
|
||||
panel.page.innerHTML = '';
|
||||
var container = document.createElement('div');
|
||||
container.className = "container";
|
||||
container.innerHTML = `
|
||||
<h2 class="subtitle">Create new album</h2>
|
||||
|
||||
<p class="control has-addons has-addons-centered">
|
||||
<input id="albumName" class="input" type="text" placeholder="Name">
|
||||
<a id="submitAlbum" class="button is-primary">Submit</a>
|
||||
</p>
|
||||
|
||||
<h2 class="subtitle">List of albums</h2>
|
||||
|
||||
<table class="table is-striped is-narrow">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Files</th>
|
||||
<th>Created At</th>
|
||||
<th>Public link</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table">
|
||||
</tbody>
|
||||
</table>`;
|
||||
|
||||
panel.page.appendChild(container);
|
||||
var table = document.getElementById('table');
|
||||
|
||||
for(var item of response.data.albums){
|
||||
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<tr>
|
||||
<th>${item.name}</th>
|
||||
<th>${item.files}</th>
|
||||
<td>${item.date}</td>
|
||||
<td><a href="${item.identifier}" target="_blank">Album link</a></td>
|
||||
<td>
|
||||
<a class="button is-small is-primary is-outlined" title="Edit name" onclick="panel.renameAlbum(${item.id})">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</span>
|
||||
</a>
|
||||
<a class="button is-small is-danger is-outlined" title="Delete album" onclick="panel.deleteAlbum(${item.id})">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-trash-o"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
table.appendChild(tr);
|
||||
}
|
||||
|
||||
document.getElementById('submitAlbum').addEventListener('click', function(){
|
||||
panel.submitAlbum();
|
||||
});
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
panel.renameAlbum = function(id){
|
||||
|
||||
swal({
|
||||
title: "Rename album",
|
||||
text: "New name you want to give the album:",
|
||||
type: "input",
|
||||
showCancelButton: true,
|
||||
closeOnConfirm: false,
|
||||
animation: "slide-from-top",
|
||||
inputPlaceholder: "My super album"
|
||||
},function(inputValue){
|
||||
if (inputValue === false) return false;
|
||||
if (inputValue === "") {
|
||||
swal.showInputError("You need to write something!");
|
||||
return false
|
||||
}
|
||||
|
||||
axios.post('/api/albums/rename', {
|
||||
id: id,
|
||||
name: inputValue
|
||||
})
|
||||
.then(function (response) {
|
||||
|
||||
if(response.data.success === false){
|
||||
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
|
||||
else if(response.data.description === 'Name already in use') swal.showInputError("That name is already in use!");
|
||||
else swal("An error ocurred", response.data.description, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
swal("Success!", "Your album was renamed to: " + inputValue, "success");
|
||||
panel.getAlbumsSidebar();
|
||||
panel.getAlbums();
|
||||
return;
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
panel.deleteAlbum = function(id){
|
||||
swal({
|
||||
title: "Are you sure?",
|
||||
text: "This won't delete your files, only the album!",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#ff3860",
|
||||
confirmButtonText: "Yes, delete it!",
|
||||
closeOnConfirm: false
|
||||
},
|
||||
function(){
|
||||
|
||||
axios.post('/api/albums/delete', {
|
||||
id: id
|
||||
})
|
||||
.then(function (response) {
|
||||
|
||||
if(response.data.success === false){
|
||||
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
|
||||
else return swal("An error ocurred", response.data.description, "error");
|
||||
}
|
||||
|
||||
swal("Deleted!", "Your album has been deleted.", "success");
|
||||
panel.getAlbumsSidebar();
|
||||
panel.getAlbums();
|
||||
return;
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
panel.submitAlbum = function(){
|
||||
|
||||
axios.post('/api/albums', {
|
||||
name: document.getElementById('albumName').value
|
||||
})
|
||||
.then(function (response) {
|
||||
|
||||
if(response.data.success === false){
|
||||
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
|
||||
else return swal("An error ocurred", response.data.description, "error");
|
||||
}
|
||||
|
||||
swal("Woohoo!", "Album was added successfully", "success");
|
||||
panel.getAlbumsSidebar();
|
||||
panel.getAlbums();
|
||||
return;
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
panel.getAlbumsSidebar = function(){
|
||||
|
||||
axios.get('/api/albums/sidebar')
|
||||
.then(function (response) {
|
||||
if(response.data.success === false){
|
||||
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
|
||||
else return swal("An error ocurred", response.data.description, "error");
|
||||
}
|
||||
|
||||
var albumsContainer = document.getElementById('albumsContainer');
|
||||
albumsContainer.innerHTML = '';
|
||||
|
||||
if(response.data.albums === undefined) return;
|
||||
|
||||
for(var album of response.data.albums){
|
||||
|
||||
li = document.createElement('li');
|
||||
a = document.createElement('a');
|
||||
a.id = album.id;
|
||||
a.innerHTML = album.name;
|
||||
|
||||
a.addEventListener('click', function(){
|
||||
panel.getAlbum(this);
|
||||
});
|
||||
|
||||
li.appendChild(a);
|
||||
albumsContainer.appendChild(li);
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
panel.getAlbum = function(item){
|
||||
panel.setActiveMenu(item);
|
||||
panel.getUploads(item.id);
|
||||
}
|
||||
|
||||
panel.changeToken = function(){
|
||||
|
||||
axios.get('/api/tokens')
|
||||
.then(function (response) {
|
||||
if(response.data.success === false){
|
||||
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
|
||||
else return swal("An error ocurred", response.data.description, "error");
|
||||
}
|
||||
|
||||
panel.page.innerHTML = '';
|
||||
var container = document.createElement('div');
|
||||
container.className = "container";
|
||||
container.innerHTML = `
|
||||
<h2 class="subtitle">Manage your token</h2>
|
||||
|
||||
<label class="label">Your current token:</label>
|
||||
<p class="control has-addons">
|
||||
<input id="token" readonly class="input is-expanded" type="text" placeholder="Your token" value="${response.data.token}">
|
||||
<a id="getNewToken" class="button is-primary">Request new token</a>
|
||||
</p>
|
||||
`;
|
||||
|
||||
panel.page.appendChild(container);
|
||||
|
||||
document.getElementById('getNewToken').addEventListener('click', function(){
|
||||
panel.getNewToken();
|
||||
});
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
panel.getNewToken = function(){
|
||||
|
||||
axios.post('/api/tokens/change')
|
||||
.then(function (response) {
|
||||
|
||||
if(response.data.success === false){
|
||||
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
|
||||
else return swal("An error ocurred", response.data.description, "error");
|
||||
}
|
||||
|
||||
swal({
|
||||
title: "Woohoo!",
|
||||
text: 'Your token was changed successfully.',
|
||||
type: "success"
|
||||
}, function(){
|
||||
localStorage.token = response.data.token;
|
||||
location.reload();
|
||||
})
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
panel.changePassword = function(){
|
||||
|
||||
panel.page.innerHTML = '';
|
||||
var container = document.createElement('div');
|
||||
container.className = "container";
|
||||
container.innerHTML = `
|
||||
<h2 class="subtitle">Change your password</h2>
|
||||
|
||||
<label class="label">New password:</label>
|
||||
<p class="control has-addons">
|
||||
<input id="password" class="input is-expanded" type="password" placeholder="Your new password">
|
||||
</p>
|
||||
<label class="label">Confirm password:</label>
|
||||
<p class="control has-addons">
|
||||
<input id="passwordConfirm" class="input is-expanded" type="password" placeholder="Verify your new password">
|
||||
<a id="sendChangePassword" class="button is-primary">Set new password</a>
|
||||
</p>
|
||||
`;
|
||||
|
||||
panel.page.appendChild(container);
|
||||
|
||||
document.getElementById('sendChangePassword').addEventListener('click', function(){
|
||||
if (document.getElementById('password').value === document.getElementById('passwordConfirm').value) {
|
||||
panel.sendNewPassword(document.getElementById('password').value);
|
||||
} else {
|
||||
swal({
|
||||
title: "Password mismatch!",
|
||||
text: 'Your passwords do not match, please try again.',
|
||||
type: "error"
|
||||
}, function() {
|
||||
panel.changePassword();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
panel.sendNewPassword = function(pass){
|
||||
|
||||
axios.post('/api/password/change', {password: pass})
|
||||
.then(function (response) {
|
||||
|
||||
if(response.data.success === false){
|
||||
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
|
||||
else return swal("An error ocurred", response.data.description, "error");
|
||||
}
|
||||
|
||||
swal({
|
||||
title: "Woohoo!",
|
||||
text: 'Your password was changed successfully.',
|
||||
type: "success"
|
||||
}, function(){
|
||||
location.reload();
|
||||
})
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
panel.setActiveMenu = function(item){
|
||||
var menu = document.getElementById('menu');
|
||||
var items = menu.getElementsByTagName('a');
|
||||
for(var i = 0; i < items.length; i++)
|
||||
items[i].className = "";
|
||||
|
||||
item.className = 'is-active';
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
panel.preparePage();
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
var upload = {};
|
||||
|
||||
upload.isPrivate = true;
|
||||
upload.token = localStorage.token;
|
||||
upload.maxFileSize;
|
||||
// add the album var to the upload so we can store the album id in there
|
||||
upload.album;
|
||||
|
||||
upload.checkIfPublic = function(){
|
||||
axios.get('/api/check')
|
||||
.then(function (response) {
|
||||
upload.isPrivate= response.data.private;
|
||||
upload.maxFileSize = response.data.maxFileSize;
|
||||
upload.preparePage();
|
||||
})
|
||||
.catch(function (error) {
|
||||
swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
return console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
upload.preparePage = function(){
|
||||
if(!upload.isPrivate) return upload.prepareUpload();
|
||||
if(!upload.token) return document.getElementById('loginToUpload').style.display = 'inline-flex';
|
||||
upload.verifyToken(upload.token, true);
|
||||
}
|
||||
|
||||
upload.verifyToken = function(token, reloadOnError){
|
||||
if(reloadOnError === undefined)
|
||||
reloadOnError = false;
|
||||
|
||||
axios.post('/api/tokens/verify', {
|
||||
token: token
|
||||
})
|
||||
.then(function (response) {
|
||||
|
||||
if(response.data.success === false){
|
||||
swal({
|
||||
title: "An error ocurred",
|
||||
text: response.data.description,
|
||||
type: "error"
|
||||
}, function(){
|
||||
if(reloadOnError){
|
||||
localStorage.removeItem("token");
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.token = token;
|
||||
upload.token = token;
|
||||
return upload.prepareUpload();
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
return console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
upload.prepareUpload = function(){
|
||||
// I think this fits best here because we need to check for a valid token before we can get the albums
|
||||
if (upload.token) {
|
||||
var select = document.getElementById('albumSelect');
|
||||
|
||||
select.addEventListener('change', function() {
|
||||
upload.album = select.value;
|
||||
});
|
||||
|
||||
axios.get('/api/albums', { headers: { token: upload.token }})
|
||||
.then(function(res) {
|
||||
var albums = res.data.albums;
|
||||
|
||||
// if the user doesn't have any albums we don't really need to display
|
||||
// an album selection
|
||||
if (albums.length === 0) return;
|
||||
|
||||
// loop through the albums and create an option for each album
|
||||
for (var i = 0; i < albums.length; i++) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = albums[i].id;
|
||||
opt.innerHTML = albums[i].name;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
// display the album selection
|
||||
document.getElementById('albumDiv').style.display = 'block';
|
||||
})
|
||||
.catch(function(e) {
|
||||
swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
|
||||
return console.log(e);
|
||||
})
|
||||
}
|
||||
|
||||
div = document.createElement('div');
|
||||
div.id = 'dropzone';
|
||||
div.innerHTML = 'Click here or drag and drop files';
|
||||
div.style.display = 'flex';
|
||||
|
||||
document.getElementById('maxFileSize').innerHTML = 'Maximum upload size per file is ' + upload.maxFileSize;
|
||||
document.getElementById('loginToUpload').style.display = 'none';
|
||||
|
||||
if(upload.token === undefined)
|
||||
document.getElementById('loginLinkText').innerHTML = 'Create an account and keep track of your uploads';
|
||||
|
||||
document.getElementById('uploadContainer').appendChild(div);
|
||||
|
||||
upload.prepareDropzone();
|
||||
|
||||
}
|
||||
|
||||
upload.prepareDropzone = function(){
|
||||
var previewNode = document.querySelector('#template');
|
||||
previewNode.id = '';
|
||||
var previewTemplate = previewNode.parentNode.innerHTML;
|
||||
previewNode.parentNode.removeChild(previewNode);
|
||||
|
||||
var dropzone = new Dropzone('div#dropzone', {
|
||||
url: '/api/upload',
|
||||
paramName: 'files[]',
|
||||
maxFilesize: upload.maxFileSize.slice(0, -2),
|
||||
parallelUploads: 2,
|
||||
uploadMultiple: false,
|
||||
previewsContainer: 'div#uploads',
|
||||
previewTemplate: previewTemplate,
|
||||
createImageThumbnails: false,
|
||||
maxFiles: 1000,
|
||||
autoProcessQueue: true,
|
||||
headers: {
|
||||
'token': upload.token
|
||||
},
|
||||
init: function() {
|
||||
this.on('addedfile', function(file) {
|
||||
myDropzone = this;
|
||||
document.getElementById('uploads').style.display = 'block';
|
||||
});
|
||||
// add the selected albumid, if an album is selected, as a header
|
||||
this.on('sending', function(file, xhr) {
|
||||
if (upload.album) {
|
||||
xhr.setRequestHeader('albumid', upload.album)
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update the total progress bar
|
||||
dropzone.on('uploadprogress', function(file, progress) {
|
||||
file.previewElement.querySelector('.progress').setAttribute('value', progress);
|
||||
file.previewElement.querySelector('.progress').innerHTML = progress + '%';
|
||||
});
|
||||
|
||||
dropzone.on('success', function(file, response) {
|
||||
|
||||
// Handle the responseText here. For example, add the text to the preview element:
|
||||
|
||||
if(response.success === false){
|
||||
var span = document.createElement('span');
|
||||
span.innerHTML = response.description;
|
||||
file.previewTemplate.querySelector('.link').appendChild(span);
|
||||
return;
|
||||
}
|
||||
|
||||
a = document.createElement('a');
|
||||
a.href = response.files[0].url;
|
||||
a.target = '_blank';
|
||||
a.innerHTML = response.files[0].url;
|
||||
file.previewTemplate.querySelector('.link').appendChild(a);
|
||||
|
||||
file.previewTemplate.querySelector('.progress').style.display = 'none';
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//Handle image paste event
|
||||
window.addEventListener('paste', function(event) {
|
||||
var items = (event.clipboardData || event.originalEvent.clipboardData).items;
|
||||
for (index in items) {
|
||||
var item = items[index];
|
||||
if (item.kind === 'file') {
|
||||
var blob = item.getAsFile();
|
||||
console.log(blob.type);
|
||||
var file = new File([blob], "pasted-image."+blob.type.match(/(?:[^\/]*\/)([^;]*)/)[1]);
|
||||
file.type = blob.type;
|
||||
console.log(file);
|
||||
myDropzone.addFile(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.onload = function () {
|
||||
upload.checkIfPublic();
|
||||
};
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
const config = require('../config.js')
|
||||
const routes = require('express').Router()
|
||||
const db = require('knex')(config.database)
|
||||
const path = require('path')
|
||||
const utils = require('../controllers/utilsController.js')
|
||||
|
||||
routes.get('/a/:identifier', (req, res, next) => {
|
||||
|
||||
let identifier = req.params.identifier
|
||||
if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided' })
|
||||
|
||||
db.table('albums')
|
||||
.where('identifier', identifier)
|
||||
.then((albums) => {
|
||||
if (albums.length === 0) return res.json({ success: false, description: 'Album not found' })
|
||||
|
||||
let title = albums[0].name
|
||||
db.table('files').select('name').where('albumid', albums[0].id).orderBy('id', 'DESC').then((files) => {
|
||||
|
||||
let thumb = ''
|
||||
let basedomain = req.get('host')
|
||||
for (let domain of config.domains)
|
||||
if (domain.host === req.get('host'))
|
||||
if (domain.hasOwnProperty('resolve'))
|
||||
basedomain = domain.resolve
|
||||
|
||||
for (let file of files) {
|
||||
file.file = basedomain + '/' + file.name
|
||||
|
||||
let ext = path.extname(file.name).toLowerCase()
|
||||
if (utils.extensions.includes(ext)) {
|
||||
file.thumb = basedomain + '/thumbs/' + file.name.slice(0, -ext.length) + '.png'
|
||||
|
||||
/*
|
||||
If thumbnail for album is still not set, do it.
|
||||
A potential improvement would be to let the user upload a specific image as an album cover
|
||||
since embedding the first image could potentially result in nsfw content when pasting links.
|
||||
*/
|
||||
|
||||
if (thumb === '') {
|
||||
thumb = file.thumb
|
||||
}
|
||||
|
||||
file.thumb = `<img src="${file.thumb}"/>`
|
||||
} else {
|
||||
file.thumb = `<h1 class="title">.${ext}</h1>`
|
||||
}
|
||||
}
|
||||
|
||||
return res.render('album', {
|
||||
layout: false,
|
||||
title: title,
|
||||
count: files.length,
|
||||
thumb,
|
||||
files
|
||||
})
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
}).catch(function(error) { console.log(error); res.json({ success: false, description: 'error' }) })
|
||||
})
|
||||
|
||||
module.exports = routes
|
|
@ -1,40 +0,0 @@
|
|||
const config = require('../config.js')
|
||||
const routes = require('express').Router()
|
||||
const uploadController = require('../controllers/uploadController')
|
||||
const albumsController = require('../controllers/albumsController')
|
||||
const tokenController = require('../controllers/tokenController')
|
||||
const authController = require('../controllers/authController')
|
||||
|
||||
routes.get ('/check', (req, res, next) => {
|
||||
return res.json({
|
||||
private: config.private,
|
||||
maxFileSize: config.uploads.maxSize
|
||||
})
|
||||
})
|
||||
|
||||
routes.post ('/login', (req, res, next) => authController.verify(req, res, next))
|
||||
routes.post ('/register', (req, res, next) => authController.register(req, res, next))
|
||||
routes.post ('/password/change', (req, res, next) => authController.changePassword(req, res, next))
|
||||
|
||||
routes.get ('/uploads', (req, res, next) => uploadController.list(req, res))
|
||||
routes.get ('/uploads/:page', (req, res, next) => uploadController.list(req, res))
|
||||
routes.post ('/upload', (req, res, next) => uploadController.upload(req, res, next))
|
||||
routes.post ('/upload/delete', (req, res, next) => uploadController.delete(req, res, next))
|
||||
routes.post ('/upload/:albumid', (req, res, next) => uploadController.upload(req, res, next))
|
||||
|
||||
routes.get ('/album/get/:identifier', (req, res, next) => albumsController.get(req, res, next))
|
||||
routes.get ('/album/:id', (req, res, next) => uploadController.list(req, res, next))
|
||||
routes.get ('/album/:id/:page', (req, res, next) => uploadController.list(req, res, next))
|
||||
|
||||
routes.get ('/albums', (req, res, next) => albumsController.list(req, res, next))
|
||||
routes.get ('/albums/:sidebar', (req, res, next) => albumsController.list(req, res, next))
|
||||
routes.post ('/albums', (req, res, next) => albumsController.create(req, res, next))
|
||||
routes.post ('/albums/delete', (req, res, next) => albumsController.delete(req, res, next))
|
||||
routes.post ('/albums/rename', (req, res, next) => albumsController.rename(req, res, next))
|
||||
routes.get ('/albums/test', (req, res, next) => albumsController.test(req, res, next))
|
||||
|
||||
routes.get ('/tokens', (req, res, next) => tokenController.list(req, res))
|
||||
routes.post ('/tokens/verify', (req, res, next) => tokenController.verify(req, res))
|
||||
routes.post ('/tokens/change', (req, res, next) => tokenController.change(req, res))
|
||||
|
||||
module.exports = routes
|
|
@ -0,0 +1,94 @@
|
|||
exports.up = async knex => {
|
||||
await knex.schema.createTable('users', table => {
|
||||
table.increments();
|
||||
table.string('username');
|
||||
table.text('password');
|
||||
table.boolean('enabled');
|
||||
table.boolean('isAdmin');
|
||||
table.string('apiKey');
|
||||
table.timestamp('passwordEditedAt');
|
||||
table.timestamp('apiKeyEditedAt');
|
||||
table.timestamp('createdAt');
|
||||
table.timestamp('editedAt');
|
||||
});
|
||||
|
||||
await knex.schema.createTable('albums', table => {
|
||||
table.increments();
|
||||
table.integer('userId');
|
||||
table.string('name');
|
||||
table.timestamp('zippedAt');
|
||||
table.timestamp('createdAt');
|
||||
table.timestamp('editedAt');
|
||||
});
|
||||
|
||||
await knex.schema.createTable('files', table => {
|
||||
table.increments();
|
||||
table.integer('userId');
|
||||
table.string('name');
|
||||
table.string('original');
|
||||
table.string('type');
|
||||
table.integer('size');
|
||||
table.string('hash');
|
||||
table.string('ip');
|
||||
table.timestamp('createdAt');
|
||||
table.timestamp('editedAt');
|
||||
});
|
||||
|
||||
await knex.schema.createTable('links', table => {
|
||||
table.increments();
|
||||
table.integer('userId');
|
||||
table.integer('albumId');
|
||||
table.string('identifier');
|
||||
table.integer('views');
|
||||
table.boolean('enabled');
|
||||
table.boolean('enableDownload');
|
||||
table.timestamp('expiresAt');
|
||||
table.timestamp('createdAt');
|
||||
table.timestamp('editedAt');
|
||||
});
|
||||
|
||||
await knex.schema.createTable('albumsFiles', table => {
|
||||
table.increments();
|
||||
table.integer('albumId');
|
||||
table.integer('fileId');
|
||||
});
|
||||
|
||||
await knex.schema.createTable('albumsLinks', table => {
|
||||
table.increments();
|
||||
table.integer('albumId');
|
||||
table.integer('linkId');
|
||||
});
|
||||
|
||||
await knex.schema.createTable('tags', table => {
|
||||
table.increments();
|
||||
table.string('uuid');
|
||||
table.integer('userId');
|
||||
table.string('name');
|
||||
table.timestamp('createdAt');
|
||||
table.timestamp('editedAt');
|
||||
});
|
||||
|
||||
await knex.schema.createTable('fileTags', table => {
|
||||
table.increments();
|
||||
table.integer('fileId');
|
||||
table.integer('tagId');
|
||||
});
|
||||
|
||||
await knex.schema.createTable('bans', table => {
|
||||
table.increments();
|
||||
table.string('ip');
|
||||
table.timestamp('createdAt');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async knex => {
|
||||
await knex.schema.dropTableIfExists('users');
|
||||
await knex.schema.dropTableIfExists('albums');
|
||||
await knex.schema.dropTableIfExists('files');
|
||||
await knex.schema.dropTableIfExists('links');
|
||||
await knex.schema.dropTableIfExists('albumsFiles');
|
||||
await knex.schema.dropTableIfExists('albumsLinks');
|
||||
await knex.schema.dropTableIfExists('tags');
|
||||
await knex.schema.dropTableIfExists('fileTags');
|
||||
await knex.schema.dropTableIfExists('bans');
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
exports.up = async knex => {
|
||||
await knex.schema.alterTable('users', table => {
|
||||
table.unique(['username', 'apiKey']);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('albums', table => {
|
||||
table.boolean('nsfw').defaultTo(false);
|
||||
table.unique(['userId', 'name']);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('links', table => {
|
||||
table.unique(['userId', 'albumId', 'identifier']);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('albumsFiles', table => {
|
||||
table.unique(['albumId', 'fileId']);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('albumsLinks', table => {
|
||||
table.unique(['linkId']);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('tags', table => {
|
||||
table.unique(['userId', 'name']);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('fileTags', table => {
|
||||
table.unique(['fileId', 'tagId']);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async () => {
|
||||
// Nothing
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
exports.up = async knex => {
|
||||
await knex.schema.createTable('statistics', table => {
|
||||
table.increments();
|
||||
table.integer('batchId');
|
||||
table.string('type');
|
||||
table.json('data');
|
||||
table.timestamp('createdAt');
|
||||
|
||||
table.unique(['batchId', 'type']);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async knex => {
|
||||
await knex.schema.dropTableIfExists('statistics');
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
const Util = require('../../utils/Util');
|
||||
|
||||
exports.up = async knex => {
|
||||
await knex.schema.createTable('settings', table => {
|
||||
table.string('key');
|
||||
table.string('value');
|
||||
});
|
||||
|
||||
try {
|
||||
const defaults = Util.getEnvironmentDefaults();
|
||||
const keys = Object.keys(defaults);
|
||||
for (const item of keys) {
|
||||
await knex('settings').insert({
|
||||
key: item,
|
||||
value: JSON.stringify(defaults[item])
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async knex => {
|
||||
await knex.schema.dropTableIfExists('settings');
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
/* eslint-disable no-console */
|
||||
const bcrypt = require('bcrypt');
|
||||
const moment = require('moment');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
exports.seed = async db => {
|
||||
const now = moment.utc().toDate();
|
||||
|
||||
// Save environment variables to the database
|
||||
try {
|
||||
const defaults = Util.getEnvironmentDefaults();
|
||||
const keys = Object.keys(defaults);
|
||||
for (const item of keys) {
|
||||
await Util.writeConfigToDb({
|
||||
key: item,
|
||||
value: defaults[item]
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
// Create admin user if it doesnt exist
|
||||
const user = await db.table('users').where({ username: 'admin' }).first();
|
||||
if (user) {
|
||||
console.log();
|
||||
console.log('=========================================================');
|
||||
console.log('== admin account already exists, skipping. ==');
|
||||
console.log('=========================================================');
|
||||
console.log('== Run `pm2 start pm2.json` to start the service ==');
|
||||
console.log('=========================================================');
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const hash = await bcrypt.hash('admin', 10);
|
||||
await db.table('users').insert({
|
||||
username: 'admin',
|
||||
password: hash,
|
||||
passwordEditedAt: now,
|
||||
createdAt: now,
|
||||
editedAt: now,
|
||||
enabled: true,
|
||||
isAdmin: true
|
||||
});
|
||||
console.log();
|
||||
console.log('=========================================================');
|
||||
console.log('== Successfully created the admin account. ==');
|
||||
console.log('=========================================================');
|
||||
console.log('== Run `pm2 start pm2.json` to start the service ==');
|
||||
console.log('=========================================================');
|
||||
console.log();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class banIP extends Route {
|
||||
constructor() {
|
||||
super('/admin/ban/ip', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { ip } = req.body;
|
||||
if (!ip) return res.status(400).json({ message: 'No ip provided' });
|
||||
|
||||
try {
|
||||
await db.table('bans').insert({ ip });
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully banned the ip'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = banIP;
|
|
@ -0,0 +1,32 @@
|
|||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class filesGET extends Route {
|
||||
constructor() {
|
||||
super('/admin/file/:id', 'get', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
const { id } = req.params;
|
||||
if (!id) return res.status(400).json({ message: 'Invalid file ID supplied' });
|
||||
|
||||
let file = await db.table('files').where({ id }).first();
|
||||
const user = await db.table('users')
|
||||
.select('id', 'username', 'enabled', 'createdAt', 'editedAt', 'apiKeyEditedAt', 'isAdmin')
|
||||
.where({ id: file.userId })
|
||||
.first();
|
||||
file = Util.constructFilePublicLink(req, file);
|
||||
|
||||
// Additional relevant data
|
||||
const filesFromUser = await db.table('files').where({ userId: user.id }).select('id');
|
||||
user.fileCount = filesFromUser.length;
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully retrieved file',
|
||||
file,
|
||||
user
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = filesGET;
|
|
@ -0,0 +1,27 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class unBanIP extends Route {
|
||||
constructor() {
|
||||
super('/admin/unban/ip', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { ip } = req.body;
|
||||
if (!ip) return res.status(400).json({ message: 'No ip provided' });
|
||||
|
||||
try {
|
||||
await db.table('bans')
|
||||
.where({ ip })
|
||||
.delete();
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully unbanned the ip'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = unBanIP;
|
|
@ -0,0 +1,29 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class userDemote extends Route {
|
||||
constructor() {
|
||||
super('/admin/users/demote', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { id } = req.body;
|
||||
if (!id) return res.status(400).json({ message: 'No id provided' });
|
||||
if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' });
|
||||
|
||||
try {
|
||||
await db.table('users')
|
||||
.where({ id })
|
||||
.update({ isAdmin: false })
|
||||
.wasMutated();
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully demoted user'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = userDemote;
|
|
@ -0,0 +1,29 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class userDisable extends Route {
|
||||
constructor() {
|
||||
super('/admin/users/disable', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { id } = req.body;
|
||||
if (!id) return res.status(400).json({ message: 'No id provided' });
|
||||
if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' });
|
||||
|
||||
try {
|
||||
await db.table('users')
|
||||
.where({ id })
|
||||
.update({ enabled: false })
|
||||
.wasMutated();
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully disabled user'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = userDisable;
|
|
@ -0,0 +1,29 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class userEnable extends Route {
|
||||
constructor() {
|
||||
super('/admin/users/enable', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { id } = req.body;
|
||||
if (!id) return res.status(400).json({ message: 'No id provided' });
|
||||
if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' });
|
||||
|
||||
try {
|
||||
await db.table('users')
|
||||
.where({ id })
|
||||
.update({ enabled: true })
|
||||
.wasMutated();
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully enabled user'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = userEnable;
|
|
@ -0,0 +1,55 @@
|
|||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class usersGET extends Route {
|
||||
constructor() {
|
||||
super('/admin/users/:id', 'get', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
const { id } = req.params;
|
||||
if (!id) return res.status(400).json({ message: 'Invalid user ID supplied' });
|
||||
|
||||
try {
|
||||
const user = await db.table('users')
|
||||
.select('id', 'username', 'enabled', 'createdAt', 'editedAt', 'apiKeyEditedAt', 'isAdmin')
|
||||
.where({ id })
|
||||
.first();
|
||||
|
||||
let count = 0;
|
||||
let files = db.table('files')
|
||||
.where({ userId: user.id })
|
||||
.orderBy('id', 'desc');
|
||||
|
||||
const { page, limit = 100 } = req.query;
|
||||
if (page && page >= 0) {
|
||||
files = await files.offset((page - 1) * limit).limit(limit);
|
||||
|
||||
const dbRes = await db.table('files')
|
||||
.count('* as count')
|
||||
.where({ userId: user.id })
|
||||
.first();
|
||||
|
||||
count = dbRes.count;
|
||||
} else {
|
||||
files = await files; // execute the query
|
||||
count = files.length;
|
||||
}
|
||||
|
||||
for (let file of files) {
|
||||
file = Util.constructFilePublicLink(req, file);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully retrieved user',
|
||||
user,
|
||||
files,
|
||||
count
|
||||
});
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = usersGET;
|
|
@ -0,0 +1,28 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class userPromote extends Route {
|
||||
constructor() {
|
||||
super('/admin/users/promote', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { id } = req.body;
|
||||
if (!id) return res.status(400).json({ message: 'No id provided' });
|
||||
if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' });
|
||||
|
||||
try {
|
||||
await db.table('users')
|
||||
.where({ id })
|
||||
.update({ isAdmin: true });
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully promoted user'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = userPromote;
|
|
@ -0,0 +1,26 @@
|
|||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class userDemote extends Route {
|
||||
constructor() {
|
||||
super('/admin/users/purge', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { id } = req.body;
|
||||
if (!id) return res.status(400).json({ message: 'No id provided' });
|
||||
|
||||
try {
|
||||
await Util.deleteAllFilesFromUser(id);
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully deleted the user\'s files'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = userDemote;
|
|
@ -0,0 +1,23 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class usersGET extends Route {
|
||||
constructor() {
|
||||
super('/admin/users', 'get', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
try {
|
||||
const users = await db.table('users')
|
||||
.select('id', 'username', 'enabled', 'isAdmin', 'createdAt');
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully retrieved users',
|
||||
users
|
||||
});
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = usersGET;
|
|
@ -0,0 +1,39 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class albumDELETE extends Route {
|
||||
constructor() {
|
||||
super('/album/:id', 'delete');
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
const { id } = req.params;
|
||||
if (!id) return res.status(400).json({ message: 'Invalid album ID supplied' });
|
||||
|
||||
/*
|
||||
Check if the album exists
|
||||
*/
|
||||
const album = await db.table('albums').where({ id, userId: user.id }).first();
|
||||
if (!album) return res.status(400).json({ message: 'The album doesn\'t exist or doesn\'t belong to the user' });
|
||||
|
||||
try {
|
||||
// Delete the album
|
||||
await db.table('albums').where({ id }).delete();
|
||||
|
||||
// Delete the relation of any files attached to this album
|
||||
await db.table('albumsFiles').where({ albumId: id }).delete();
|
||||
|
||||
// Delete the relation of any links attached to this album
|
||||
await db.table('albumsLinks').where({ albumId: id }).delete();
|
||||
|
||||
// Delete any album links created for this album
|
||||
await db.table('links').where({ albumId: id }).delete()
|
||||
.wasMutated();
|
||||
|
||||
return res.json({ message: 'The album was deleted successfully' });
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = albumDELETE;
|
|
@ -0,0 +1,33 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class albumEditPOST extends Route {
|
||||
constructor() {
|
||||
super('/album/edit', 'post');
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { id, name, nsfw } = req.body;
|
||||
if (!id) return res.status(400).json({ message: 'Invalid album identifier supplied' });
|
||||
|
||||
|
||||
const album = await db.table('albums').where({ id, userId: user.id }).first();
|
||||
if (!album) return res.status(400).json({ message: 'The album doesn\'t exist or doesn\'t belong to the user' });
|
||||
|
||||
try {
|
||||
const updateObj = {
|
||||
name: name || album.name,
|
||||
nsfw: nsfw === true ? true : nsfw === false ? false : album.nsfw
|
||||
};
|
||||
await db
|
||||
.table('albums')
|
||||
.where({ id })
|
||||
.update(updateObj);
|
||||
return res.json({ message: 'Editing the album was successful', data: updateObj });
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = albumEditPOST;
|
|
@ -0,0 +1,58 @@
|
|||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class albumGET extends Route {
|
||||
constructor() {
|
||||
super('/album/:id/full', 'get');
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
const { id } = req.params;
|
||||
if (!id) return res.status(400).json({ message: 'Invalid id supplied' });
|
||||
|
||||
const album = await db
|
||||
.table('albums')
|
||||
.where({ id, userId: user.id })
|
||||
.first();
|
||||
if (!album) return res.status(404).json({ message: 'Album not found' });
|
||||
|
||||
let count = 0;
|
||||
|
||||
let files = db
|
||||
.table('albumsFiles')
|
||||
.where({ albumId: id })
|
||||
.join('files', 'albumsFiles.fileId', 'files.id')
|
||||
.select('files.id', 'files.name', 'files.createdAt')
|
||||
.orderBy('files.id', 'desc');
|
||||
|
||||
const { page, limit = 100 } = req.query;
|
||||
if (page && page >= 0) {
|
||||
files = await files.offset((page - 1) * limit).limit(limit);
|
||||
|
||||
const dbRes = await db
|
||||
.table('albumsFiles')
|
||||
.count('* as count')
|
||||
.where({ albumId: id })
|
||||
.first();
|
||||
|
||||
count = dbRes.count;
|
||||
} else {
|
||||
files = await files; // execute the query
|
||||
count = files.length;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (let file of files) {
|
||||
file = Util.constructFilePublicLink(req, file);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully retrieved album',
|
||||
name: album.name,
|
||||
files,
|
||||
count
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = albumGET;
|
|
@ -0,0 +1,64 @@
|
|||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class albumGET extends Route {
|
||||
constructor() {
|
||||
super('/album/:identifier', 'get', { bypassAuth: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
const { identifier } = req.params;
|
||||
if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' });
|
||||
|
||||
// Make sure it exists and it's enabled
|
||||
const link = await db.table('links').where({ identifier, enabled: true }).first();
|
||||
if (!link) return res.status(404).json({ message: 'The album could not be found' });
|
||||
|
||||
// Same with the album, just to make sure is not a deleted album and a leftover link
|
||||
const album = await db.table('albums').where('id', link.albumId).first();
|
||||
if (!album) return res.status(404).json({ message: 'Album not found' });
|
||||
|
||||
let count = 0;
|
||||
let files = db.table('albumsFiles')
|
||||
.where({ albumId: link.albumId })
|
||||
.join('files', 'albumsFiles.fileId', 'files.id')
|
||||
.select('files.name', 'files.id')
|
||||
.orderBy('files.id', 'desc');
|
||||
|
||||
const { page, limit = 50 } = req.query;
|
||||
if (page && page >= 0) {
|
||||
files = await files.offset((page - 1) * limit).limit(limit);
|
||||
|
||||
const dbRes = await db.table('albumsFiles')
|
||||
.where({ albumId: link.albumId })
|
||||
.join('files', 'albumsFiles.fileId', 'files.id')
|
||||
.select('files.name', 'files.id')
|
||||
.orderBy('files.id', 'desc')
|
||||
.count('* as count')
|
||||
.first();
|
||||
|
||||
count = dbRes.count;
|
||||
} else {
|
||||
files = await files; // execute the query
|
||||
count = files.length;
|
||||
}
|
||||
|
||||
for (let file of files) {
|
||||
file = Util.constructFilePublicLink(req, file);
|
||||
}
|
||||
|
||||
// Add 1 more view to the link
|
||||
await db.table('links').where({ identifier }).update('views', Number(link.views) + 1);
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully retrieved files',
|
||||
name: album.name,
|
||||
downloadEnabled: link.enableDownload,
|
||||
isNsfw: album.nsfw,
|
||||
files,
|
||||
count
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = albumGET;
|
|
@ -0,0 +1,43 @@
|
|||
const moment = require('moment');
|
||||
const Route = require('../../structures/Route');
|
||||
|
||||
class albumPOST extends Route {
|
||||
constructor() {
|
||||
super('/album/new', 'post', { canApiKey: true });
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { name } = req.body;
|
||||
if (!name) return res.status(400).json({ message: 'No name provided' });
|
||||
|
||||
/*
|
||||
Check that an album with that name doesn't exist yet
|
||||
*/
|
||||
const album = await db
|
||||
.table('albums')
|
||||
.where({ name, userId: user.id })
|
||||
.first();
|
||||
if (album) return res.status(401).json({ message: "There's already an album with that name" });
|
||||
|
||||
const now = moment.utc().toDate();
|
||||
const insertObj = {
|
||||
name,
|
||||
userId: user.id,
|
||||
createdAt: now,
|
||||
editedAt: now
|
||||
};
|
||||
|
||||
const dbRes = await db
|
||||
.table('albums')
|
||||
.insert(insertObj)
|
||||
.returning('id')
|
||||
.wasMutated();
|
||||
|
||||
insertObj.id = dbRes.pop();
|
||||
|
||||
return res.json({ message: 'The album was created successfully', data: insertObj });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = albumPOST;
|
|
@ -0,0 +1,30 @@
|
|||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class albumDELETE extends Route {
|
||||
constructor() {
|
||||
super('/album/:id/purge', 'delete');
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
const { id } = req.params;
|
||||
if (!id) return res.status(400).json({ message: 'Invalid album ID supplied' });
|
||||
|
||||
/*
|
||||
Check if the album exists
|
||||
*/
|
||||
const album = await db.table('albums').where({ id, userId: user.id }).first();
|
||||
if (!album) return res.status(400).json({ message: 'The album doesn\'t exist or doesn\'t belong to the user' });
|
||||
|
||||
try {
|
||||
await Util.deleteAllFilesFromAlbum(id);
|
||||
await db.table('albums').where({ id }).delete()
|
||||
.wasMutated();
|
||||
return res.json({ message: 'The album was deleted successfully' });
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = albumDELETE;
|
|
@ -0,0 +1,90 @@
|
|||
const path = require('path');
|
||||
const jetpack = require('fs-jetpack');
|
||||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
const log = require('../../utils/Log');
|
||||
|
||||
class albumGET extends Route {
|
||||
constructor() {
|
||||
super('/album/:identifier/zip', 'get', { bypassAuth: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
const { identifier } = req.params;
|
||||
if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' });
|
||||
|
||||
// TODO: Do we really want to let anyone create a zip of an album?
|
||||
/*
|
||||
Make sure it exists and it's enabled
|
||||
*/
|
||||
const link = await db.table('links')
|
||||
.where({
|
||||
identifier,
|
||||
enabled: true,
|
||||
enableDownload: true
|
||||
})
|
||||
.first();
|
||||
if (!link) return res.status(400).json({ message: 'The supplied identifier could not be found' });
|
||||
|
||||
/*
|
||||
Same with the album, just to make sure is not a deleted album and a leftover link
|
||||
*/
|
||||
const album = await db.table('albums')
|
||||
.where('id', link.albumId)
|
||||
.first();
|
||||
if (!album) return res.status(400).json({ message: 'Album not found' });
|
||||
|
||||
/*
|
||||
If the date when the album was zipped is greater than the album's last edit, we just send the zip to the user
|
||||
*/
|
||||
if (album.zippedAt > album.editedAt) {
|
||||
const filePath = path.join(__dirname, '../../../../uploads', 'zips', `${album.userId}-${album.id}.zip`);
|
||||
const exists = await jetpack.existsAsync(filePath);
|
||||
/*
|
||||
Make sure the file exists just in case, and if not, continue to it's generation.
|
||||
*/
|
||||
if (exists) {
|
||||
const fileName = `${Util.config.serviceName}-${identifier}.zip`;
|
||||
return res.download(filePath, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Grab the files in a very unoptimized way. (This should be a join between both tables)
|
||||
*/
|
||||
const fileList = await db.table('albumsFiles')
|
||||
.where('albumId', link.albumId)
|
||||
.select('fileId');
|
||||
|
||||
/*
|
||||
If there are no files, stop here
|
||||
*/
|
||||
if (!fileList || !fileList.length) return res.status(400).json({ message: 'Can\'t download an empty album' });
|
||||
|
||||
/*
|
||||
Get the actual files
|
||||
*/
|
||||
const fileIds = fileList.map(el => el.fileId);
|
||||
const files = await db.table('files')
|
||||
.whereIn('id', fileIds)
|
||||
.select('name');
|
||||
const filesToZip = files.map(el => el.name);
|
||||
|
||||
try {
|
||||
Util.createZip(filesToZip, album);
|
||||
await db.table('albums')
|
||||
.where('id', link.albumId)
|
||||
.update('zippedAt', db.fn.now())
|
||||
.wasMutated();
|
||||
|
||||
const filePath = path.join(__dirname, '../../../../uploads', 'zips', `${album.userId}-${album.id}.zip`);
|
||||
const fileName = `${Util.config.serviceName}-${identifier}.zip`;
|
||||
return res.download(filePath, fileName);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
return res.status(500).json({ message: 'There was a problem downloading the album' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = albumGET;
|
|
@ -0,0 +1,71 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class albumsGET extends Route {
|
||||
constructor() {
|
||||
super('/albums/mini', 'get', { canApiKey: true });
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
/*
|
||||
Let's fetch the albums. This route will only return a small portion
|
||||
of the album files for displaying on the dashboard. It's probably useless
|
||||
for anyone consuming the API outside of the chibisafe frontend.
|
||||
*/
|
||||
const albums = await db
|
||||
.table('albums')
|
||||
.where('albums.userId', user.id)
|
||||
.select('id', 'name', 'nsfw', 'createdAt', 'editedAt')
|
||||
.orderBy('createdAt', 'desc');
|
||||
|
||||
for (const album of albums) {
|
||||
// Fetch the total amount of files each album has.
|
||||
const fileCount = await db // eslint-disable-line no-await-in-loop
|
||||
.table('albumsFiles')
|
||||
.where('albumId', album.id)
|
||||
.count({ count: 'id' });
|
||||
|
||||
// Fetch the file list from each album but limit it to 5 per album
|
||||
const files = await db // eslint-disable-line no-await-in-loop
|
||||
.table('albumsFiles')
|
||||
.join('files', { 'files.id': 'albumsFiles.fileId' })
|
||||
.where('albumId', album.id)
|
||||
.select('files.id', 'files.name')
|
||||
.orderBy('albumsFiles.id', 'desc')
|
||||
.limit(5);
|
||||
|
||||
// Fetch thumbnails and stuff
|
||||
for (let file of files) {
|
||||
file = Util.constructFilePublicLink(req, file);
|
||||
}
|
||||
|
||||
album.fileCount = fileCount[0].count;
|
||||
album.files = files;
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully retrieved albums',
|
||||
albums
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class albumsDropdownGET extends Route {
|
||||
constructor() {
|
||||
super('/albums/dropdown', 'get', { canApiKey: true });
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
const albums = await db
|
||||
.table('albums')
|
||||
.where('userId', user.id)
|
||||
.select('id', 'name');
|
||||
return res.json({
|
||||
message: 'Successfully retrieved albums',
|
||||
albums
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = [albumsGET, albumsDropdownGET];
|
|
@ -0,0 +1,35 @@
|
|||
const Route = require('../../../structures/Route');
|
||||
|
||||
class linkDELETE extends Route {
|
||||
constructor() {
|
||||
super('/album/link/delete/:identifier', 'delete');
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
const { identifier } = req.params;
|
||||
if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' });
|
||||
|
||||
try {
|
||||
const link = await db.table('links')
|
||||
.where({ identifier, userId: user.id })
|
||||
.first();
|
||||
|
||||
if (!link) return res.status(400).json({ message: 'Identifier doesn\'t exist or doesnt\'t belong to the user' });
|
||||
|
||||
await db.table('links')
|
||||
.where({ id: link.id })
|
||||
.delete();
|
||||
await db.table('albumsLinks')
|
||||
.where({ linkId: link.id })
|
||||
.delete();
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully deleted link'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = linkDELETE;
|