├── .env
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .idea
├── .gitignore
├── Server.iml
├── discord.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jsLinters
│ └── eslint.xml
├── misc.xml
├── modules.xml
└── vcs.xml
├── .prettierrc.js
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
├── app.ts
├── middlewares
│ ├── AdminAuthMiddleware.ts
│ ├── AdminMiddleware.ts
│ ├── AuthMiddleware.ts
│ ├── OAuthMiddleware.ts
│ ├── SessionMiddleware.ts
│ ├── UploadMiddleware.ts
│ └── ValidationMiddleware.ts
├── models
│ ├── CounterModel.ts
│ ├── DomainModel.ts
│ ├── FileModel.ts
│ ├── InvisibleUrlModel.ts
│ ├── InviteModel.ts
│ ├── PasswordResetModel.ts
│ ├── RefreshTokenModel.ts
│ ├── ShortenerModel.ts
│ └── UserModel.ts
├── routes
│ ├── AdminRouter.ts
│ ├── AuthRouter
│ │ ├── DiscordRouter.ts
│ │ ├── PasswordResetsRouter.ts
│ │ └── index.ts
│ ├── BaseRouter.ts
│ ├── DomainsRouter.ts
│ ├── FilesRouter.ts
│ ├── InvitesRouter.ts
│ ├── ShortenerRouter.ts
│ ├── UsersRouter
│ │ ├── MeRouter.ts
│ │ ├── SettingsRouter.ts
│ │ └── index.ts
│ └── index.ts
├── schemas
│ ├── BlacklistSchema.ts
│ ├── BulkInvSchema.ts
│ ├── ChangePasswordSchema.ts
│ ├── ChangeUsernameSchema.ts
│ ├── ConfigSchema.ts
│ ├── CustomDomainSchema.ts
│ ├── DeletionSchema.ts
│ ├── DomainSchema.ts
│ ├── EmbedSchema.ts
│ ├── FakeUrlSchema.ts
│ ├── GenInvSchema.ts
│ ├── InviteAddSchema.ts
│ ├── InviteWaveSchema.ts
│ ├── LoginSchema.ts
│ ├── PasswordResetConfirmationSchema.ts
│ ├── PasswordResetSchema.ts
│ ├── PreferencesSchema.ts
│ ├── PremiumSchema.ts
│ ├── RandomDomainSchema.ts
│ ├── RegisterSchema.ts
│ ├── SetUIDSchema.ts
│ ├── ShortenerSchema.ts
│ ├── TestimonialSchema.ts
│ ├── UpdateDomainSchema.ts
│ ├── VerifyEmailSchema.ts
│ └── WipeIntervalSchema.ts
└── utils
│ ├── CloudflareUtil.ts
│ ├── FormatUtil.ts
│ ├── GenerateUtil.ts
│ ├── IPLoggers.json
│ ├── Intervals.ts
│ ├── LoggingUtil.ts
│ ├── MulterUtil.ts
│ ├── OAuthUtil.ts
│ ├── S3Util.ts
│ ├── SafetyUtils.ts
│ └── interfaces
│ ├── AuthorizationInterface.ts
│ ├── DiscordUserInterface.ts
│ └── EmbedInterface.ts
├── tsconfig.json
└── typings
└── index.d.ts
/.env:
--------------------------------------------------------------------------------
1 | PORT=3001
2 | MONGO_URI=mongodb://127.0.0.1:27017/higure
3 | API_KEY=607cdbae-73b6-49d2-b023-5d3667083766
4 | BACKEND_URL=https://api.higure.wtf
5 | FRONTEND_URL=https://higure.wtf
6 | S3_SECRET_KEY=28M9-Cj840RRiKLPoT-9oJz54AhBvRw4pGGHaCHt3sbGAaVc
7 | S3_ACCESS_KEY_ID=YRMSE1A0_WF3N50LMAZCW3RZ
8 | S3_ENDPOINT=https://cdn.higure.wtf
9 | S3_BUCKET=higure
10 | CLOUDFLARE_API_KEY=no
11 | CLOUDFLARE_ACCOUNT_ID=d8c4952f424ffbaabebefd50673da761
12 | CLOUDFLARE_EMAIL=no
13 | WEBHOOK_URL=https://canary.discord.com/api/webhooks/821333728630407189/yrmKBOeI0iSzvgomyUzXu4wMr2YWZQnD3y1aSYpFgXBfsdZnGZkRUFUDRu6ajeP5cjOc
14 | CUSTOM_DOMAIN_WEBHOOK=https://canary.discord.com/api/webhooks/821333837467353139/XmOdq5jAGQJJnn1ayDvnBcQ9A6wubuYeOm9K-EOgz-7HMgTcdH5rZPo_wWDy2S0u9I6M
15 | ACCESS_TOKEN_SECRET=ef203360-f032-4643-b5b5-35c6e1399bb5
16 | REFRESH_TOKEN_SECRET=ac27e6c2-0912-4d47-b6e6-17d4ea5f0a05
17 | DISCORD_CLIENT_ID=821326543392342096
18 | DISCORD_CLIENT_SECRET=no
19 | DISCORD_LOGIN_URL=https://discord.com/oauth2/authorize?client_id=821326543392342096&redirect_uri=https://api.higure.wtf/auth/discord/login/callback&response_type=code&scope=identify%20guilds.join&prompt=none
20 | DISCORD_LINK_URL=https://discord.com/oauth2/authorize?client_id=821326543392342096&redirect_uri=https://api.higure.wtf/auth/discord/link/callback&response_type=code&scope=identify%20guilds%20guilds.join
21 | DISCORD_LOGIN_REDIRECT_URI=https://api.higure.wtf/auth/discord/login/callback
22 | DISCORD_LINK_REDIRECT_URI=https://api.higure.wtf/auth/discord/link/callback
23 | DISCORD_ROLES=821327879161118722
24 | DISCORD_SERVER_ID=821292414777032744
25 | DISCORD_BOT_TOKEN=no
26 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./node_modules/gts/"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | requests.rest
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # TypeScript cache
46 | *.tsbuildinfo
47 |
48 | # Optional npm cache directory
49 | .npm
50 |
51 | # Optional eslint cache
52 | .eslintcache
53 |
54 | # Microbundle cache
55 | .rpt2_cache/
56 | .rts2_cache_cjs/
57 | .rts2_cache_es/
58 | .rts2_cache_umd/
59 |
60 | # Optional REPL history
61 | .node_repl_history
62 |
63 | # Output of 'npm pack'
64 | *.tgz
65 |
66 | # Yarn Integrity file
67 | .yarn-integrity
68 |
69 | # dotenv environment variables file
70 | .env.test
71 |
72 | # parcel-bundler cache (https://parceljs.org/)
73 | .cache
74 |
75 | # Next.js build output
76 | .next
77 |
78 | # Nuxt.js build / generate output
79 | .nuxt
80 | build
81 |
82 | # Gatsby files
83 | .cache/
84 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
85 | # https://nextjs.org/blog/next-9-1#public-directory-support
86 | # public
87 |
88 | # vuepress build output
89 | .vuepress/dist
90 |
91 | # Serverless directories
92 | .serverless/
93 |
94 | # FuseBox cache
95 | .fusebox/
96 |
97 | # DynamoDB Local files
98 | .dynamodb/
99 |
100 | # TernJS port file
101 | .tern-port
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /../../../../../../../../:\Users\Shrunkie\Desktop\Dev\dny.wtf\Server\.idea/dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/.idea/Server.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/discord.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jsLinters/eslint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ...require('gts/.prettierrc.json')
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Higure.wtf Backend
2 |
3 |
4 | This is the backend used on higure.wtf, It is based on clippy's backend. Recommend using [the imgs.bar v1 backend](https://github.com/imgs-bar/Backend) since that has more features and better security.
5 |
6 |
7 | ## How to fix counter error?
8 | ```
9 | mongo
10 |
11 | use database
12 |
13 | db.counters.insert({"_id": "counter"})
14 | ```
15 |
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "higure-server",
3 | "version": "1.0.0",
4 | "description": "First version of higure backend",
5 | "main": "src/app.ts",
6 | "scripts": {
7 | "build": "tsc --build",
8 | "start": "node build/app",
9 | "dev": "nodemon --exec ts-node --files src/app.ts",
10 | "lint": "gts lint",
11 | "clean": "gts clean",
12 | "compile": "tsc",
13 | "fix": "gts fix"
14 | },
15 | "author": "pringlepot",
16 | "devDependencies": {
17 | "@types/node": "^14.11.2",
18 | "@types/uuid": "^8.3.0",
19 | "@typescript-eslint/eslint-plugin": "^4.8.2",
20 | "@typescript-eslint/parser": "^4.8.2",
21 | "eslint": "^7.14.0",
22 | "eslint-config-google": "^0.14.0",
23 | "gts": "^3.1.0",
24 | "typescript": "^4.0.3"
25 | },
26 | "dependencies": {
27 | "@typegoose/typegoose": "^7.4.2",
28 | "@types/archiver": "^5.1.0",
29 | "@types/aws-sdk": "^2.7.0",
30 | "@types/bad-words": "^3.0.0",
31 | "@types/cookie-parser": "^1.4.2",
32 | "@types/cors": "^2.8.8",
33 | "@types/express": "^4.17.9",
34 | "@types/is-valid-domain": "0.0.0",
35 | "@types/joi": "^14.3.4",
36 | "@types/jsonwebtoken": "^8.5.0",
37 | "@types/mongoose": "^5.10.1",
38 | "@types/ms": "^0.7.31",
39 | "@types/multer": "^1.4.4",
40 | "@types/multer-s3": "^2.7.8",
41 | "@types/nodemailer": "^6.4.0",
42 | "archiver": "^5.1.0",
43 | "argon2": "^0.27.1",
44 | "aws-sdk": "^2.799.0",
45 | "axios": "^0.21.1",
46 | "cookie-parser": "^1.4.5",
47 | "cors": "^2.8.5",
48 | "dotenv": "^8.2.0",
49 | "express": "^4.17.1",
50 | "express-rate-limit": "^5.2.3",
51 | "getipintel": "^1.0.4",
52 | "gts": "^3.1.0",
53 | "helmet": "^4.4.1",
54 | "is-valid-domain": "0.0.17",
55 | "joi": "^17.3.0",
56 | "jsonwebtoken": "^8.5.1",
57 | "mongoose": "^5.10.18",
58 | "ms": "^2.1.3",
59 | "multer": "^1.4.2",
60 | "multer-s3": "^2.9.0",
61 | "nodemailer": "^6.4.16",
62 | "nodemailer-smtp-transport": "^2.7.4",
63 | "path": "^0.12.7",
64 | "proxycheck-node.js": "^2.0.0-a",
65 | "querystring": "^0.2.0",
66 | "safe-browse-url-lookup": "^0.1.1",
67 | "stream": "0.0.2",
68 | "uuid": "^8.3.1"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/app.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import {
3 | AdminRouter,
4 | AuthRouter,
5 | BaseRouter,
6 | DomainsRouter,
7 | FilesRouter,
8 | InvitesRouter,
9 | ShortenerRouter,
10 | UsersRouter,
11 | } from './routes';
12 | import {connect} from 'mongoose';
13 | import {intervals} from './utils/Intervals';
14 | import {updateStorage, wipeFiles} from './utils/S3Util';
15 | import express, {json} from 'express';
16 | import cookieParser from 'cookie-parser';
17 | import cors from 'cors';
18 | import helmet from 'helmet';
19 | import SessionMiddleware from './middlewares/SessionMiddleware';
20 | import UserModel from './models/UserModel';
21 | import ms from 'ms';
22 | import CounterModel from './models/CounterModel';
23 | import FileModel from './models/FileModel';
24 | import InvisibleUrlModel from './models/InvisibleUrlModel';
25 |
26 | const app = express();
27 | const PORT = process.env.PORT || 3000;
28 | let MOTD = 'Message of the day';
29 |
30 | try {
31 | const errors = [];
32 | const requiredEnvs = [
33 | 'MONGO_URI',
34 | 'API_KEY',
35 | 'BACKEND_URL',
36 | 'FRONTEND_URL',
37 | 'S3_SECRET_KEY',
38 | 'S3_ACCESS_KEY_ID',
39 | 'S3_ENDPOINT',
40 | 'S3_BUCKET',
41 | 'CLOUDFLARE_API_KEY',
42 | 'CLOUDFLARE_ACCOUNT_ID',
43 | 'CLOUDFLARE_EMAIL',
44 | 'WEBHOOK_URL',
45 | 'CUSTOM_DOMAIN_WEBHOOK',
46 | 'ACCESS_TOKEN_SECRET',
47 | 'REFRESH_TOKEN_SECRET',
48 | 'DISCORD_CLIENT_ID',
49 | 'DISCORD_CLIENT_SECRET',
50 | 'DISCORD_LOGIN_URL',
51 | 'DISCORD_LINK_URL',
52 | 'DISCORD_LOGIN_REDIRECT_URI',
53 | 'DISCORD_LINK_REDIRECT_URI',
54 | 'DISCORD_ROLES',
55 | 'DISCORD_SERVER_ID',
56 | 'DISCORD_BOT_TOKEN',
57 | ];
58 |
59 | for (const env of requiredEnvs) {
60 | // eslint-disable-next-line no-prototype-builtins
61 | if (!process.env.hasOwnProperty(env)) {
62 | errors.push(env);
63 | }
64 | }
65 |
66 | if (errors.length > 0)
67 | throw new Error(
68 | `${errors.join(', ')} ${errors.length > 1 ? 'are' : 'is'} required`
69 | );
70 |
71 | app.use(
72 | cors({
73 | credentials: true,
74 | origin: [
75 | 'https://www.higure.wtf',
76 | 'https://higure.wtf',
77 | 'http://localhost:3000',
78 | 'http://localhost:3000',
79 | ],
80 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
81 | })
82 | );
83 | app.set('trust proxy', 1);
84 | app.use(helmet.originAgentCluster());
85 | app.use(helmet.dnsPrefetchControl());
86 | app.use(helmet.permittedCrossDomainPolicies());
87 | app.use(helmet.hidePoweredBy());
88 | app.use(json());
89 | app.use(cookieParser());
90 | app.use(SessionMiddleware);
91 | app.use('/', BaseRouter);
92 | app.use('/files', FilesRouter);
93 | app.use('/invites', InvitesRouter);
94 | app.use('/domains', DomainsRouter);
95 | app.use('/auth', AuthRouter);
96 | app.use('/users', UsersRouter);
97 | app.use('/shortener', ShortenerRouter);
98 | app.use('/admin', AdminRouter);
99 | app.use((_req, res) => {
100 | res.redirect(process.env.FRONTEND_URL);
101 | });
102 | app.listen(PORT, () => {
103 | console.log(`Listening to port ${PORT}`);
104 | });
105 |
106 | connect(
107 | process.env.MONGO_URI,
108 | {
109 | useNewUrlParser: true,
110 | useUnifiedTopology: true,
111 | useFindAndModify: false,
112 | },
113 | () => {
114 | console.log('Connected to MongoDB cluster');
115 | }
116 | );
117 |
118 | (async () => {
119 | const findCounter = await CounterModel.findById('counter');
120 | if (!findCounter)
121 | throw new Error(
122 | 'Create a counter document with the value 1 as the count'
123 | );
124 | for (const user of await UserModel.find({
125 | 'settings.autoWipe.enabled': true,
126 | })) {
127 | const {interval} = user.settings.autoWipe;
128 | const validIntervals = [
129 | ms('1h'),
130 | ms('2h'),
131 | ms('12h'),
132 | ms('24h'),
133 | ms('1w'),
134 | ms('2w'),
135 | 2147483647,
136 | ];
137 |
138 | if (validIntervals.includes(interval)) {
139 | const findInterval = intervals.find(i => i.uuid === user._id);
140 | if (findInterval) clearInterval(findInterval.id);
141 |
142 | const id = setInterval(async () => {
143 | try {
144 | await wipeFiles(user);
145 |
146 | await FileModel.deleteMany({
147 | 'uploader.uuid': user._id,
148 | });
149 |
150 | await InvisibleUrlModel.deleteMany({
151 | uploader: user._id,
152 | });
153 |
154 | await UserModel.findByIdAndUpdate(user._id, {
155 | uploads: 0,
156 | });
157 | // eslint-disable-next-line no-empty
158 | } catch (err) {}
159 | }, interval);
160 |
161 | intervals.push({
162 | id,
163 | uuid: user._id,
164 | });
165 | }
166 | }
167 | await updateStorage();
168 | console.log('Started autowipe thread');
169 | })();
170 | } catch (err) {
171 | throw new Error(err);
172 | }
173 | function getMOTD() {
174 | return MOTD;
175 | }
176 | function setMOTD(motd: string) {
177 | MOTD = motd;
178 | }
179 | export {getMOTD, setMOTD};
180 |
--------------------------------------------------------------------------------
/src/middlewares/AdminAuthMiddleware.ts:
--------------------------------------------------------------------------------
1 | import {NextFunction, Request, Response} from 'express';
2 | import UserModel from '../models/UserModel';
3 |
4 | export default async (req: Request, res: Response, next: NextFunction) => {
5 | let {user} = req;
6 | const key = req.headers.authorization as string;
7 |
8 | if (user) user = await UserModel.findById(user._id);
9 |
10 | if ((!key && !user) || (key !== process.env.API_KEY && !user))
11 | return res.status(401).json({
12 | success: false,
13 | error: 'unauthorized',
14 | });
15 |
16 | next();
17 | };
18 |
--------------------------------------------------------------------------------
/src/middlewares/AdminMiddleware.ts:
--------------------------------------------------------------------------------
1 | import {NextFunction, Request, Response} from 'express';
2 | import UserModel from '../models/UserModel';
3 |
4 | export default async (req: Request, res: Response, next: NextFunction) => {
5 | let {user} = req;
6 | const key = req.headers.authorization as string;
7 |
8 | if (user) user = await UserModel.findById(user._id);
9 |
10 | if ((!key && !user) || key !== process.env.API_KEY)
11 | return res.status(401).json({
12 | success: false,
13 | error: 'unauthorized',
14 | });
15 |
16 | next();
17 | };
18 |
--------------------------------------------------------------------------------
/src/middlewares/AuthMiddleware.ts:
--------------------------------------------------------------------------------
1 | import {NextFunction, Request, Response} from 'express';
2 | import UserModel from '../models/UserModel';
3 |
4 | export default async (req: Request, res: Response, next: NextFunction) => {
5 | let {user} = req;
6 |
7 | if (!user)
8 | return res.status(401).json({
9 | success: false,
10 | error: 'unauthorized',
11 | });
12 |
13 | user = await UserModel.findById(user._id).select('-__v -password');
14 |
15 | if (!user.emailVerified)
16 | return res.status(401).json({
17 | success: false,
18 | error: 'please verify your email',
19 | });
20 |
21 | if (user.blacklisted.status)
22 | return res.status(401).json({
23 | success: false,
24 | error: `you are blacklisted for: ${user.blacklisted.reason}`,
25 | });
26 |
27 | if (user.disabled)
28 | return res.status(401).json({
29 | success: false,
30 | error: "you've disabled your account",
31 | });
32 |
33 | req.user = user;
34 | next();
35 | };
36 |
--------------------------------------------------------------------------------
/src/middlewares/OAuthMiddleware.ts:
--------------------------------------------------------------------------------
1 | import {NextFunction, Request, Response} from 'express';
2 | import {OAuth} from '../utils/OAuthUtil';
3 |
4 | export default (request = 'login') => {
5 | return async (req: Request, res: Response, next: NextFunction) => {
6 | const {code} = req.query;
7 | const discord = new OAuth(code as string);
8 |
9 | try {
10 | await discord.validate(request);
11 | await discord.getUser();
12 |
13 | req.discord = discord;
14 | next();
15 | } catch (err) {
16 | res.status(500).json({
17 | success: false,
18 | error: err.message,
19 | });
20 | }
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/src/middlewares/SessionMiddleware.ts:
--------------------------------------------------------------------------------
1 | import {NextFunction, Request, Response} from 'express';
2 | import {verify} from 'jsonwebtoken';
3 |
4 | export default (req: Request, _res: Response, next: NextFunction) => {
5 | // terrible, I know
6 | const accessToken =
7 | req.headers['x-access-token'] &&
8 | (req.headers['x-access-token'] as string).split(' ')[1];
9 |
10 | if (!accessToken) return next();
11 |
12 | try {
13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
14 | const token: any = verify(accessToken, process.env.ACCESS_TOKEN_SECRET);
15 | req.user = token;
16 |
17 | next();
18 | } catch (err) {
19 | next();
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/src/middlewares/UploadMiddleware.ts:
--------------------------------------------------------------------------------
1 | import {NextFunction, Request, Response} from 'express';
2 | import DomainModel from '../models/DomainModel';
3 | import UserModel from '../models/UserModel';
4 |
5 | export default async (req: Request, res: Response, next: NextFunction) => {
6 | const key = req.headers.key as string;
7 |
8 | if (!key)
9 | return res.status(400).json({
10 | success: false,
11 | error: 'provide a key',
12 | });
13 |
14 | const user = await UserModel.findOne({key});
15 |
16 | if (!user)
17 | return res.status(401).json({
18 | success: false,
19 | error: 'invalid key',
20 | });
21 |
22 | if (user.blacklisted.status)
23 | return res.status(401).json({
24 | success: false,
25 | error: `you are blacklisted for: ${user.blacklisted.reason}`,
26 | });
27 |
28 | if (user.disabled)
29 | return res.status(401).json({
30 | success: false,
31 | error: "you've disabled your account",
32 | });
33 |
34 | if (!user.emailVerified)
35 | return res.status(401).json({
36 | success: false,
37 | error: 'please verify your email',
38 | });
39 |
40 | if (!user.discord.id || user.discord.id === '')
41 | return res.status(401).json({
42 | success: false,
43 | error: 'please link your discord',
44 | });
45 |
46 | const domain = await DomainModel.findOne({name: user.settings.domain.name});
47 |
48 | if (!domain)
49 | return res.status(400).json({
50 | success: false,
51 | erorr: 'invalid domain, change it on the dashboard',
52 | });
53 |
54 | if (domain.userOnly && domain.donatedBy && domain.donatedBy !== user._id)
55 | return res.status(401).json({
56 | success: false,
57 | error: 'you are not allowed to upload to this domain',
58 | });
59 |
60 | req.user = user;
61 | next();
62 | };
63 |
--------------------------------------------------------------------------------
/src/middlewares/ValidationMiddleware.ts:
--------------------------------------------------------------------------------
1 | import {NextFunction, Request, Response} from 'express';
2 | import {ArraySchema, ObjectSchema} from 'joi';
3 |
4 | export default (
5 | schema: ObjectSchema | ArraySchema,
6 | property: 'body' | 'query' = 'body'
7 | ) => {
8 | return async (req: Request, res: Response, next: NextFunction) => {
9 | try {
10 | await schema.validateAsync(req[property]);
11 | next();
12 | } catch (err) {
13 | res.status(400).json({
14 | success: false,
15 | error: err.details
16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
17 | .map((x: any) => x.message.replace(/"/gi, ''))
18 | .join(', '),
19 | });
20 | }
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/src/models/CounterModel.ts:
--------------------------------------------------------------------------------
1 | import {getModelForClass, modelOptions, prop} from '@typegoose/typegoose';
2 |
3 | @modelOptions({options: {allowMixed: 0}})
4 | export class Counter {
5 | /**
6 | * The counter identifier.
7 | */
8 | @prop()
9 | _id: string;
10 |
11 | /**
12 | * The current count.
13 | */
14 | @prop()
15 | count: number;
16 | /**
17 | * The current count.
18 | */
19 | @prop()
20 | storageUsed: number;
21 | }
22 |
23 | export default getModelForClass(Counter);
24 |
--------------------------------------------------------------------------------
/src/models/DomainModel.ts:
--------------------------------------------------------------------------------
1 | import {getModelForClass, modelOptions, prop} from '@typegoose/typegoose';
2 |
3 | @modelOptions({options: {allowMixed: 0}})
4 | export class Domain {
5 | /**
6 | * The domain name.
7 | */
8 | @prop()
9 | name: string;
10 |
11 | /**
12 | * If the domain is wildcarded or not.
13 | */
14 | @prop()
15 | wildcard: boolean;
16 |
17 | /**
18 | * If the domain was donated or not.
19 | */
20 | @prop()
21 | donated: boolean;
22 |
23 | /**
24 | * The uuid of the user who donated the domain.
25 | */
26 | @prop()
27 | donatedBy: string;
28 |
29 | /**
30 | * Whether or not the domain is only usable by the donator.
31 | */
32 | @prop()
33 | userOnly: boolean;
34 |
35 | /**
36 | * The date the domain was added.
37 | */
38 | @prop()
39 | dateAdded: Date;
40 | }
41 |
42 | export default getModelForClass(Domain);
43 |
--------------------------------------------------------------------------------
/src/models/FileModel.ts:
--------------------------------------------------------------------------------
1 | import {getModelForClass, modelOptions, prop} from '@typegoose/typegoose';
2 |
3 | @modelOptions({options: {allowMixed: 0}})
4 | export class File {
5 | /**
6 | * The filename.
7 | */
8 | @prop()
9 | filename: string;
10 |
11 | /**
12 | * The s3 key.
13 | */
14 | @prop()
15 | key: string;
16 |
17 | /**
18 | * The timestamp the file was uploaded at.
19 | */
20 | @prop()
21 | timestamp: Date;
22 |
23 | /**
24 | * The file's mimetype.
25 | */
26 | @prop()
27 | mimetype: string;
28 |
29 | /**
30 | * The file size.
31 | */
32 | @prop()
33 | size: string;
34 |
35 | /**
36 | * The domain the user used.
37 | */
38 | @prop()
39 | domain: string;
40 |
41 | /**
42 | * Whether or not the domain is user-only.
43 | */
44 | @prop()
45 | userOnlyDomain: boolean;
46 |
47 | /**
48 | * The file's deletion key.
49 | */
50 | @prop()
51 | deletionKey: string;
52 |
53 | /**
54 | * The file's embed settings.
55 | */
56 | @prop()
57 | embed: {
58 | enabled: boolean;
59 | color: string;
60 | title: string;
61 | description: string;
62 | author: string;
63 | randomColor: boolean;
64 | };
65 |
66 | /**
67 | * Whether or not the file's link should show in discord.
68 | */
69 | @prop()
70 | showLink: boolean;
71 |
72 | /**
73 | * The user who uploaded the file.
74 | */
75 | @prop()
76 | uploader: {
77 | uuid: string;
78 | username: string;
79 | };
80 | }
81 |
82 | export default getModelForClass(File);
83 |
--------------------------------------------------------------------------------
/src/models/InvisibleUrlModel.ts:
--------------------------------------------------------------------------------
1 | import {getModelForClass, modelOptions, prop} from '@typegoose/typegoose';
2 |
3 | @modelOptions({options: {allowMixed: 0}})
4 | export class InvisibleUrl {
5 | /**
6 | * The invisible url id.
7 | */
8 | @prop()
9 | _id: string;
10 |
11 | /**
12 | * The original file name.
13 | */
14 | @prop()
15 | filename: string;
16 |
17 | /**
18 | * The uuid of the user who uploaded the file.
19 | */
20 | @prop()
21 | uploader: string;
22 | }
23 |
24 | export default getModelForClass(InvisibleUrl);
25 |
--------------------------------------------------------------------------------
/src/models/InviteModel.ts:
--------------------------------------------------------------------------------
1 | import {getModelForClass, modelOptions, prop} from '@typegoose/typegoose';
2 |
3 | @modelOptions({options: {allowMixed: 0}})
4 | export class Invite {
5 | /**
6 | * The invite code.
7 | */
8 | @prop()
9 | _id: string;
10 |
11 | /**
12 | * The user who created the invite.
13 | */
14 | @prop()
15 | createdBy: {
16 | username: string;
17 | uuid: string;
18 | };
19 |
20 | /**
21 | * The date the invite was created.
22 | */
23 | @prop()
24 | dateCreated: Date;
25 |
26 | /**
27 | * The date it was redeemed on.
28 | */
29 | @prop()
30 | dateRedeemed: Date;
31 |
32 | /**
33 | * The user who claimed the invite.
34 | */
35 | @prop()
36 | usedBy: string;
37 |
38 | /**
39 | * Whether or not the invite has been redeemed.
40 | */
41 | @prop()
42 | redeemed: boolean;
43 |
44 | /**
45 | * Whether or not the invite is useable.
46 | */
47 | @prop()
48 | useable: boolean;
49 | }
50 |
51 | export default getModelForClass(Invite);
52 |
--------------------------------------------------------------------------------
/src/models/PasswordResetModel.ts:
--------------------------------------------------------------------------------
1 | import {getModelForClass, modelOptions, prop} from '@typegoose/typegoose';
2 |
3 | @modelOptions({options: {allowMixed: 0}})
4 | export class PasswordReset {
5 | /**
6 | * The reset code.
7 | */
8 | @prop()
9 | _id: string;
10 |
11 | /**
12 | * The user's uuid.
13 | */
14 | @prop()
15 | user: string;
16 |
17 | /**
18 | * The user's email.
19 | */
20 | @prop()
21 | email: string;
22 | }
23 |
24 | export default getModelForClass(PasswordReset);
25 |
--------------------------------------------------------------------------------
/src/models/RefreshTokenModel.ts:
--------------------------------------------------------------------------------
1 | import {getModelForClass, modelOptions, prop} from '@typegoose/typegoose';
2 |
3 | @modelOptions({options: {allowMixed: 0}})
4 | export class RefreshToken {
5 | /**
6 | * The refresh token.
7 | */
8 | @prop()
9 | token: string;
10 |
11 | /**
12 | * The uuid of the user who made the token.
13 | */
14 | @prop()
15 | user: string;
16 |
17 | /**
18 | * The date the token is going to expire.
19 | */
20 | @prop()
21 | expires: Date;
22 | }
23 |
24 | export default getModelForClass(RefreshToken);
25 |
--------------------------------------------------------------------------------
/src/models/ShortenerModel.ts:
--------------------------------------------------------------------------------
1 | import {getModelForClass, modelOptions, prop} from '@typegoose/typegoose';
2 |
3 | @modelOptions({options: {allowMixed: 0}})
4 | export class Shortener {
5 | /**
6 | * The shortened id.
7 | */
8 | @prop()
9 | shortId: string;
10 |
11 | /**
12 | * The destination url.
13 | */
14 | @prop()
15 | destination: string;
16 |
17 | /**
18 | * The key used to delete the link.
19 | */
20 | @prop()
21 | deletionKey: string;
22 |
23 | /**
24 | * The date the url was shortened.
25 | */
26 | @prop()
27 | timestamp: Date;
28 |
29 | /**
30 | * The uuid of the user who shortened the url.
31 | */
32 | @prop()
33 | user: string;
34 | }
35 |
36 | export default getModelForClass(Shortener);
37 |
--------------------------------------------------------------------------------
/src/models/UserModel.ts:
--------------------------------------------------------------------------------
1 | import {getModelForClass, modelOptions, prop} from '@typegoose/typegoose';
2 |
3 | @modelOptions({options: {allowMixed: 0}})
4 | export class User {
5 | /**
6 | * The user's uuid.
7 | */
8 | @prop()
9 | _id: string;
10 |
11 | /**
12 | * The user's uid.
13 | */
14 | @prop()
15 | uid: number;
16 |
17 | /**
18 | * The user's username.
19 | */
20 | @prop()
21 | username: string;
22 |
23 | /**
24 | * The user's password.
25 | */
26 | @prop()
27 | password: string;
28 |
29 | /**
30 | * The user's invite code.
31 | */
32 | @prop()
33 | invite: string;
34 |
35 | /**
36 | * The user's upload key.
37 | */
38 | @prop()
39 | key: string;
40 |
41 | /**
42 | * Whether or not the user is premium.
43 | */
44 | @prop()
45 | premium: boolean;
46 |
47 | /**
48 | * The last time the user requested to add a custom domain.
49 | */
50 | @prop()
51 | lastDomainAddition: Date;
52 |
53 | /**
54 | * The last time the key was regened.
55 | */
56 | @prop()
57 | lastKeyRegen: Date;
58 |
59 | /**
60 | * The last time the user's username was changed.
61 | */
62 | @prop()
63 | lastUsernameChange: Date;
64 |
65 | /**
66 | * The last time the user requested a file archive.
67 | */
68 | @prop()
69 | lastFileArchive: Date;
70 |
71 | /**
72 | * The user's email.
73 | */
74 | @prop()
75 | email: string;
76 |
77 | /**
78 | * Whether or not the user's email is verified.
79 | */
80 | @prop()
81 | emailVerified: boolean;
82 |
83 | /**
84 | * The user's email verification key.
85 | */
86 | @prop()
87 | emailVerificationKey: string;
88 |
89 | /**
90 | * The user's discord id and avatar.
91 | */
92 | @prop()
93 | discord: {
94 | id: string;
95 | avatar: string;
96 | };
97 |
98 | /**
99 | * The number of strikes the user has.
100 | */
101 | @prop()
102 | strikes: number;
103 |
104 | /**
105 | * If the users account is disabled
106 | */
107 | @prop()
108 | disabled!: boolean;
109 |
110 | /**
111 | * The user's blacklist status and reason.
112 | */
113 | @prop()
114 | blacklisted: {
115 | status: boolean;
116 | reason: string;
117 | };
118 |
119 | /**
120 | * The amount of files the user has uploaded.
121 | */
122 | @prop()
123 | uploads: number;
124 |
125 | /**
126 | * The amount of invites the user has.
127 | */
128 | @prop()
129 | invites: number;
130 |
131 | /**
132 | * The user that the user was invited by.
133 | */
134 | @prop()
135 | invitedBy: string;
136 |
137 | /**
138 | * The users that the user invited.
139 | */
140 | @prop({type: () => [String]})
141 | invitedUsers: string[];
142 |
143 | /**
144 | * The date that the user registered.
145 | */
146 | @prop()
147 | registrationDate: Date;
148 |
149 | /**
150 | * The last time the user logged in.
151 | */
152 | @prop()
153 | lastLogin: Date;
154 |
155 | /**
156 | * Whether or not the user is a admin.
157 | */
158 | @prop()
159 | admin: boolean;
160 | /**
161 | * Whether or not the user bypasses the alt checks.
162 | */
163 | @prop()
164 | bypassAltCheck: boolean;
165 |
166 | /**
167 | * The user's settings, their preferences, their domain, etc.
168 | */
169 | @prop()
170 | settings: {
171 | domain: {
172 | name: string;
173 | subdomain: string;
174 | };
175 | randomDomain: {
176 | enabled: boolean;
177 | domains: string[];
178 | };
179 | embed: {
180 | enabled: boolean;
181 | color: string;
182 | title: string;
183 | description: string;
184 | author: string;
185 | randomColor: boolean;
186 | };
187 | fakeUrl: {
188 | enabled: boolean;
189 | url: string;
190 | };
191 | autoWipe: {
192 | enabled: boolean;
193 | interval: number;
194 | };
195 | showLink: boolean;
196 | invisibleUrl: boolean;
197 | longUrl: boolean;
198 | };
199 | }
200 |
201 | export default getModelForClass(User);
202 |
--------------------------------------------------------------------------------
/src/routes/AdminRouter.ts:
--------------------------------------------------------------------------------
1 | import {Request, Response, Router} from 'express';
2 | import {generateInvite} from '../utils/GenerateUtil';
3 | import AdminMiddleware from '../middlewares/AdminMiddleware';
4 | import InviteModel from '../models/InviteModel';
5 | import ValidationMiddleware from '../middlewares/ValidationMiddleware';
6 | import BlacklistSchema from '../schemas/BlacklistSchema';
7 | import UserModel from '../models/UserModel';
8 | import InviteAddSchema from '../schemas/InviteAddSchema';
9 | import InviteWaveSchema from '../schemas/InviteWaveSchema';
10 | import FileModel from '../models/FileModel';
11 | import {s3, wipeFiles} from '../utils/S3Util';
12 | import GenInvSchema from '../schemas/GenInvSchema';
13 | import InvisibleUrlModel from '../models/InvisibleUrlModel';
14 | import RefreshTokenModel from '../models/RefreshTokenModel';
15 | import PremiumSchema from '../schemas/PremiumSchema';
16 | import SetUIDSchema from '../schemas/SetUIDSchema';
17 | import {addPremium} from '../utils/OAuthUtil';
18 | import {setMOTD} from '../app';
19 |
20 | const router = Router();
21 |
22 | router.use(AdminMiddleware);
23 |
24 | router.post(
25 | '/invites',
26 | ValidationMiddleware(GenInvSchema),
27 | async (req: Request, res: Response) => {
28 | const {executerId} = req.body;
29 | const invite = generateInvite();
30 | const dateCreated = new Date();
31 | const executer = await UserModel.findOne({
32 | $or: [
33 | {_id: executerId},
34 | {username: executerId},
35 | {email: executerId},
36 | {invite: executerId},
37 | {key: executerId},
38 | {'discord.id': executerId.replace('<@!', '').replace('>', '')},
39 | ],
40 | });
41 | if (!executer)
42 | return res.status(404).json({
43 | success: false,
44 | error: 'invalid user',
45 | });
46 |
47 | try {
48 | await InviteModel.create({
49 | _id: invite,
50 | createdBy: {
51 | username: executer ? executer.username : 'Admin',
52 | uuid: executer ? executer._id : 'N/A',
53 | },
54 | dateCreated,
55 | dateRedeemed: null,
56 | usedBy: null,
57 | redeemed: false,
58 | useable: true,
59 | });
60 |
61 | res.status(200).json({
62 | success: true,
63 | link: `https://higure.wtf/?code=${invite}`,
64 | code: invite,
65 | dateCreated,
66 | });
67 | } catch (err) {
68 | res.status(500).json({
69 | success: false,
70 | error: err.message,
71 | });
72 | }
73 | }
74 | );
75 |
76 | router.post(
77 | '/blacklist',
78 | ValidationMiddleware(BlacklistSchema),
79 | async (req: Request, res: Response) => {
80 | const {id, reason, executerId} = req.body;
81 |
82 | try {
83 | // this next line is lol, just lol.
84 | const user = await UserModel.findOne({
85 | $or: [
86 | {_id: id},
87 | {username: id},
88 | {email: id},
89 | {invite: id},
90 | {key: id},
91 | {'discord.id': id.replace('<@!', '').replace('>', '')},
92 | ],
93 | });
94 |
95 | if (!user)
96 | return res.status(404).json({
97 | success: false,
98 | error: 'invalid user',
99 | });
100 |
101 | if (user.blacklisted.status)
102 | return res.status(400).json({
103 | success: false,
104 | error: 'this user is already blacklisted',
105 | });
106 | const executer = await UserModel.findOne({
107 | $or: [
108 | {_id: executerId},
109 | {username: executerId},
110 | {email: executerId},
111 | {invite: executerId},
112 | {key: executerId},
113 | {'discord.id': executerId.replace('<@!', '').replace('>', '')},
114 | ],
115 | });
116 | if (!executer)
117 | return res.status(404).json({
118 | success: false,
119 | error: 'invalid user',
120 | });
121 |
122 | await UserModel.findByIdAndUpdate(user._id, {
123 | blacklisted: {
124 | status: true,
125 | reason: `${reason ? reason : 'No reason provided'} - ${
126 | executer.username
127 | }`,
128 | },
129 | });
130 |
131 | res.status(200).json({
132 | success: true,
133 | message: 'blacklisted user successfully',
134 | });
135 | } catch (err) {
136 | res.status(500).json({
137 | success: false,
138 | error: err.message,
139 | });
140 | }
141 | }
142 | );
143 |
144 | router.post(
145 | '/unblacklist',
146 | ValidationMiddleware(BlacklistSchema),
147 | async (req: Request, res: Response) => {
148 | const {id, reason, executerId} = req.body;
149 |
150 | try {
151 | // this next line is lol, just lol.
152 | const user = await UserModel.findOne({
153 | $or: [
154 | {_id: id},
155 | {username: id},
156 | {email: id},
157 | {invite: id},
158 | {key: id},
159 | {'discord.id': id.replace('<@!', '').replace('>', '')},
160 | ],
161 | });
162 | if (!user)
163 | return res.status(404).json({
164 | success: false,
165 | error: 'invalid user',
166 | });
167 |
168 | const executer = await UserModel.findOne({
169 | $or: [
170 | {_id: executerId},
171 | {username: executerId},
172 | {email: executerId},
173 | {invite: executerId},
174 | {key: executerId},
175 | {'discord.id': executerId.replace('<@!', '').replace('>', '')},
176 | ],
177 | });
178 | if (!executer)
179 | return res.status(404).json({
180 | success: false,
181 | error: 'invalid user',
182 | });
183 |
184 | if (!user.blacklisted.status)
185 | return res.status(400).json({
186 | success: false,
187 | error: 'this user is not blacklisted',
188 | });
189 |
190 | await UserModel.findByIdAndUpdate(user._id, {
191 | blacklisted: {
192 | status: false,
193 | reason: `Unblacklisted by ${executer.username} for: ${
194 | reason ? reason : 'No reason provided'
195 | }`,
196 | },
197 | });
198 |
199 | res.status(200).json({
200 | success: true,
201 | message: 'unblacklisted user successfully',
202 | });
203 | } catch (err) {
204 | res.status(500).json({
205 | success: false,
206 | error: err.message,
207 | });
208 | }
209 | }
210 | );
211 | router.delete('/files/:id', async (req: Request, res: Response) => {
212 | const {id} = req.params;
213 | const file = await FileModel.findOne({filename: id});
214 | if (!file)
215 | return res.status(404).json({
216 | success: false,
217 | error: 'invalid file',
218 | });
219 | const params = {
220 | Bucket: process.env.S3_BUCKET,
221 | Key: file.key,
222 | };
223 | const user = await UserModel.findOne({_id: file.uploader.uuid});
224 | try {
225 | await s3.deleteObject(params).promise();
226 |
227 | if (user.uploads > 0)
228 | await UserModel.findByIdAndUpdate(user._id, {
229 | $inc: {
230 | uploads: -1,
231 | },
232 | });
233 |
234 | await file.remove();
235 |
236 | res.status(200).json({
237 | success: true,
238 | message: 'deleted file successfully',
239 | });
240 | } catch (err) {
241 | res.status(500).json({
242 | success: false,
243 | error: err.message,
244 | });
245 | }
246 | });
247 | router.post(
248 | '/inviteadd',
249 | ValidationMiddleware(InviteAddSchema),
250 | async (req: Request, res: Response) => {
251 | const {id, amount} = req.body;
252 |
253 | try {
254 | // this next line is lol, just lol.
255 | const user = await UserModel.findOne({
256 | $or: [
257 | {_id: id},
258 | {username: id},
259 | {email: id},
260 | {invite: id},
261 | {key: id},
262 | {'discord.id': id.replace('<@!', '').replace('>', '')},
263 | ],
264 | });
265 | if (!user)
266 | return res.status(404).json({
267 | success: false,
268 | error: 'invalid user',
269 | });
270 | await UserModel.findByIdAndUpdate(user._id, {
271 | invites: user.invites + amount,
272 | });
273 |
274 | res.status(200).json({
275 | success: true,
276 | message: 'added invite successfully',
277 | });
278 | } catch (err) {
279 | res.status(500).json({
280 | success: false,
281 | error: err.message,
282 | });
283 | }
284 | }
285 | );
286 | router.post(
287 | '/premium',
288 | ValidationMiddleware(PremiumSchema),
289 | async (req: Request, res: Response) => {
290 | const {id} = req.body;
291 |
292 | try {
293 | // this next line is lol, just lol.
294 | const user = await UserModel.findOne({
295 | $or: [
296 | {_id: id},
297 | {username: id},
298 | {email: id},
299 | {invite: id},
300 | {key: id},
301 | {'discord.id': id.replace('<@!', '').replace('>', '')},
302 | ],
303 | });
304 | if (!user)
305 | return res.status(404).json({
306 | success: false,
307 | error: 'invalid user',
308 | });
309 | await UserModel.findByIdAndUpdate(user._id, {
310 | premium: true,
311 | });
312 | await addPremium(user).catch(e => console.log(e));
313 |
314 | res.status(200).json({
315 | success: true,
316 | message: 'set user as premium correctly',
317 | });
318 | } catch (err) {
319 | res.status(500).json({
320 | success: false,
321 | error: err.message,
322 | });
323 | }
324 | }
325 | );
326 | router.post(
327 | '/verifyemail',
328 | ValidationMiddleware(PremiumSchema),
329 | async (req: Request, res: Response) => {
330 | const {id} = req.body;
331 |
332 | try {
333 | // this next line is lol, just lol.
334 | const user = await UserModel.findOne({
335 | $or: [
336 | {_id: id},
337 | {username: id},
338 | {email: id},
339 | {invite: id},
340 | {key: id},
341 | {'discord.id': id.replace('<@!', '').replace('>', '')},
342 | ],
343 | });
344 | if (!user)
345 | return res.status(404).json({
346 | success: false,
347 | error: 'invalid user',
348 | });
349 | await UserModel.findByIdAndUpdate(user._id, {
350 | emailVerified: true,
351 | });
352 | res.status(200).json({
353 | success: true,
354 | message: 'verified users mail',
355 | });
356 | } catch (err) {
357 | res.status(500).json({
358 | success: false,
359 | error: err.message,
360 | });
361 | }
362 | }
363 | );
364 | router.post(
365 | '/setuid',
366 | ValidationMiddleware(SetUIDSchema),
367 | async (req: Request, res: Response) => {
368 | const {id, newuid} = req.body;
369 |
370 | try {
371 | // this next line is lol, just lol.
372 | const user = await UserModel.findOne({
373 | $or: [
374 | {_id: id},
375 | {username: id},
376 | {email: id},
377 | {invite: id},
378 | {key: id},
379 | {'discord.id': id.replace('<@!', '').replace('>', '')},
380 | ],
381 | });
382 | if (!user)
383 | return res.status(404).json({
384 | success: false,
385 | error: 'invalid user',
386 | });
387 | const uuid = await UserModel.findOne({
388 | uid: newuid,
389 | });
390 | if (uuid)
391 | return res.status(404).json({
392 | success: false,
393 | error: 'uid already in use',
394 | });
395 |
396 | await UserModel.findByIdAndUpdate(user._id, {
397 | uid: newuid,
398 | });
399 | res.status(200).json({
400 | success: true,
401 | message: 'set users id',
402 | });
403 | } catch (err) {
404 | res.status(500).json({
405 | success: false,
406 | error: err.message,
407 | });
408 | }
409 | }
410 | );
411 | router.post(
412 | '/invitewave',
413 | ValidationMiddleware(InviteWaveSchema),
414 | async (req: Request, res: Response) => {
415 | const {amount} = req.body;
416 | try {
417 | await UserModel.updateMany(
418 | {username: {$ne: null}},
419 | {$inc: {invites: +amount}}
420 | );
421 | return res.status(200).json({
422 | success: true,
423 | message: 'Invite wave sent out successfully.',
424 | });
425 | } catch (e) {
426 | return res.status(500).json({
427 | success: false,
428 | error: e.message,
429 | });
430 | }
431 | }
432 | );
433 | router.post('/setmotd', async (req: Request, res: Response) => {
434 | const {motd} = req.body;
435 | try {
436 | setMOTD(motd);
437 | return res.status(200).json({
438 | success: true,
439 | message: 'Set MOTD successfully.',
440 | });
441 | } catch (e) {
442 | return res.status(500).json({
443 | success: false,
444 | error: e.message,
445 | });
446 | }
447 | });
448 | router.post(
449 | '/wipeuser',
450 | AdminMiddleware,
451 | async (req: Request, res: Response) => {
452 | const {id} = req.body;
453 |
454 | try {
455 | const user = await UserModel.findOne({
456 | $or: [
457 | {_id: id},
458 | {username: id},
459 | {email: id},
460 | {invite: id},
461 | {key: id},
462 | {'discord.id': id.replace('<@!', '').replace('>', '')},
463 | ],
464 | });
465 | const count = await wipeFiles(user);
466 |
467 | await FileModel.deleteMany({
468 | 'uploader.uuid': user._id,
469 | });
470 | await InvisibleUrlModel.deleteMany({
471 | uploader: user._id,
472 | });
473 | await InviteModel.deleteMany({
474 | 'createdBy.uuid': user._id,
475 | });
476 | await RefreshTokenModel.deleteMany({user: user._id});
477 | await UserModel.deleteOne({_id: user._id});
478 | res.status(200).json({
479 | success: true,
480 | message: `wiped ${id} successfully`,
481 | count,
482 | });
483 | } catch (err) {
484 | res.status(500).json({
485 | success: false,
486 | error: err.message,
487 | });
488 | }
489 | }
490 | );
491 | router.post(
492 | '/wipefiles',
493 | AdminMiddleware,
494 | async (req: Request, res: Response) => {
495 | const {id} = req.body;
496 |
497 | try {
498 | const user = await UserModel.findOne({
499 | $or: [
500 | {_id: id},
501 | {username: id},
502 | {email: id},
503 | {invite: id},
504 | {key: id},
505 | {'discord.id': id.replace('<@!', '').replace('>', '')},
506 | ],
507 | });
508 | const count = await wipeFiles(user);
509 |
510 | await FileModel.deleteMany({
511 | 'uploader.uuid': user._id,
512 | });
513 | await InvisibleUrlModel.deleteMany({
514 | uploader: user._id,
515 | });
516 | await InviteModel.deleteMany({
517 | 'createdBy.uuid': user._id,
518 | });
519 | await RefreshTokenModel.deleteMany({user: user._id});
520 | res.status(200).json({
521 | success: true,
522 | message: `wiped ${id} successfully`,
523 | count,
524 | });
525 | } catch (err) {
526 | res.status(500).json({
527 | success: false,
528 | error: err.message,
529 | });
530 | }
531 | }
532 | );
533 |
534 | router.get('/users/:id', async (req: Request, res: Response) => {
535 | const {id} = req.params;
536 |
537 | try {
538 | const user = await UserModel.find({
539 | $or: [
540 | {_id: id},
541 | {username: id},
542 | {invite: id},
543 | {key: id},
544 | {'discord.id': id.replace('<@!', '').replace('>', '')},
545 | {uid: parseInt(id) || null},
546 | ],
547 | });
548 |
549 | if (!user)
550 | return res.status(404).json({
551 | success: false,
552 | error: 'invalid user',
553 | });
554 | res.status(200).json({
555 | success: true,
556 | users: user,
557 | });
558 | } catch (err) {
559 | res.status(500).json({
560 | success: false,
561 | error: err.message,
562 | });
563 | }
564 | });
565 |
566 | router.post(
567 | '/enable_user',
568 | AdminMiddleware,
569 | async (req: Request, res: Response) => {
570 | const {id} = req.params;
571 |
572 | try {
573 | const user = await UserModel.findOne({
574 | $or: [
575 | {_id: id},
576 | {username: id},
577 | {email: id},
578 | {invite: id},
579 | {key: id},
580 | {'discord.id': id.replace('<@!', '').replace('>', '')},
581 | ],
582 | });
583 |
584 | if (!user)
585 | return res.status(404).json({
586 | success: false,
587 | error: 'invalid user',
588 | });
589 |
590 | await UserModel.findOneAndUpdate(
591 | {_id: user._id},
592 | {
593 | disabled: false,
594 | }
595 | );
596 |
597 | return res.status(200).json({
598 | success: true,
599 | message: 're-enabled user successfully',
600 | });
601 | } catch (err) {
602 | return res.status(500).json({
603 | success: false,
604 | error: err.message,
605 | });
606 | }
607 | }
608 | );
609 |
610 | export default router;
611 |
--------------------------------------------------------------------------------
/src/routes/AuthRouter/DiscordRouter.ts:
--------------------------------------------------------------------------------
1 | import {Request, Response, Router} from 'express';
2 | import {sign, verify} from 'jsonwebtoken';
3 | import OAuthMiddleware from '../../middlewares/OAuthMiddleware';
4 | import PasswordResetModel from '../../models/PasswordResetModel';
5 | import RefreshTokenModel from '../../models/RefreshTokenModel';
6 | import UserModel from '../../models/UserModel';
7 |
8 | const router = Router();
9 |
10 | router.get('/login', (req: Request, res: Response) => {
11 | const cookie = req.cookies['x-refresh-token'];
12 |
13 | cookie
14 | ? res.redirect(`${process.env.FRONTEND_URL}/dashboard`)
15 | : res.redirect(process.env.DISCORD_LOGIN_URL);
16 | });
17 |
18 | router.get(
19 | '/login/callback',
20 | OAuthMiddleware(),
21 | async (req: Request, res: Response) => {
22 | const {id, avatar, discriminator} = req.discord.user;
23 |
24 | try {
25 | const user = await UserModel.findOne({'discord.id': id});
26 |
27 | if (
28 | !user ||
29 | user.blacklisted.status ||
30 | !user.emailVerified ||
31 | user.disabled
32 | ) {
33 | return res.status(401).redirect(process.env.FRONTEND_URL);
34 | }
35 | const passwordReset = await PasswordResetModel.findOne({user: user._id});
36 | if (passwordReset) await passwordReset.remove();
37 | let avatarurl;
38 | if (avatar && avatar.startsWith('a_')) {
39 | avatarurl = `https://cdn.discordapp.com/${
40 | avatar
41 | ? `avatars/${id}/${avatar}`
42 | : `embed/avatars/${discriminator % 5}`
43 | }.gif`;
44 | } else {
45 | avatarurl = `https://cdn.discordapp.com/${
46 | avatar
47 | ? `avatars/${id}/${avatar}`
48 | : `embed/avatars/${discriminator % 5}`
49 | }.png`;
50 | }
51 | const update = {
52 | lastLogin: new Date(),
53 | 'discord.avatar': avatarurl,
54 | };
55 |
56 | await UserModel.findByIdAndUpdate(user._id, update);
57 | const refreshToken = sign(
58 | {_id: user._id},
59 | process.env.REFRESH_TOKEN_SECRET
60 | );
61 |
62 | await RefreshTokenModel.create({
63 | token: refreshToken,
64 | user: user._id,
65 | expires: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
66 | });
67 |
68 | res.cookie('x-refresh-token', refreshToken, {
69 | httpOnly: true,
70 | secure: false,
71 | });
72 | res.redirect(`${process.env.FRONTEND_URL}/dashboard`);
73 | } catch (err) {
74 | res.status(500).json({
75 | success: false,
76 | error: err.message,
77 | });
78 | }
79 | }
80 | );
81 |
82 | router.get('/link', (req: Request, res: Response) => {
83 | const cookie = req.cookies['x-refresh-token'];
84 |
85 | cookie
86 | ? res.redirect(process.env.DISCORD_LINK_URL)
87 | : res.status(401).json({
88 | success: false,
89 | error: 'unauthorized',
90 | });
91 | });
92 |
93 | router.get(
94 | '/link/callback',
95 | OAuthMiddleware('link'),
96 | async (req: Request, res: Response) => {
97 | const cookie = req.cookies['x-refresh-token'];
98 |
99 | if (!cookie)
100 | return res.status(401).json({
101 | success: false,
102 | error: 'unauthorized',
103 | });
104 |
105 | try {
106 | const refreshToken = await RefreshTokenModel.findOne({token: cookie});
107 |
108 | if (
109 | !refreshToken ||
110 | Date.now() >= new Date(refreshToken.expires).getTime()
111 | ) {
112 | if (refreshToken) await refreshToken.remove();
113 |
114 | res.status(401).json({
115 | success: false,
116 | error: 'invalid refresh token',
117 | });
118 |
119 | return;
120 | }
121 |
122 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
123 | const token: any = verify(
124 | refreshToken.token,
125 | process.env.REFRESH_TOKEN_SECRET
126 | );
127 |
128 | const user = await UserModel.findOne({_id: token._id}).select(
129 | '-__v -password'
130 | );
131 |
132 | if (!user)
133 | return res.status(401).json({
134 | success: false,
135 | error: 'invalid session',
136 | });
137 |
138 | if (!user.emailVerified)
139 | return res.status(401).json({
140 | success: false,
141 | error: 'your email is not verified',
142 | });
143 |
144 | const {id, avatar, discriminator} = req.discord.user;
145 |
146 | await req.discord.addGuildMember(user);
147 | let avatarurl;
148 | if (avatar && avatar.startsWith('a_')) {
149 | avatarurl = `https://cdn.discordapp.com/${
150 | avatar
151 | ? `avatars/${id}/${avatar}`
152 | : `embed/avatars/${discriminator % 5}`
153 | }.gif`;
154 | } else {
155 | avatarurl = `https://cdn.discordapp.com/${
156 | avatar
157 | ? `avatars/${id}/${avatar}`
158 | : `embed/avatars/${discriminator % 5}`
159 | }.png`;
160 | }
161 | await UserModel.findByIdAndUpdate(user._id, {
162 | discord: {
163 | id,
164 | avatar: avatarurl,
165 | },
166 | });
167 |
168 | res.status(200).redirect(`${process.env.FRONTEND_URL}/dashboard`);
169 | } catch (err) {
170 | res.status(500).json({
171 | success: false,
172 | error: 'error: ' + err.stack + err,
173 | });
174 | }
175 | }
176 | );
177 |
178 | export default router;
179 |
--------------------------------------------------------------------------------
/src/routes/AuthRouter/PasswordResetsRouter.ts:
--------------------------------------------------------------------------------
1 | import {Request, Response, Router} from 'express';
2 | import PasswordResetModel, {
3 | PasswordReset,
4 | } from '../../models/PasswordResetModel';
5 | import ValidationMiddleware from '../../middlewares/ValidationMiddleware';
6 | import PasswordResetConfirmationSchema from '../../schemas/PasswordResetConfirmationSchema';
7 | import UserModel, {User} from '../../models/UserModel';
8 | import {generateString} from '../../utils/GenerateUtil';
9 | import PasswordResetSchema from '../../schemas/PasswordResetSchema';
10 | import {hash, verify} from 'argon2';
11 | import RefreshTokenModel from '../../models/RefreshTokenModel';
12 |
13 | const router = Router();
14 |
15 | router.post(
16 | '/send',
17 | ValidationMiddleware(PasswordResetConfirmationSchema),
18 | async (req: Request, res: Response) => {
19 | if (req.user)
20 | return res.status(400).json({
21 | success: false,
22 | error: 'you are already logged in',
23 | });
24 |
25 | const {email} = req.body;
26 | let user: PasswordReset | User = await PasswordResetModel.findOne({email});
27 |
28 | if (user)
29 | return res.status(400).json({
30 | success: false,
31 | error: 'you already have a ongoing password reset',
32 | });
33 |
34 | user = await UserModel.findOne({email});
35 |
36 | const resetKey = generateString(40);
37 |
38 | try {
39 | if (user) {
40 | //await sendPasswordReset(user, resetKey);
41 |
42 | const doc = await PasswordResetModel.create({
43 | _id: resetKey,
44 | user: user._id,
45 | email,
46 | });
47 |
48 | setTimeout(async () => {
49 | await doc.remove();
50 | }, 600000);
51 | }
52 |
53 | res.status(200).json({
54 | success: true,
55 | message:
56 | "if a user exist with that email we'll send over the password reset instructions",
57 | });
58 | } catch (err) {
59 | res.status(500).json({
60 | success: false,
61 | error: err.message,
62 | });
63 | }
64 | }
65 | );
66 |
67 | router.post(
68 | '/reset',
69 | ValidationMiddleware(PasswordResetSchema),
70 | async (req: Request, res: Response) => {
71 | let {user} = req;
72 |
73 | if (user)
74 | return res.status(400).json({
75 | success: false,
76 | error: 'you are already logged in',
77 | });
78 |
79 | const {key, password, confirmPassword} = req.body;
80 | const reset = await PasswordResetModel.findById(key);
81 |
82 | if (!reset)
83 | return res.status(404).json({
84 | success: false,
85 | error: 'invalid key',
86 | });
87 |
88 | user = await UserModel.findById(reset.user);
89 |
90 | if (!user) {
91 | res.status(400).json({
92 | success: false,
93 | error: 'the user attached to this reset does not exist',
94 | });
95 |
96 | await reset.remove();
97 |
98 | return;
99 | }
100 |
101 | if (password.trim() !== confirmPassword.trim())
102 | return res.status(400).json({
103 | success: false,
104 | error: 'confirmation must match password',
105 | });
106 |
107 | if (await verify(user.password, password))
108 | return res.status(400).json({
109 | success: false,
110 | error: 'you must choose a different password',
111 | });
112 |
113 | try {
114 | await RefreshTokenModel.deleteMany({user: user._id});
115 |
116 | await UserModel.findByIdAndUpdate(user._id, {
117 | password: await hash(password),
118 | });
119 |
120 | await reset.remove();
121 |
122 | res.status(200).json({
123 | success: true,
124 | message: 'reset password successfully',
125 | });
126 | } catch (err) {
127 | res.status(500).json({
128 | success: false,
129 | error: err.message,
130 | });
131 | }
132 | }
133 | );
134 |
135 | router.get('/:key', async (req: Request, res: Response) => {
136 | const {key} = req.params;
137 | const doc = await PasswordResetModel.findById(key);
138 |
139 | if (!doc)
140 | return res.status(404).json({
141 | success: false,
142 | error: 'invalid key',
143 | });
144 |
145 | res.status(200).json({
146 | success: true,
147 | message: 'valid key',
148 | });
149 | });
150 |
151 | export default router;
152 |
--------------------------------------------------------------------------------
/src/routes/AuthRouter/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable prefer-const */
2 | import {hash, verify} from 'argon2';
3 | import {Request, Response, Router} from 'express';
4 | import {sign, verify as verifyjwt} from 'jsonwebtoken';
5 | import {v4 as uuid} from 'uuid';
6 | import {generateString} from '../../utils/GenerateUtil';
7 | import ValidationMiddleware from '../../middlewares/ValidationMiddleware';
8 | import InviteModel from '../../models/InviteModel';
9 | import UserModel from '../../models/UserModel';
10 | import LoginSchema from '../../schemas/LoginSchema';
11 | import RegisterSchema from '../../schemas/RegisterSchema';
12 | import VerifyEmailSchema from '../../schemas/VerifyEmailSchema';
13 | import DiscordRouter from './DiscordRouter';
14 | import PasswordResetsRouter from './PasswordResetsRouter';
15 | import PasswordResetModel from '../../models/PasswordResetModel';
16 | import CounterModel from '../../models/CounterModel';
17 | import RefreshTokenModel from '../../models/RefreshTokenModel';
18 | import {logPossibleAlts} from '../../utils/LoggingUtil';
19 |
20 | const router = Router();
21 |
22 | async function getNextUid() {
23 | const {count} = await CounterModel.findByIdAndUpdate('counter', {
24 | $inc: {
25 | count: 1,
26 | },
27 | });
28 |
29 | return count;
30 | }
31 |
32 | router.use('/discord', DiscordRouter);
33 | router.use('/password_resets', PasswordResetsRouter);
34 |
35 | router.post('/token', async (req: Request, res: Response) => {
36 | const cookie = req.cookies['x-refresh-token'];
37 |
38 | if (!cookie)
39 | return res.status(401).json({
40 | success: false,
41 | error: 'provide a refresh token',
42 | });
43 |
44 | try {
45 | const refreshToken = await RefreshTokenModel.findOne({token: cookie});
46 |
47 | if (
48 | !refreshToken ||
49 | Date.now() >= new Date(refreshToken.expires).getTime()
50 | ) {
51 | if (refreshToken) await refreshToken.remove();
52 |
53 | res.status(401).json({
54 | success: false,
55 | error: 'invalid refresh token',
56 | });
57 |
58 | return;
59 | }
60 |
61 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
62 | const token: any = verifyjwt(
63 | refreshToken.token,
64 | process.env.REFRESH_TOKEN_SECRET
65 | );
66 |
67 | const user = await UserModel.findOne({_id: token._id}).select(
68 | '-__v -password -ips'
69 | );
70 |
71 | if (!user)
72 | return res.status(401).json({
73 | success: false,
74 | error: 'invalid refresh token',
75 | });
76 |
77 | await UserModel.findByIdAndUpdate(user._id, {
78 | lastLogin: new Date(),
79 | });
80 |
81 | if (!user.settings.fakeUrl) {
82 | await UserModel.findByIdAndUpdate(user._id, {
83 | settings: {
84 | ...user.settings,
85 | fakeUrl: {
86 | enabled: false,
87 | url: 'https://google.com',
88 | },
89 | },
90 | });
91 | }
92 |
93 | const accessToken = sign({_id: user._id}, process.env.ACCESS_TOKEN_SECRET, {
94 | expiresIn: '15m',
95 | });
96 |
97 | res.status(200).json({
98 | success: true,
99 | accessToken,
100 | user,
101 | });
102 | } catch (err) {
103 | res.status(401).json({
104 | success: false,
105 | error: 'invalid refresh token',
106 | });
107 | }
108 | });
109 |
110 | router.post(
111 | '/register',
112 | ValidationMiddleware(RegisterSchema),
113 | async (req: Request, res: Response) => {
114 | let {
115 | email,
116 | username,
117 | password,
118 | invite,
119 | }: {
120 | email: string;
121 | username: string;
122 | password: string;
123 | invite: string;
124 | } = req.body;
125 |
126 | if (req.user)
127 | return res.status(400).json({
128 | success: false,
129 | error: 'you are already logged in',
130 | });
131 |
132 | const usernameTaken = await UserModel.findOne({
133 | username: {$regex: new RegExp(username, 'i')},
134 | });
135 |
136 | if (usernameTaken)
137 | return res.status(400).json({
138 | success: false,
139 | error: 'the provided username is already taken',
140 | });
141 |
142 | const emailTaken = await UserModel.findOne({
143 | email: {$regex: new RegExp(email, 'i')},
144 | });
145 |
146 | if (emailTaken)
147 | return res.status(400).json({
148 | success: false,
149 | error: 'an account has already been registered with this email',
150 | });
151 |
152 | const inviteDoc = await InviteModel.findById(invite);
153 |
154 | if (!inviteDoc || !inviteDoc.useable)
155 | return res.status(400).json({
156 | success: false,
157 | error: 'invalid invite code',
158 | });
159 |
160 | if (inviteDoc.redeemed)
161 | return res.status(400).json({
162 | success: false,
163 | error: 'this invite has already been redeemed',
164 | });
165 |
166 | let invitedBy = 'Admin';
167 | const inviter = await UserModel.findOne({_id: inviteDoc.createdBy.uuid});
168 |
169 | if (inviter) {
170 | invitedBy = inviter.username;
171 |
172 | if (inviter.blacklisted.status) {
173 | return res.status(400).json({
174 | success: false,
175 | error: 'the user you got invited by is banned',
176 | });
177 | }
178 | await UserModel.findByIdAndUpdate(inviter._id, {
179 | $push: {
180 | invitedUsers: username,
181 | },
182 | });
183 | await InviteModel.findByIdAndUpdate(invite, {
184 | usedBy: username,
185 | redeemed: true,
186 | useable: false,
187 | });
188 | }
189 |
190 | password = await hash(password);
191 |
192 | try {
193 | const user = await UserModel.create({
194 | _id: uuid(),
195 | uid: await getNextUid(),
196 | username,
197 | password,
198 | invite,
199 | key: `${username}_${generateString(30)}`,
200 | premium: false,
201 | lastDomainAddition: null,
202 | lastKeyRegen: null,
203 | lastUsernameChange: null,
204 | lastFileArchive: null,
205 | email,
206 | emailVerified: true,
207 | emailVerificationKey: generateString(30),
208 | discord: {
209 | id: null,
210 | avatar: null,
211 | },
212 | strikes: 0,
213 | disabled: false,
214 | blacklisted: {
215 | status: false,
216 | reason: null,
217 | },
218 | uploads: 0,
219 | invites: 0,
220 | invitedBy,
221 | invitedUsers: [],
222 | registrationDate: new Date(),
223 | lastLogin: null,
224 | admin: false,
225 | bypassAltCheck: false,
226 | settings: {
227 | domain: {
228 | name: 'i.higure.wtf',
229 | subdomain: null,
230 | },
231 | randomDomain: {
232 | enabled: false,
233 | domains: [],
234 | },
235 | embed: {
236 | enabled: true,
237 | color: '#13ed7c',
238 | title: 'default',
239 | description: 'default',
240 | author: 'default',
241 | randomColor: true,
242 | },
243 | fakeUrl: {
244 | enabled: false,
245 | url: 'google.com',
246 | },
247 | autoWipe: {
248 | enabled: false,
249 | interval: 3600000,
250 | },
251 | showLink: false,
252 | invisibleUrl: false,
253 | longUrl: false,
254 | },
255 | });
256 |
257 | await user.save();
258 |
259 | res.status(200).json({
260 | success: true,
261 | message: 'registered successfully, please login',
262 | });
263 | } catch (err) {
264 | res.status(500).json({
265 | success: false,
266 | error: err.message,
267 | });
268 | }
269 | }
270 | );
271 |
272 | router.post(
273 | '/login',
274 | ValidationMiddleware(LoginSchema),
275 | async (req: Request, res: Response) => {
276 | const {
277 | username,
278 | password,
279 | }: {
280 | username: string;
281 | password: string;
282 | } = req.body;
283 |
284 | const user = await UserModel.findOne({username});
285 |
286 | if (
287 | !user ||
288 | !(user.password.startsWith('$')
289 | ? await verify(user.password, password)
290 | : false)
291 | )
292 | return res.status(401).json({
293 | success: false,
294 | error: 'invalid username or password',
295 | });
296 |
297 | if (!user.emailVerified)
298 | return res.status(401).json({
299 | success: false,
300 | error: 'your email is not verified',
301 | });
302 |
303 | if (user.blacklisted.status)
304 | return res.status(401).json({
305 | success: false,
306 | error: `you are blacklisted for: ${user.blacklisted.reason}`,
307 | });
308 |
309 | if (user.disabled)
310 | return res.status(401).json({
311 | success: false,
312 | error: "you've disabled your account",
313 | });
314 |
315 | try {
316 | const passwordReset = await PasswordResetModel.findOne({user: user._id});
317 | if (passwordReset) await passwordReset.remove();
318 | if (!user.settings.fakeUrl) {
319 | await UserModel.findByIdAndUpdate(user._id, {
320 | settings: {
321 | ...user.settings,
322 | fakeUrl: {
323 | enabled: false,
324 | url: 'https://google.com',
325 | },
326 | },
327 | });
328 | }
329 |
330 | const accessToken = sign(
331 | {_id: user._id},
332 | process.env.ACCESS_TOKEN_SECRET,
333 | {expiresIn: '15m'}
334 | );
335 | const refreshToken = sign(
336 | {_id: user._id},
337 | process.env.REFRESH_TOKEN_SECRET
338 | );
339 |
340 | await RefreshTokenModel.create({
341 | token: refreshToken,
342 | user: user._id,
343 | expires: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
344 | });
345 |
346 | res.cookie('x-refresh-token', refreshToken, {
347 | httpOnly: true,
348 | secure: false,
349 | });
350 |
351 | res.status(200).json({
352 | success: true,
353 | accessToken,
354 | user: await UserModel.findById(user._id).select('-__v -password'),
355 | });
356 | } catch (err) {
357 | res.status(500).json({
358 | success: false,
359 | error: err.message,
360 | });
361 | }
362 | }
363 | );
364 |
365 | router.get('/logout', async (req: Request, res: Response) => {
366 | const cookie = req.cookies['x-refresh-token'];
367 |
368 | if (!cookie)
369 | return res.status(401).json({
370 | success: false,
371 | error: 'unauthorized',
372 | });
373 |
374 | try {
375 | const refreshToken = await RefreshTokenModel.findOne({token: cookie});
376 |
377 | if (!refreshToken)
378 | return res.status(401).json({
379 | success: false,
380 | error: 'unauthorized',
381 | });
382 |
383 | await refreshToken.remove();
384 | res.clearCookie('x-refresh-token');
385 |
386 | res.status(200).json({
387 | success: true,
388 | message: 'logged out successfully',
389 | });
390 | } catch (err) {
391 | res.status(500).json({
392 | success: false,
393 | error: err.message,
394 | });
395 | }
396 | });
397 |
398 | router.get(
399 | '/verify',
400 | ValidationMiddleware(VerifyEmailSchema, 'query'),
401 | async (req: Request, res: Response) => {
402 | const key = req.query.key as string;
403 | const user = await UserModel.findOne({emailVerificationKey: key});
404 |
405 | if (!user)
406 | return res.status(404).json({
407 | success: false,
408 | error: 'invalid verification key',
409 | });
410 |
411 | if (user.emailVerified)
412 | return res.status(400).json({
413 | success: false,
414 | error: 'your email is already verified',
415 | });
416 |
417 | try {
418 | await UserModel.findByIdAndUpdate(user._id, {
419 | emailVerified: true,
420 | });
421 |
422 | res.status(200).json({
423 | success: true,
424 | message: 'verified email successfully',
425 | });
426 | } catch (err) {
427 | res.status(500).json({
428 | success: false,
429 | error: err.message,
430 | });
431 | }
432 | }
433 | );
434 |
435 | export default router;
436 |
--------------------------------------------------------------------------------
/src/routes/BaseRouter.ts:
--------------------------------------------------------------------------------
1 | import {Request, Response, Router} from 'express';
2 |
3 | const router = Router();
4 |
5 | router.get('/', (_req: Request, res: Response) => {
6 | res.json({
7 | status: 200,
8 | message: 'Hello there ( ͡° ͜ʖ ͡°)',
9 | });
10 | });
11 |
12 | export default router;
13 |
--------------------------------------------------------------------------------
/src/routes/DomainsRouter.ts:
--------------------------------------------------------------------------------
1 | import {Request, Response, Router} from 'express';
2 | import AdminMiddleware from '../middlewares/AdminMiddleware';
3 | import AuthMiddleware from '../middlewares/AuthMiddleware';
4 | import ValidationMiddleware from '../middlewares/ValidationMiddleware';
5 | import DomainModel from '../models/DomainModel';
6 | import UserModel, {User} from '../models/UserModel';
7 | import CustomDomainSchema from '../schemas/CustomDomainSchema';
8 | import DomainSchema from '../schemas/DomainSchema';
9 | import CloudflareUtil from '../utils/CloudflareUtil';
10 | import {logCustomDomain, logDomains} from '../utils/LoggingUtil';
11 | import AdminAuthMiddleware from '../middlewares/AdminAuthMiddleware';
12 | import isValidDomain from 'is-valid-domain';
13 |
14 | const router = Router();
15 |
16 | router.get('/', AdminAuthMiddleware, async (req: Request, res: Response) => {
17 | const {user} = req;
18 | try {
19 | const count = await DomainModel.countDocuments();
20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
21 | let domains: any = await DomainModel.find({userOnly: false})
22 | .select('-__v -_id -donatedBy')
23 | .sort({name: 1})
24 | .lean();
25 |
26 | if (user)
27 | domains = (
28 | await DomainModel.find({
29 | userOnly: true,
30 | donatedBy: user._id,
31 | })
32 | .select('-__v -_id -donatedBy')
33 | .lean()
34 | ).concat(domains);
35 |
36 | for (let i = 0; i < domains.length; i++) {
37 | domains[i].users = await UserModel.countDocuments({
38 | 'settings.domain.name': domains[i].name,
39 | });
40 | }
41 |
42 | res.status(200).json({
43 | success: true,
44 | count,
45 | domains,
46 | });
47 | } catch (err) {
48 | res.status(500).json({
49 | success: false,
50 | error: err.message,
51 | });
52 | }
53 | });
54 |
55 | router.post(
56 | '/',
57 | AdminMiddleware,
58 | ValidationMiddleware(DomainSchema),
59 | async (req: Request, res: Response) => {
60 | const {user, body} = req;
61 |
62 | if (body.length <= 0)
63 | return res.status(400).json({
64 | success: false,
65 | error: 'provide at least one domain object',
66 | });
67 | let donator: User = null;
68 |
69 | try {
70 | for (const field of body) {
71 | // eslint-disable-next-line prefer-const
72 | let {name, wildcard, donated, donatedBy, userOnly} = field;
73 | const domain = await DomainModel.findOne({name});
74 |
75 | if (domain)
76 | return res.status(400).json({
77 | success: false,
78 | error: `${name} already exists`,
79 | });
80 |
81 | if (!isValidDomain(name))
82 | return res.status(401).json({
83 | success: false,
84 | error: "domain isn't formatted correctly",
85 | });
86 | if (user && userOnly && !donatedBy) {
87 | donatedBy = user._id;
88 | } else if (donated) {
89 | donatedBy = donator = await UserModel.findOne({
90 | $or: [
91 | {_id: donatedBy},
92 | {username: donatedBy},
93 | {email: donatedBy},
94 | {invite: donatedBy},
95 | {key: donatedBy},
96 | {'discord.id': donatedBy.replace('<@!', '').replace('>', '')},
97 | ],
98 | });
99 | if (!donatedBy) {
100 | return res.status(400).json({
101 | success: false,
102 | error: 'invalid donator',
103 | });
104 | }
105 | }
106 |
107 | await CloudflareUtil.addDomain(name, wildcard).catch(e =>
108 | console.log(e)
109 | );
110 |
111 | await DomainModel.create({
112 | name,
113 | wildcard,
114 | donated: donated || false,
115 | donatedBy: donatedBy || null,
116 | userOnly: userOnly || false,
117 | dateAdded: new Date(),
118 | });
119 | }
120 | if (!req.body[0].userOnly) {
121 | await logDomains(req.body, donator);
122 | }
123 |
124 | res.status(200).json({
125 | success: true,
126 | message: `${
127 | req.body.length > 1
128 | ? `added ${req.body.length} domains`
129 | : 'added domain'
130 | } successfully`,
131 | });
132 | } catch (err) {
133 | res.status(500).json({
134 | success: false,
135 | error: err.message,
136 | });
137 | }
138 | }
139 | );
140 |
141 | router.post(
142 | '/custom',
143 | AuthMiddleware,
144 | ValidationMiddleware(CustomDomainSchema),
145 | async (req: Request, res: Response) => {
146 | const {user} = req;
147 | const {name, wildcard, userOnly} = req.body;
148 |
149 | if (!user.premium && !user.admin)
150 | return res.status(401).json({
151 | success: false,
152 | error: 'you do not have permission to add custom domains',
153 | });
154 |
155 | if (name.startsWith('http'))
156 | return res.status(401).json({
157 | success: false,
158 | error: 'please enter domains in the right format',
159 | });
160 |
161 | if (!isValidDomain(name))
162 | return res.status(401).json({
163 | success: false,
164 | error: "domain isn't formatted correctly",
165 | });
166 |
167 | try {
168 | const now = Date.now();
169 | const difference =
170 | user.lastDomainAddition && now - user.lastDomainAddition.getTime();
171 | const duration = 43200000 - difference;
172 |
173 | if (user.lastDomainAddition && duration > 0) {
174 | const hours = Math.floor(duration / 1000 / 60 / 60);
175 | const minutes = Math.floor((duration / 1000 / 60 / 60 - hours) * 60);
176 | const timeLeft = `${hours} hours and ${minutes} minutes`;
177 |
178 | res.status(400).json({
179 | success: false,
180 | error: `you cannot add a domain for another ${timeLeft}`,
181 | });
182 |
183 | return;
184 | }
185 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
186 | let domain: any = await DomainModel.findOne({
187 | name: {$regex: new RegExp(name, 'i')},
188 | });
189 |
190 | if (domain)
191 | return res.status(400).json({
192 | success: false,
193 | error: `${name} already exists`,
194 | });
195 |
196 | await CloudflareUtil.addDomain(name, wildcard);
197 |
198 | domain = await DomainModel.create({
199 | name,
200 | wildcard,
201 | donated: true,
202 | donatedBy: user._id,
203 | userOnly: userOnly,
204 | dateAdded: new Date(),
205 | });
206 |
207 | await logCustomDomain(domain);
208 |
209 | await UserModel.findByIdAndUpdate(user._id, {
210 | lastDomainAddition: new Date(),
211 | });
212 |
213 | res.status(200).json({
214 | success: true,
215 | message: 'added domain successfully',
216 | domain,
217 | });
218 | } catch (err) {
219 | console.log(err.response.data);
220 | res.status(500).json({
221 | success: false,
222 | error: err.message,
223 | });
224 | }
225 | }
226 | );
227 |
228 | router.delete(
229 | '/:name',
230 | AdminMiddleware,
231 | async (req: Request, res: Response) => {
232 | const {name} = req.params;
233 | const domain = await DomainModel.findOne({name});
234 |
235 | if (!domain)
236 | return res.status(404).json({
237 | success: false,
238 | error: 'invalid domain',
239 | });
240 |
241 | try {
242 | await CloudflareUtil.deleteZone(domain.name).catch(e => console.log(e));
243 | await domain.remove();
244 |
245 | await UserModel.updateMany(
246 | {'settings.domain.name': domain.name},
247 | {
248 | 'settings.domain.name': 'i.higure.wtf',
249 | 'settings.domain.subdomain': null,
250 | }
251 | );
252 |
253 | res.status(200).json({
254 | success: true,
255 | message: 'deleted domain successfully',
256 | });
257 | } catch (err) {
258 | res.status(500).json({
259 | success: false,
260 | error: err.message,
261 | });
262 | }
263 | }
264 | );
265 |
266 | router.get(
267 | '/list',
268 | AdminAuthMiddleware,
269 | async (_req: Request, res: Response) => {
270 | try {
271 | const domains = await DomainModel.find({}).select(
272 | '-__v -_id -wildcard -donated -donatedBy -dateAdded'
273 | );
274 |
275 | res.status(200).json(domains.map(d => d.name).join(', '));
276 | } catch (err) {
277 | res.status(500).json({
278 | success: false,
279 | error: err.message,
280 | });
281 | }
282 | }
283 | );
284 |
285 | router.get(
286 | '/rank',
287 | AdminAuthMiddleware,
288 | async (_req: Request, res: Response) => {
289 | try {
290 | const domains = await DomainModel.find({});
291 | const ranks = [];
292 |
293 | for (const domain of domains) {
294 | const users = await UserModel.countDocuments({
295 | 'settings.domain.name': domain.name,
296 | });
297 | ranks.push({
298 | domain: domain.name,
299 | users,
300 | });
301 | }
302 |
303 | const sorted = ranks.sort((a, b) => a.users - b.users).reverse();
304 |
305 | res.status(200).json(sorted);
306 | } catch (err) {
307 | res.status(500).json({
308 | success: false,
309 | error: err.message,
310 | });
311 | }
312 | }
313 | );
314 |
315 | export default router;
316 |
--------------------------------------------------------------------------------
/src/routes/FilesRouter.ts:
--------------------------------------------------------------------------------
1 | import {Request, Response, Router} from 'express';
2 | import {upload} from '../utils/MulterUtil';
3 | import {s3, wipeFiles} from '../utils/S3Util';
4 | import {formatEmbed, formatFakeUrl, formatFilesize} from '../utils/FormatUtil';
5 | import {generateInvisibleId, generateString} from '../utils/GenerateUtil';
6 | import {DocumentType} from '@typegoose/typegoose';
7 | import {PassThrough} from 'stream';
8 | import UploadMiddleware from '../middlewares/UploadMiddleware';
9 | import FileModel, {File} from '../models/FileModel';
10 | import UserModel, {User} from '../models/UserModel';
11 | import InvisibleUrlModel from '../models/InvisibleUrlModel';
12 | import ValidationMiddleware from '../middlewares/ValidationMiddleware';
13 | import DeletionSchema from '../schemas/DeletionSchema';
14 | import ConfigSchema from '../schemas/ConfigSchema';
15 | import AuthMiddleware from '../middlewares/AuthMiddleware';
16 | import Archiver from 'archiver';
17 | import {extname} from 'path';
18 | import CounterModel from '../models/CounterModel';
19 | import AdminAuthMiddleware from '../middlewares/AdminAuthMiddleware';
20 | //import crypto from 'crypto'
21 |
22 | const rateLimit = require('express-rate-limit');
23 |
24 | const allowedMimetypes = ['video', 'image'];
25 |
26 | const router = Router();
27 | const fileLimiter = rateLimit({
28 | windowMs: 10 * 1000,
29 | max: 3,
30 | headers: false,
31 | });
32 |
33 | router.get('/', AdminAuthMiddleware, async (_req: Request, res: Response) => {
34 | try {
35 | const total = await FileModel.countDocuments();
36 | const invisibleUrls = await InvisibleUrlModel.countDocuments();
37 | const {storageUsed} = await CounterModel.findById('counter');
38 | res.status(200).json({
39 | success: true,
40 | total,
41 | invisibleUrls,
42 | storageUsed: formatFilesize(storageUsed),
43 | });
44 | } catch (err) {
45 | res.status(500).json({
46 | success: false,
47 | error: err.message,
48 | });
49 | }
50 | });
51 |
52 | router.post(
53 | '/',
54 | fileLimiter,
55 | UploadMiddleware,
56 | upload.single('file'),
57 | async (req: Request, res: Response) => {
58 | let {
59 | file,
60 | // eslint-disable-next-line prefer-const
61 | user,
62 | }: {
63 | file: Express.Multer.File | DocumentType;
64 | user: User;
65 | } = req;
66 |
67 | if (!file)
68 | return res.status(400).json({
69 | success: false,
70 | error: 'provide a file',
71 | });
72 |
73 | if (
74 | !allowedMimetypes.some((mimeType: string) =>
75 | file.mimetype.split('/')[0].toLowerCase().includes(mimeType)
76 | )
77 | )
78 | return res.sendStatus(403).json({
79 | success: false,
80 | error: `The allowed mimetypes are ${allowedMimetypes
81 | .map((fileType: string) => `.${fileType}`)
82 | .join(', ')
83 | .replace(/,\s([^,]+)$/, ', and $1')}`,
84 | });
85 |
86 | if ((file.size > 15728640 && !user.premium) || file.size > 104857600)
87 | return res.sendStatus(413).json({
88 | success: false,
89 | error: `your file is too large, your upload limit is: ${
90 | user.premium ? '100' : '15'
91 | } MB`,
92 | });
93 | // const hash = crypto.createHash('sha256');
94 | // file.stream.on('data', function(data) {
95 | // hash.update(data)
96 | // })
97 | // file.stream.on('end', function () {
98 | // hash.digest('hex')
99 | // })
100 | // console.log(hash)
101 | const {
102 | domain,
103 | randomDomain,
104 | embed,
105 | showLink,
106 | invisibleUrl,
107 | fakeUrl,
108 | } = user.settings;
109 |
110 | let baseUrl = req.headers.domain
111 | ? req.headers.domain
112 | : `${
113 | domain.subdomain && domain.subdomain !== ''
114 | ? `${domain.subdomain}.`
115 | : ''
116 | }${domain.name}`;
117 |
118 | if (
119 | req.headers.randomdomain
120 | ? req.headers.randomdomain === 'true'
121 | : randomDomain.enabled
122 | )
123 | baseUrl =
124 | randomDomain.domains.length > 0
125 | ? randomDomain.domains[
126 | Math.floor(Math.random() * randomDomain.domains.length)
127 | ]
128 | : baseUrl;
129 |
130 | let imageUrl = `https://${baseUrl}/${file.filename}`;
131 |
132 | const deletionKey = generateString(40);
133 | const deletionUrl = `${process.env.BACKEND_URL}/files/delete?key=${deletionKey}`;
134 | const timestamp = new Date();
135 |
136 | file = new FileModel({
137 | filename: file.filename,
138 | key: file.key,
139 | timestamp,
140 | mimetype: file.mimetype,
141 | domain: baseUrl,
142 | userOnlyDomain: file.userOnlyDomain ? file.userOnlyDomain : false,
143 | size: formatFilesize(file.size),
144 | deletionKey,
145 | embed,
146 | showLink,
147 | uploader: {
148 | uuid: user._id,
149 | username: user.username,
150 | },
151 | });
152 |
153 | file.embed = formatEmbed(embed, user, file);
154 |
155 | await file.save();
156 |
157 | if (
158 | req.headers.invisibleurl
159 | ? req.headers.invisibleurl === 'true'
160 | : invisibleUrl
161 | ) {
162 | const invisibleUrlId = generateInvisibleId();
163 |
164 | await InvisibleUrlModel.create({
165 | _id: invisibleUrlId,
166 | filename: file.filename,
167 | uploader: user._id,
168 | });
169 |
170 | imageUrl = `https://${baseUrl}/${invisibleUrlId}`;
171 | }
172 | if (fakeUrl && fakeUrl.enabled) {
173 | imageUrl =
174 | (isValidHttpUrl(formatFakeUrl(fakeUrl.url, user, file))
175 | ? '<' + formatFakeUrl(fakeUrl.url, user, file) + '>'
176 | : formatFakeUrl(fakeUrl.url, user, file)) +
177 | '|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ' +
178 | imageUrl;
179 | }
180 | await UserModel.findByIdAndUpdate(user._id, {
181 | $inc: {
182 | uploads: +1,
183 | },
184 | });
185 | res.status(200).json({
186 | success: true,
187 | imageUrl,
188 | deletionUrl,
189 | });
190 | }
191 | );
192 |
193 | function isValidHttpUrl(string: string) {
194 | let url;
195 |
196 | try {
197 | // eslint-disable-next-line node/no-unsupported-features/node-builtins
198 | url = new URL(string);
199 | } catch (_) {
200 | return false;
201 | }
202 |
203 | return url.protocol === 'http:' || url.protocol === 'https:';
204 | }
205 |
206 | router.get(
207 | '/delete',
208 | ValidationMiddleware(DeletionSchema, 'query'),
209 | async (req: Request, res: Response) => {
210 | const deletionKey = req.query.key as string;
211 | const file = await FileModel.findOne({deletionKey});
212 |
213 | if (!file)
214 | return res.status(404).json({
215 | success: false,
216 | error: 'invalid deletion key',
217 | });
218 |
219 | const user = await UserModel.findById(file.uploader.uuid);
220 |
221 | const params = {
222 | Bucket: process.env.S3_BUCKET,
223 | Key: file.key,
224 | };
225 |
226 | try {
227 | await s3.deleteObject(params).promise();
228 |
229 | if (user.uploads > 0)
230 | await UserModel.findByIdAndUpdate(user._id, {
231 | $inc: {
232 | uploads: -1,
233 | },
234 | });
235 |
236 | await file.remove();
237 |
238 | res.status(200).json({
239 | success: true,
240 | message: 'deleted file successfully',
241 | });
242 | } catch (err) {
243 | res.status(500).json({
244 | success: false,
245 | error: err.message,
246 | });
247 | }
248 | }
249 | );
250 |
251 | router.delete('/:id', AuthMiddleware, async (req: Request, res: Response) => {
252 | const {id} = req.params;
253 | const {user} = req;
254 |
255 | const file = await FileModel.findOne({filename: id});
256 |
257 | if (!file)
258 | return res.status(404).json({
259 | success: false,
260 | error: 'invalid file',
261 | });
262 | if (user._id !== file.uploader.uuid)
263 | return res.status(404).json({
264 | success: false,
265 | error: 'not your file',
266 | });
267 |
268 | const params = {
269 | Bucket: process.env.S3_BUCKET,
270 | Key: file.key,
271 | };
272 |
273 | try {
274 | await s3.deleteObject(params).promise();
275 |
276 | if (user.uploads > 0)
277 | await UserModel.findByIdAndUpdate(user._id, {
278 | $inc: {
279 | uploads: -1,
280 | },
281 | });
282 |
283 | await file.remove();
284 |
285 | res.status(200).json({
286 | success: true,
287 | message: 'deleted file successfully',
288 | });
289 | } catch (err) {
290 | res.status(500).json({
291 | success: false,
292 | error: err.message,
293 | });
294 | }
295 | });
296 |
297 | router.post('/wipe', AuthMiddleware, async (req: Request, res: Response) => {
298 | const {user} = req;
299 |
300 | try {
301 | const count = await wipeFiles(user);
302 |
303 | await FileModel.deleteMany({
304 | 'uploader.uuid': user._id,
305 | });
306 |
307 | await InvisibleUrlModel.deleteMany({
308 | uploader: user._id,
309 | });
310 |
311 | await UserModel.findByIdAndUpdate(user._id, {
312 | uploads: 0,
313 | });
314 |
315 | res.status(200).json({
316 | success: true,
317 | message: `wiped ${count} files successfully`,
318 | count,
319 | });
320 | } catch (err) {
321 | res.status(500).json({
322 | success: false,
323 | error: err.message,
324 | });
325 | }
326 | });
327 |
328 | router.get(
329 | '/config',
330 | ValidationMiddleware(ConfigSchema, 'query'),
331 | async (req: Request, res: Response) => {
332 | const key = req.query.key as string;
333 | const user = await UserModel.findOne({key});
334 |
335 | if (!user)
336 | return res.status(401).json({
337 | success: false,
338 | error: 'unauthorized',
339 | });
340 |
341 | const config = {
342 | Name: 'higure.wtf file uploader',
343 | DestinationType: 'ImageUploader, FileUploader',
344 | RequestType: 'POST',
345 | RequestURL: 'https://api.higure.wtf/files',
346 | FileFormName: 'file',
347 | Body: 'MultipartFormData',
348 | Headers: {
349 | key,
350 | },
351 | URL: '$json:imageUrl$',
352 | DeletionURL: '$json:deletionUrl$',
353 | ErrorMessage: '$json:error$',
354 | };
355 |
356 | res.set('Content-Disposition', 'attachment; filename=higure.wtf.sxcu');
357 | res.send(Buffer.from(JSON.stringify(config, null, 2), 'utf8'));
358 | }
359 | );
360 |
361 | function writeStream(key: string) {
362 | const passThrough = new PassThrough();
363 |
364 | const params = {
365 | Bucket: process.env.S3_BUCKET,
366 | Key: key,
367 | Body: passThrough,
368 | ACL: 'public-read',
369 | };
370 |
371 | return {
372 | passThrough,
373 | uploaded: s3.upload(params, err => {
374 | throw new Error(err);
375 | }),
376 | };
377 | }
378 |
379 | router.get('/archive', AuthMiddleware, async (req: Request, res: Response) => {
380 | const {user} = req;
381 |
382 | if (user.uploads <= 0)
383 | return res.status(400).json({
384 | success: false,
385 | error: "you haven't uploaded any files",
386 | });
387 |
388 | try {
389 | const now = Date.now();
390 | const difference =
391 | user.lastFileArchive && now - user.lastFileArchive.getTime();
392 | const duration = 43200000 - difference;
393 |
394 | if (user.lastFileArchive && duration > 0) {
395 | const hours = Math.floor(duration / 1000 / 60 / 60);
396 | const minutes = Math.floor((duration / 1000 / 60 / 60 - hours) * 60);
397 | const timeLeft = `${hours} hours and ${minutes} minutes`;
398 |
399 | res.status(400).json({
400 | success: false,
401 | error: `you cannot create a file archive for another ${timeLeft}`,
402 | });
403 |
404 | return;
405 | }
406 |
407 | const params = {
408 | Bucket: process.env.S3_BUCKET,
409 | Prefix: `${user._id}/`,
410 | };
411 |
412 | const objects = await s3.listObjectsV2(params).promise();
413 | const streams = objects.Contents.map(object => {
414 | return {
415 | stream: s3
416 | .getObject({Bucket: process.env.S3_BUCKET, Key: object.Key})
417 | .createReadStream(),
418 | object: object,
419 | };
420 | });
421 |
422 | const {passThrough, uploaded} = writeStream(
423 | `${user._id}/${generateString(5)}.zip`
424 | );
425 |
426 | await new Promise((resolve, reject) => {
427 | const archive = Archiver('zip');
428 |
429 | archive.on('error', err => {
430 | throw new Error(err.message);
431 | });
432 |
433 | passThrough.on('close', resolve);
434 | passThrough.on('end', resolve);
435 | passThrough.on('error', reject);
436 |
437 | archive.pipe(passThrough);
438 |
439 | let i = 1;
440 |
441 | streams.forEach(ctx => {
442 | if (
443 | !ctx.object.Key.endsWith('/') &&
444 | extname(ctx.object.Key) !== '.zip'
445 | ) {
446 | archive.append(ctx.stream, {
447 | name: i.toString() + extname(ctx.object.Key),
448 | });
449 |
450 | i++;
451 | }
452 | });
453 |
454 | archive.finalize();
455 | }).catch(err => {
456 | throw new Error(err);
457 | });
458 |
459 | const {Key} = await uploaded.promise();
460 | const Location = `${process.env.S3_ENDPOINT}/${process.env.S3_BUCKET}/${Key}`;
461 |
462 | // await sendFileArchive(
463 | // user,
464 | // s3.getObject({Bucket: process.env.S3_BUCKET, Key}).createReadStream()
465 | // );
466 |
467 | await UserModel.findByIdAndUpdate(user._id, {
468 | lastFileArchive: new Date(),
469 | });
470 |
471 | res.status(200).json({
472 | success: true,
473 | message: 'sent archive to your email successfully',
474 | directLink: Location,
475 | });
476 | } catch (err) {
477 | res.status(500).json({
478 | success: false,
479 | error: err.message,
480 | });
481 | }
482 | });
483 |
484 | export default router;
485 |
--------------------------------------------------------------------------------
/src/routes/InvitesRouter.ts:
--------------------------------------------------------------------------------
1 | import {Request, Response, Router} from 'express';
2 | import AdminMiddleware from '../middlewares/AdminMiddleware';
3 | import AuthMiddleware from '../middlewares/AuthMiddleware';
4 | import InviteModel from '../models/InviteModel';
5 | import UserModel from '../models/UserModel';
6 | import {generateInvite} from '../utils/GenerateUtil';
7 |
8 | const router = Router();
9 |
10 | router.post('/', AuthMiddleware, async (req: Request, res: Response) => {
11 | const {user} = req;
12 |
13 | if (user.invites <= 0 && !user.admin)
14 | return res.status(401).json({
15 | success: false,
16 | error: 'you do not have any invites',
17 | });
18 |
19 | const invite = generateInvite();
20 | const dateCreated = new Date();
21 |
22 | await InviteModel.create({
23 | _id: invite,
24 | createdBy: {
25 | username: user.username,
26 | uuid: user._id,
27 | },
28 | dateCreated,
29 | dateRedeemed: null,
30 | usedBy: null,
31 | redeemed: false,
32 | useable: true,
33 | });
34 |
35 | if (!user.admin)
36 | await UserModel.findByIdAndUpdate(user._id, {
37 | invites: user.invites - 1,
38 | });
39 |
40 | res.status(200).json({
41 | success: true,
42 | link: `https://higure.wtf/?code=${invite}`,
43 | code: invite,
44 | dateCreated,
45 | });
46 | });
47 |
48 | router.get('/', AdminMiddleware, async (_req: Request, res: Response) => {
49 | const count = await InviteModel.countDocuments();
50 |
51 | const invites = await InviteModel.find({}).select('-__v');
52 |
53 | const redeemedInvites = await InviteModel.find({redeemed: true}).select(
54 | '-__v'
55 | );
56 |
57 | const unusableInvites = await InviteModel.find({useable: false}).select(
58 | '-__v'
59 | );
60 |
61 | res.json({
62 | success: true,
63 | count,
64 | invites,
65 | redeemedInvites,
66 | unusableInvites,
67 | });
68 | });
69 |
70 | router.get('/:code', AdminMiddleware, async (req: Request, res: Response) => {
71 | const {code} = req.params;
72 |
73 | const invite = await InviteModel.findById(code).select('-__v');
74 |
75 | if (!invite)
76 | return res.status(404).json({
77 | success: false,
78 | error: 'invalid invite code',
79 | });
80 |
81 | res.status(200).json({
82 | success: true,
83 | invite,
84 | });
85 | });
86 |
87 | router.delete(
88 | '/:code',
89 | AdminMiddleware,
90 | async (req: Request, res: Response) => {
91 | const {code} = req.params;
92 |
93 | const invite = await InviteModel.findById(code);
94 |
95 | if (!invite)
96 | return res.status(404).json({
97 | success: false,
98 | error: 'invalid invite code',
99 | });
100 |
101 | try {
102 | await InviteModel.findByIdAndUpdate(invite._id, {
103 | useable: false,
104 | });
105 |
106 | res.status(200).json({
107 | success: true,
108 | message: 'deleted invite successfully',
109 | });
110 | } catch (err) {
111 | res.status(500).json({
112 | success: false,
113 | error: err.message,
114 | });
115 | }
116 | }
117 | );
118 |
119 | export default router;
120 |
--------------------------------------------------------------------------------
/src/routes/ShortenerRouter.ts:
--------------------------------------------------------------------------------
1 | import {Request, Response, Router} from 'express';
2 | import {generateString} from '../utils/GenerateUtil';
3 | import UploadMiddleware from '../middlewares/UploadMiddleware';
4 | import ValidationMiddleware from '../middlewares/ValidationMiddleware';
5 | import ShortenerModel from '../models/ShortenerModel';
6 | import UserModel from '../models/UserModel';
7 | import ConfigSchema from '../schemas/ConfigSchema';
8 | import DeletionSchema from '../schemas/DeletionSchema';
9 | import ShortenerSchema from '../schemas/ShortenerSchema';
10 | import AuthMiddleware from '../middlewares/AuthMiddleware';
11 | import {isMalicious} from '../utils/SafetyUtils';
12 |
13 | const router = Router();
14 |
15 | router.get('/urls', AuthMiddleware, async (req: Request, res: Response) => {
16 | const {user} = req;
17 |
18 | try {
19 | const urls = await ShortenerModel.find({user: user._id}).select(
20 | '-_id -__v'
21 | );
22 |
23 | res.status(200).json({
24 | success: true,
25 | urls,
26 | count: urls.length,
27 | });
28 | } catch (err) {
29 | res.status(500).json({
30 | success: false,
31 | error: err.message,
32 | });
33 | }
34 | });
35 |
36 | router.post(
37 | '/',
38 | UploadMiddleware,
39 | ValidationMiddleware(ShortenerSchema),
40 | async (req: Request, res: Response) => {
41 | const {user} = req;
42 | const {url} = req.body;
43 |
44 | if (await isMalicious(url)) {
45 | if (user.strikes === 3 || user.strikes + 1 === 3) {
46 | await UserModel.findByIdAndUpdate(user._id, {
47 | blacklisted: {
48 | status: true,
49 | reason: 'banned by auto-mod, shortening iploggers',
50 | },
51 | strikes: 3,
52 | });
53 |
54 | res.status(400).json({
55 | success: false,
56 | error:
57 | 'you have been suspended by auto-mod, create a ticket in the discord to appeal',
58 | });
59 |
60 | return;
61 | }
62 |
63 | await UserModel.findByIdAndUpdate(user._id, {
64 | $inc: {
65 | strikes: +1,
66 | },
67 | });
68 |
69 | res.status(400).json({
70 | success: false,
71 | error:
72 | 'ip logger detected, attempting to shorten any more ip loggers will result in a suspension',
73 | });
74 |
75 | return;
76 | }
77 |
78 | try {
79 | const {domain} = user.settings;
80 | const longUrl = req.headers.longurl
81 | ? req.headers.longurl === 'true'
82 | : user.settings.longUrl;
83 |
84 | const shortId = longUrl ? generateString(17) : generateString(10);
85 |
86 | const baseUrl = req.headers.domain
87 | ? req.headers.domain
88 | : `${
89 | domain.subdomain && domain.subdomain !== ''
90 | ? `${domain.subdomain}.`
91 | : ''
92 | }${domain.name}`;
93 |
94 | const deletionKey = generateString(40);
95 | const deletionUrl = `${process.env.BACKEND_URL}/shortener/delete?key=${deletionKey}`;
96 | const shortendUrl = `https://${baseUrl}/s/${shortId}`;
97 |
98 | await ShortenerModel.create({
99 | shortId,
100 | destination: url,
101 | deletionKey,
102 | timestamp: new Date(),
103 | user: user._id,
104 | });
105 |
106 | res.status(200).json({
107 | success: true,
108 | shortendUrl,
109 | deletionUrl,
110 | document: await ShortenerModel.findOne({shortId}).select('-_id -__v'),
111 | });
112 | } catch (err) {
113 | res.status(500).json({
114 | success: false,
115 | error: err.message,
116 | });
117 | }
118 | }
119 | );
120 |
121 | router.get(
122 | '/delete',
123 | ValidationMiddleware(DeletionSchema, 'query'),
124 | async (req: Request, res: Response) => {
125 | const deletionKey = req.query.key as string;
126 | const shortened = await ShortenerModel.findOne({deletionKey});
127 |
128 | if (!shortened)
129 | return res.status(404).json({
130 | success: false,
131 | error: 'invalid deletion key',
132 | });
133 |
134 | try {
135 | await shortened.remove();
136 |
137 | res.status(200).json({
138 | success: true,
139 | message: 'deleted url successfully',
140 | });
141 | } catch (err) {
142 | res.status(500).json({
143 | success: false,
144 | error: err.message,
145 | });
146 | }
147 | }
148 | );
149 |
150 | router.get(
151 | '/config',
152 | ValidationMiddleware(ConfigSchema, 'query'),
153 | async (req: Request, res: Response) => {
154 | const key = req.query.key as string;
155 | const user = await UserModel.findOne({key});
156 |
157 | if (!user)
158 | return res.status(401).json({
159 | success: false,
160 | error: 'unauthorized',
161 | });
162 |
163 | const config = {
164 | Name: 'higure.wtf shortener',
165 | DestinationType: 'URLShortener',
166 | RequestMethod: 'POST',
167 | RequestURL: `${process.env.BACKEND_URL}/shortener`,
168 | Headers: {
169 | key: user.key,
170 | },
171 | Body: 'JSON',
172 | Data: '{"url":"$input$"}',
173 | URL: '$json:shortendUrl$',
174 | DeletionURL: '$json:deletionUrl$',
175 | ErrorMessage: '$json:error$',
176 | };
177 |
178 | res.set(
179 | 'Content-Disposition',
180 | 'attachment; filename=higure.wtf-shortener.sxcu'
181 | );
182 | res.send(Buffer.from(JSON.stringify(config, null, 2), 'utf8'));
183 | }
184 | );
185 |
186 | export default router;
187 |
--------------------------------------------------------------------------------
/src/routes/UsersRouter/MeRouter.ts:
--------------------------------------------------------------------------------
1 | import {Request, Response, Router} from 'express';
2 | import {s3} from '../../utils/S3Util';
3 | import AuthMiddleware from '../../middlewares/AuthMiddleware';
4 | import ValidationMiddleware from '../../middlewares/ValidationMiddleware';
5 | import UserModel from '../../models/UserModel';
6 | import SettingsRouter from './SettingsRouter';
7 | import {formatFilesize} from '../../utils/FormatUtil';
8 | import {generateString} from '../../utils/GenerateUtil';
9 | import InviteModel from '../../models/InviteModel';
10 | import RefreshTokenModel from '../../models/RefreshTokenModel';
11 | import ChangeUsernameSchema from '../../schemas/ChangeUsernameSchema';
12 | import {hash, verify} from 'argon2';
13 | import ChangePasswordSchema from '../../schemas/ChangePasswordSchema';
14 | import {getMOTD} from '../../app';
15 |
16 | const router = Router();
17 |
18 | router.use(AuthMiddleware);
19 | router.use('/settings', SettingsRouter);
20 |
21 | router.get('/', async (req: Request, res: Response) => {
22 | const {user} = req;
23 |
24 | res.status(200).json(user);
25 | });
26 |
27 | router.get('/images', async (req: Request, res: Response) => {
28 | const {user} = req;
29 | try {
30 | const params = {
31 | Bucket: process.env.S3_BUCKET,
32 | Prefix: `${user._id}/`,
33 | };
34 | const objects = await s3.listObjectsV2(params).promise();
35 | objects.Contents.sort(
36 | (a, b) => b.LastModified.getTime() - a.LastModified.getTime()
37 | );
38 | const images = [];
39 | for (const object of objects.Contents) {
40 | images.push({
41 | link: `${process.env.S3_ENDPOINT}/${process.env.S3_BUCKET}/${
42 | user._id
43 | }/${object.Key.split('/')[1]}`,
44 | dateUploaded: object.LastModified,
45 | filename: object.Key.split('/')[1],
46 | size: formatFilesize(object.Size),
47 | });
48 | }
49 | res.status(200).json({
50 | success: true,
51 | images,
52 | motd: getMOTD(),
53 | });
54 | } catch (err) {
55 | res.status(500).json({
56 | success: false,
57 | error: err.message,
58 | });
59 | }
60 | });
61 |
62 | router.post('/disable', async (req: Request, res: Response) => {
63 | const {user} = req;
64 |
65 | try {
66 | await UserModel.findByIdAndUpdate(user._id, {
67 | disabled: true,
68 | });
69 |
70 | await RefreshTokenModel.deleteMany({user: user._id});
71 |
72 | res.clearCookie('x-refresh-token');
73 |
74 | res.status(200).json({
75 | success: true,
76 | message: 'disabled account successfully',
77 | });
78 | } catch (err) {
79 | res.status(500).json({
80 | success: false,
81 | error: err.message,
82 | });
83 | }
84 | });
85 |
86 | router.post('/regen_key', async (req: Request, res: Response) => {
87 | const {user} = req;
88 |
89 | try {
90 | const now = Date.now();
91 | const difference = user.lastKeyRegen && now - user.lastKeyRegen.getTime();
92 | const duration = 43200000 - difference;
93 |
94 | if (user.lastKeyRegen && duration > 0) {
95 | const hours = Math.floor(duration / 1000 / 60 / 60);
96 | const minutes = Math.floor((duration / 1000 / 60 / 60 - hours) * 60);
97 | const timeLeft = `${hours} hours and ${minutes} minutes`;
98 |
99 | res.status(400).json({
100 | success: false,
101 | error: `you cannot regen your key for another ${timeLeft}`,
102 | });
103 |
104 | return;
105 | }
106 |
107 | const key = `${user.username}_${generateString(30)}`;
108 |
109 | await UserModel.findByIdAndUpdate(user._id, {
110 | lastKeyRegen: new Date(),
111 | key,
112 | });
113 |
114 | res.status(200).json({
115 | success: true,
116 | key,
117 | message: 'regenerated key successfully',
118 | });
119 | } catch (err) {
120 | res.status(500).json({
121 | success: false,
122 | error: err.message,
123 | });
124 | }
125 | });
126 |
127 | router.get('/created_invites', async (req: Request, res: Response) => {
128 | const {user} = req;
129 |
130 | try {
131 | // eslint-disable-next-line quote-props
132 | const invites = await InviteModel.find({
133 | 'createdBy.uuid': user._id,
134 | useable: true,
135 | }).select('-__v -createdBy');
136 |
137 | res.status(200).json({
138 | success: true,
139 | invites,
140 | });
141 | } catch (err) {
142 | res.status(500).json({
143 | success: false,
144 | error: err.message,
145 | });
146 | }
147 | });
148 |
149 | router.put(
150 | '/change_username',
151 | ValidationMiddleware(ChangeUsernameSchema),
152 | async (req: Request, res: Response) => {
153 | let {user} = req;
154 | const {username, password} = req.body;
155 |
156 | try {
157 | user = await UserModel.findById(user._id);
158 | const correctPassword = await verify(user.password, password);
159 |
160 | if (!correctPassword)
161 | return res.status(401).json({
162 | success: false,
163 | error: 'invalid password',
164 | });
165 |
166 | const now = Date.now();
167 | const difference =
168 | user.lastUsernameChange && now - user.lastUsernameChange.getTime();
169 | const duration = 1209600000 - difference;
170 |
171 | if (user.lastUsernameChange && duration > 0) {
172 | const hours = Math.floor(duration / 1000 / 60 / 60);
173 | const minutes = Math.floor((duration / 1000 / 60 / 60 - hours) * 60);
174 | const days = Math.floor(hours / 24);
175 | const timeLeft = `${days} days, ${hours} hours and ${minutes} minutes`;
176 |
177 | res.status(400).json({
178 | success: false,
179 | error: `you cannot change your username for another ${timeLeft}`,
180 | });
181 |
182 | return;
183 | }
184 |
185 | if (username.toLowerCase() === user.username.toLowerCase())
186 | return res.status(400).json({
187 | success: false,
188 | error: 'provide a new username',
189 | });
190 |
191 | const usernameTaken = await UserModel.findOne({
192 | username: {$regex: new RegExp(username, 'i')},
193 | });
194 |
195 | if (usernameTaken)
196 | return res.status(400).json({
197 | success: false,
198 | error: 'the provided username is already taken',
199 | });
200 |
201 | await UserModel.findByIdAndUpdate(user._id, {
202 | username,
203 | lastUsernameChange: new Date(),
204 | });
205 |
206 | res.status(200).json({
207 | success: true,
208 | message: 'changed username successfully',
209 | });
210 | } catch (err) {
211 | res.status(500).json({
212 | success: false,
213 | error: err.message,
214 | });
215 | }
216 | }
217 | );
218 |
219 | router.put(
220 | '/change_password',
221 | ValidationMiddleware(ChangePasswordSchema),
222 | async (req: Request, res: Response) => {
223 | let {user} = req;
224 | const {newPassword, password} = req.body;
225 |
226 | try {
227 | user = await UserModel.findById(user._id);
228 | const correctPassword = await verify(user.password, password);
229 |
230 | if (!correctPassword)
231 | return res.status(401).json({
232 | success: false,
233 | error: 'invalid password',
234 | });
235 |
236 | if (await verify(user.password, newPassword))
237 | return res.status(400).json({
238 | succes: false,
239 | error: 'choose a new password',
240 | });
241 |
242 | const hashed = await hash(newPassword);
243 |
244 | await UserModel.findByIdAndUpdate(user._id, {
245 | password: hashed,
246 | });
247 |
248 | await RefreshTokenModel.deleteMany({user: user._id});
249 |
250 | res.status(200).json({
251 | success: true,
252 | message: 'changed password successfully',
253 | });
254 | } catch (err) {
255 | res.status(500).json({
256 | success: false,
257 | error: err.message,
258 | });
259 | }
260 | }
261 | );
262 |
263 | export default router;
264 |
--------------------------------------------------------------------------------
/src/routes/UsersRouter/SettingsRouter.ts:
--------------------------------------------------------------------------------
1 | import {Request, Response, Router} from 'express';
2 | import ms from 'ms';
3 | import ValidationMiddleware from '../../middlewares/ValidationMiddleware';
4 | import DomainModel from '../../models/DomainModel';
5 | import FileModel from '../../models/FileModel';
6 | import InvisibleUrlModel from '../../models/InvisibleUrlModel';
7 | import UserModel from '../../models/UserModel';
8 | import EmbedSchema from '../../schemas/EmbedSchema';
9 | import PreferencesSchema from '../../schemas/PreferencesSchema';
10 | import RandomDomainSchema from '../../schemas/RandomDomainSchema';
11 | import UpdateDomainSchema from '../../schemas/UpdateDomainSchema';
12 | import WipeIntervalSchema from '../../schemas/WipeIntervalSchema';
13 | import {delInterval, intervals} from '../../utils/Intervals';
14 | import {wipeFiles} from '../../utils/S3Util';
15 | import FakeUrlSchema from '../../schemas/FakeUrlSchema';
16 |
17 | const router = Router();
18 |
19 | router.put(
20 | '/domain',
21 | ValidationMiddleware(UpdateDomainSchema),
22 | async (req: Request, res: Response) => {
23 | const {user} = req;
24 | // eslint-disable-next-line prefer-const
25 | let {domain, subdomain} = req.body;
26 |
27 | try {
28 | const validDomain = await DomainModel.findOne({name: domain});
29 |
30 | if (!validDomain)
31 | return res.status(400).json({
32 | success: false,
33 | error: 'invalid domain name',
34 | });
35 |
36 | if (validDomain.userOnly && validDomain.donatedBy !== user._id)
37 | return res.status(400).json({
38 | success: false,
39 | error: 'you do not have permission to use this domain',
40 | });
41 |
42 | if (!validDomain.wildcard) subdomain = null;
43 |
44 | await UserModel.findByIdAndUpdate(user._id, {
45 | 'settings.domain': {
46 | name: domain,
47 | subdomain: subdomain || null,
48 | },
49 | });
50 |
51 | res.status(200).json({
52 | success: true,
53 | message: 'updated domain successfully',
54 | });
55 | } catch (err) {
56 | res.status(500).json({
57 | success: false,
58 | error: err.message,
59 | });
60 | }
61 | }
62 | );
63 |
64 | router.post(
65 | '/random_domain',
66 | ValidationMiddleware(RandomDomainSchema),
67 | async (req: Request, res: Response) => {
68 | const {user} = req;
69 | const {domain} = req.body;
70 | const {domains} = user.settings.randomDomain;
71 |
72 | try {
73 | if (domains.find(d => d === domain))
74 | return res.status(400).json({
75 | success: false,
76 | error: 'this domain is already in use',
77 | });
78 |
79 | await UserModel.findByIdAndUpdate(user._id, {
80 | $push: {
81 | 'settings.randomDomain.domains': domain,
82 | },
83 | });
84 |
85 | res.status(200).json({
86 | success: true,
87 | message: 'added domain successfully',
88 | });
89 | } catch (err) {
90 | res.status(500).json({
91 | success: false,
92 | error: err.message,
93 | });
94 | }
95 | }
96 | );
97 |
98 | router.delete(
99 | '/random_domain',
100 | ValidationMiddleware(RandomDomainSchema),
101 | async (req: Request, res: Response) => {
102 | const {user} = req;
103 | const {domain} = req.body;
104 | const {domains} = user.settings.randomDomain;
105 |
106 | try {
107 | if (!domains.find(d => d === domain))
108 | return res.status(404).json({
109 | success: false,
110 | error: 'invalid domain',
111 | });
112 |
113 | await UserModel.findByIdAndUpdate(user._id, {
114 | 'settings.randomDomain.domains': domains.filter(d => d !== domain),
115 | });
116 |
117 | res.status(200).json({
118 | success: true,
119 | message: 'deleted domain successfully',
120 | });
121 | } catch (err) {
122 | res.status(500).json({
123 | success: false,
124 | error: err.message,
125 | });
126 | }
127 | }
128 | );
129 |
130 | router.put(
131 | '/preferences',
132 | ValidationMiddleware(PreferencesSchema),
133 | async (req: Request, res: Response) => {
134 | const {user} = req;
135 |
136 | try {
137 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
138 | const toUpdate: any = {};
139 |
140 | for (const entry of Object.entries(req.body)) {
141 | switch (entry[0]) {
142 | case 'randomDomain':
143 | toUpdate['settings.randomDomain.enabled'] = entry[1];
144 | break;
145 | case 'fakeUrl':
146 | toUpdate['settings.fakeUrl.enabled'] = entry[1];
147 | break;
148 | case 'autoWipe':
149 | // eslint-disable-next-line no-case-declarations
150 | const findInterval = intervals.find(i => i.uuid === user._id);
151 |
152 | if (findInterval) {
153 | clearInterval(findInterval.id);
154 | delInterval(user._id);
155 | }
156 |
157 | if (entry[1] === true) {
158 | const interval = setInterval(async () => {
159 | try {
160 | await wipeFiles(user);
161 |
162 | await FileModel.deleteMany({
163 | 'uploader.uuid': user._id,
164 | });
165 |
166 | await InvisibleUrlModel.deleteMany({
167 | uploader: user._id,
168 | });
169 |
170 | await UserModel.findByIdAndUpdate(user._id, {
171 | uploads: 0,
172 | });
173 | // eslint-disable-next-line no-empty
174 | } catch (err) {}
175 | }, user.settings.autoWipe.interval);
176 |
177 | intervals.push({
178 | id: interval,
179 | uuid: user._id,
180 | });
181 | }
182 |
183 | toUpdate['settings.autoWipe.enabled'] = entry[1];
184 | break;
185 | case 'embeds':
186 | toUpdate['settings.embed.enabled'] = entry[1];
187 | break;
188 | default:
189 | toUpdate[`settings.${entry[0]}`] = entry[1];
190 | break;
191 | }
192 | }
193 |
194 | await UserModel.findByIdAndUpdate(user._id, toUpdate);
195 |
196 | res.status(200).json({
197 | success: true,
198 | message: 'updated preferences successfully',
199 | });
200 | } catch (err) {
201 | res.status(500).json({
202 | success: false,
203 | error: err.message,
204 | });
205 | }
206 | }
207 | );
208 |
209 | router.put(
210 | '/embed',
211 | ValidationMiddleware(EmbedSchema),
212 | async (req: Request, res: Response) => {
213 | const {color, title, description, author, randomColor} = req.body;
214 | const {user} = req;
215 |
216 | try {
217 | await UserModel.findByIdAndUpdate(user._id, {
218 | settings: {
219 | ...user.settings,
220 | embed: {
221 | ...user.settings.embed,
222 | title,
223 | description,
224 | color,
225 | author,
226 | randomColor,
227 | },
228 | },
229 | });
230 |
231 | res.status(200).json({
232 | success: true,
233 | message: 'updated embed successfully',
234 | });
235 | } catch (err) {
236 | res.status(500).json({
237 | success: false,
238 | error: err.message,
239 | });
240 | }
241 | }
242 | );
243 | router.put(
244 | '/fakeUrl',
245 | ValidationMiddleware(FakeUrlSchema),
246 | async (req: Request, res: Response) => {
247 | const {url} = req.body;
248 | const {user} = req;
249 |
250 | try {
251 | await UserModel.findByIdAndUpdate(user._id, {
252 | settings: {
253 | ...user.settings,
254 | fakeUrl: {
255 | ...user.settings.fakeUrl,
256 | url,
257 | },
258 | },
259 | });
260 |
261 | res.status(200).json({
262 | success: true,
263 | message: 'updated fakeUrl successfully',
264 | });
265 | } catch (err) {
266 | res.status(500).json({
267 | success: false,
268 | error: err.message,
269 | });
270 | }
271 | }
272 | );
273 |
274 | router.put(
275 | '/wipe_interval',
276 | ValidationMiddleware(WipeIntervalSchema),
277 | async (req: Request, res: Response) => {
278 | const {value} = req.body;
279 | const {user} = req;
280 |
281 | try {
282 | const validIntervals = [
283 | ms('1h'),
284 | ms('2h'),
285 | ms('12h'),
286 | ms('24h'),
287 | ms('1w'),
288 | ms('2w'),
289 | 2147483647,
290 | ];
291 |
292 | if (!validIntervals.includes(value))
293 | return res.status(400).json({
294 | success: false,
295 | error: 'invalid interval',
296 | });
297 |
298 | await UserModel.findByIdAndUpdate(user._id, {
299 | 'settings.autoWipe.interval': value,
300 | });
301 |
302 | if (user.settings.autoWipe.enabled) {
303 | const findInterval = intervals.find(i => i.uuid === user._id);
304 |
305 | if (findInterval) {
306 | clearInterval(findInterval.id);
307 | delInterval(user._id);
308 | }
309 |
310 | const interval = setInterval(async () => {
311 | try {
312 | await wipeFiles(user);
313 | await FileModel.deleteMany({
314 | 'uploader.uuid': user._id,
315 | });
316 | await InvisibleUrlModel.deleteMany({
317 | uploader: user._id,
318 | });
319 | await UserModel.findByIdAndUpdate(user._id, {
320 | uploads: 0,
321 | });
322 | // eslint-disable-next-line no-empty
323 | } catch (err) {}
324 | }, value);
325 |
326 | intervals.push({
327 | id: interval,
328 | uuid: user._id,
329 | });
330 | }
331 |
332 | res.status(200).json({
333 | success: true,
334 | message: 'update interval successfully',
335 | });
336 | } catch (err) {
337 | res.status(500).json({
338 | success: false,
339 | error: err.message,
340 | });
341 | }
342 | }
343 | );
344 |
345 | export default router;
346 |
--------------------------------------------------------------------------------
/src/routes/UsersRouter/index.ts:
--------------------------------------------------------------------------------
1 | import {Request, Response, Router} from 'express';
2 | import AdminMiddleware from '../../middlewares/AdminMiddleware';
3 | import AuthMiddleware from '../../middlewares/AuthMiddleware';
4 | import UserModel from '../../models/UserModel';
5 | import MeRouter from './MeRouter';
6 | import InviteModel from '../../models/InviteModel';
7 | import AdminAuthMiddleware from '../../middlewares/AdminAuthMiddleware';
8 |
9 | const router = Router();
10 |
11 | router.use('/@me', MeRouter);
12 |
13 | router.get('/', AdminAuthMiddleware, async (_req: Request, res: Response) => {
14 | try {
15 | const total = await UserModel.countDocuments();
16 | const blacklisted = await UserModel.countDocuments({
17 | 'blacklisted.status': true,
18 | });
19 | const unusedInvites = await InviteModel.countDocuments({redeemed: false});
20 | const premium = await UserModel.countDocuments({premium: true});
21 |
22 | res.status(200).json({
23 | success: true,
24 | total,
25 | blacklisted,
26 | unusedInvites,
27 | premium,
28 | });
29 | } catch (err) {
30 | res.status(500).json({
31 | success: false,
32 | error: err.message,
33 | });
34 | }
35 | });
36 |
37 | router.get('/:id', AdminMiddleware, async (req: Request, res: Response) => {
38 | const {id} = req.params;
39 |
40 | const user = await UserModel.findById(id).select('-__v -password');
41 |
42 | if (!user)
43 | return res.status(404).json({
44 | success: false,
45 | error: 'invalid user',
46 | });
47 |
48 | res.status(200).json({
49 | success: true,
50 | user,
51 | });
52 | });
53 |
54 | router.get(
55 | '/profile/:id',
56 | AuthMiddleware,
57 | async (req: Request, res: Response) => {
58 | try {
59 | const id = parseInt(req.params.id);
60 | const user = await UserModel.findOne({uid: id});
61 |
62 | if (!user)
63 | return res.status(404).json({
64 | success: false,
65 | error: 'invalid user',
66 | });
67 |
68 | res.status(200).json({
69 | success: true,
70 | user: {
71 | uuid: user._id,
72 | uid: user.uid,
73 | username: user.username,
74 | registrationDate: user.registrationDate,
75 | role: user.blacklisted.status
76 | ? 'Blacklisted'
77 | : user.admin
78 | ? 'Admin'
79 | : user.premium
80 | ? 'Premium'
81 | : 'Whitelisted',
82 | uploads: user.uploads,
83 | invitedBy: user.invitedBy,
84 | avatar: user.discord.avatar,
85 | },
86 | });
87 | } catch (err) {
88 | res.status(500).json({
89 | success: false,
90 | error: err.message,
91 | });
92 | }
93 | }
94 | );
95 |
96 | export default router;
97 |
--------------------------------------------------------------------------------
/src/routes/index.ts:
--------------------------------------------------------------------------------
1 | import FilesRouter from './FilesRouter';
2 | import InvitesRouter from './InvitesRouter';
3 | import DomainsRouter from './DomainsRouter';
4 | import AuthRouter from './AuthRouter';
5 | import UsersRouter from './UsersRouter';
6 | import BaseRouter from './BaseRouter';
7 | import ShortenerRouter from './ShortenerRouter';
8 | import AdminRouter from './AdminRouter';
9 |
10 | export {
11 | FilesRouter,
12 | InvitesRouter,
13 | DomainsRouter,
14 | AuthRouter,
15 | UsersRouter,
16 | BaseRouter,
17 | ShortenerRouter,
18 | AdminRouter,
19 | };
20 |
--------------------------------------------------------------------------------
/src/schemas/BlacklistSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | id: string().required(),
5 |
6 | reason: string().required(),
7 |
8 | executerId: string().required(),
9 | }).options({abortEarly: false});
10 |
--------------------------------------------------------------------------------
/src/schemas/BulkInvSchema.ts:
--------------------------------------------------------------------------------
1 | import {number, object, string} from 'joi';
2 |
3 | export default object({
4 | executerId: string().required(),
5 | count: number().required(),
6 | }).options({abortEarly: false});
7 |
--------------------------------------------------------------------------------
/src/schemas/ChangePasswordSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | newPassword: string().min(5).max(60).required(),
5 |
6 | password: string().required(),
7 | }).options({abortEarly: false});
8 |
--------------------------------------------------------------------------------
/src/schemas/ChangeUsernameSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | username: string().alphanum().min(3).max(30).required(),
5 |
6 | password: string().required(),
7 | }).options({abortEarly: false});
8 |
--------------------------------------------------------------------------------
/src/schemas/ConfigSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | key: string().required(),
5 | }).options({abortEarly: false});
6 |
--------------------------------------------------------------------------------
/src/schemas/CustomDomainSchema.ts:
--------------------------------------------------------------------------------
1 | import {boolean, object, string} from 'joi';
2 |
3 | export default object({
4 | name: string().required(),
5 |
6 | wildcard: boolean().required(),
7 |
8 | userOnly: boolean().required(),
9 | }).options({abortEarly: false});
10 |
--------------------------------------------------------------------------------
/src/schemas/DeletionSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | key: string().required(),
5 | }).options({abortEarly: false});
6 |
--------------------------------------------------------------------------------
/src/schemas/DomainSchema.ts:
--------------------------------------------------------------------------------
1 | import {array, boolean, object, string} from 'joi';
2 |
3 | export default array().items(
4 | object({
5 | name: string().required(),
6 |
7 | wildcard: boolean().required(),
8 |
9 | donated: boolean(),
10 |
11 | donatedBy: string(),
12 |
13 | userOnly: boolean(),
14 | }).options({abortEarly: false})
15 | );
16 |
--------------------------------------------------------------------------------
/src/schemas/EmbedSchema.ts:
--------------------------------------------------------------------------------
1 | import {boolean, object, string} from 'joi';
2 |
3 | export default object({
4 | color: string().optional(),
5 |
6 | title: string().optional().allow('').max(200),
7 |
8 | description: string().optional().allow('').max(2000),
9 |
10 | author: string().optional().allow('').max(200),
11 |
12 | randomColor: boolean().optional(),
13 | }).options({abortEarly: false});
14 |
--------------------------------------------------------------------------------
/src/schemas/FakeUrlSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | url: string().optional().allow('').max(100),
5 | }).options({abortEarly: false});
6 |
--------------------------------------------------------------------------------
/src/schemas/GenInvSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | executerId: string().required(),
5 | }).options({abortEarly: false});
6 |
--------------------------------------------------------------------------------
/src/schemas/InviteAddSchema.ts:
--------------------------------------------------------------------------------
1 | import {number, object, string} from 'joi';
2 |
3 | export default object({
4 | id: string().required(),
5 |
6 | amount: number().required(),
7 | }).options({abortEarly: false});
8 |
--------------------------------------------------------------------------------
/src/schemas/InviteWaveSchema.ts:
--------------------------------------------------------------------------------
1 | import {number, object} from 'joi';
2 |
3 | export default object({
4 | amount: number().required(),
5 | }).options({abortEarly: false});
6 |
--------------------------------------------------------------------------------
/src/schemas/LoginSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | username: string().alphanum().min(3).max(30).required(),
5 |
6 | password: string().min(5).max(60).required(),
7 | }).options({abortEarly: false});
8 |
--------------------------------------------------------------------------------
/src/schemas/PasswordResetConfirmationSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | email: string().email().required(),
5 | }).options({abortEarly: false});
6 |
--------------------------------------------------------------------------------
/src/schemas/PasswordResetSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | key: string().required(),
5 |
6 | password: string().min(5).max(60).required(),
7 |
8 | confirmPassword: string().min(5).max(60).required(),
9 | }).options({abortEarly: false});
10 |
--------------------------------------------------------------------------------
/src/schemas/PreferencesSchema.ts:
--------------------------------------------------------------------------------
1 | import {boolean, object} from 'joi';
2 |
3 | export default object({
4 | longUrl: boolean().optional(),
5 |
6 | showLink: boolean().optional(),
7 |
8 | invisibleUrl: boolean().optional(),
9 |
10 | randomDomain: boolean().optional(),
11 |
12 | embeds: boolean().optional(),
13 |
14 | autoWipe: boolean().optional(),
15 |
16 | fakeUrl: boolean().optional(),
17 | }).options({abortEarly: false});
18 |
--------------------------------------------------------------------------------
/src/schemas/PremiumSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | id: string().required(),
5 | }).options({abortEarly: false});
6 |
--------------------------------------------------------------------------------
/src/schemas/RandomDomainSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | domain: string().required(),
5 | }).options({abortEarly: false});
6 |
--------------------------------------------------------------------------------
/src/schemas/RegisterSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | email: string().email().required(),
5 |
6 | username: string().alphanum().min(3).max(30).required(),
7 |
8 | password: string().min(5).max(60).required(),
9 |
10 | invite: string().required(),
11 | }).options({abortEarly: false});
12 |
--------------------------------------------------------------------------------
/src/schemas/SetUIDSchema.ts:
--------------------------------------------------------------------------------
1 | import {number, object, string} from 'joi';
2 |
3 | export default object({
4 | id: string().required(),
5 |
6 | newuid: number().required(),
7 | }).options({abortEarly: false});
8 |
--------------------------------------------------------------------------------
/src/schemas/ShortenerSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | url: string().required(),
5 | }).options({abortEarly: false});
6 |
--------------------------------------------------------------------------------
/src/schemas/TestimonialSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | testimonial: string().min(3).max(60).required(),
5 | }).options({abortEarly: false});
6 |
--------------------------------------------------------------------------------
/src/schemas/UpdateDomainSchema.ts:
--------------------------------------------------------------------------------
1 | import {any, object, string} from 'joi';
2 |
3 | export default object({
4 | domain: any().required(),
5 |
6 | subdomain: string().allow('').optional(),
7 | }).options({abortEarly: false});
8 |
--------------------------------------------------------------------------------
/src/schemas/VerifyEmailSchema.ts:
--------------------------------------------------------------------------------
1 | import {object, string} from 'joi';
2 |
3 | export default object({
4 | key: string().required(),
5 | }).options({abortEarly: false});
6 |
--------------------------------------------------------------------------------
/src/schemas/WipeIntervalSchema.ts:
--------------------------------------------------------------------------------
1 | import {number, object} from 'joi';
2 |
3 | export default object({
4 | value: number().required(),
5 | }).options({abortEarly: false});
6 |
--------------------------------------------------------------------------------
/src/utils/CloudflareUtil.ts:
--------------------------------------------------------------------------------
1 | import Axios, {Method} from 'axios';
2 |
3 | export default new (class CloudflareUtil {
4 | /**
5 | * Send a request to the cloudflare api.
6 | * @param {string} endpoint The endpoint to send a request to.
7 | * @param {Method} method The request method.
8 | * @param {object} body The request body.
9 | */
10 | async request(endpoint: string, method: Method, body?: object) {
11 | try {
12 | const baseUrl = 'https://api.cloudflare.com/client/v4';
13 | const {data} = await Axios({
14 | url: `${baseUrl}${endpoint}`,
15 | method,
16 | headers: {
17 | 'X-Auth-Key': process.env.CLOUDFLARE_API_KEY,
18 | 'X-Auth-Email': process.env.CLOUDFLARE_EMAIL,
19 | 'Content-Type': 'application/json',
20 | },
21 | data: body ? body : null,
22 | });
23 |
24 | return data;
25 | } catch (err) {
26 | throw new Error(err.response.data.errors[0].message);
27 | }
28 | }
29 |
30 | /**
31 | * Add a domain to the cloudflare account.
32 | * @param {string} domain The domain to add.
33 | * @param {boolean} wildcard Whether or not the domain should be wildcarded.
34 | */
35 | async addDomain(domain: string, wildcard: boolean) {
36 | const data = await this.request('/zones', 'POST', {
37 | name: domain,
38 | account: {
39 | id: process.env.CLOUDFLARE_ACCOUNT_ID,
40 | },
41 | }).catch(e => console.log(e));
42 |
43 | const id = data.result.id;
44 |
45 | await this.setRecords(domain, wildcard, id);
46 | await this.setSettings(id);
47 | }
48 |
49 | /**
50 | * Delete a zone from the cloudflare account.
51 | * @param {string} domain The domain to delete.
52 | */
53 | async deleteZone(domain: string) {
54 | const {result} = await this.request(`/zones?name=${domain}`, 'GET');
55 | const {id} = result[0];
56 |
57 | await this.request(`/zones/${id}`, 'DELETE');
58 | }
59 |
60 | /**
61 | * Setup dns records for a new domain.
62 | * @param {string} domain The domain name.
63 | * @param {boolean} wildcard Whether or not the domain should be wildcarded.
64 | * @param {string} id The zone id.
65 | */
66 | async setRecords(domain: string, wildcard: boolean, id: string) {
67 | await this.request(`/zones/${id}/dns_records`, 'POST', {
68 | type: 'CNAME',
69 | name: '@',
70 | content: 'i.higure.wtf',
71 | ttl: 1,
72 | proxied: true,
73 | });
74 |
75 | if (wildcard)
76 | await this.request(`/zones/${id}/dns_records`, 'POST', {
77 | type: 'CNAME',
78 | name: '*',
79 | content: domain,
80 | ttl: 1,
81 | });
82 | }
83 |
84 | /**
85 | * Setup the ssl settings for a new domain.
86 | * @param {string} id The zone id.
87 | */
88 | async setSettings(id: string) {
89 | await this.request(`/zones/${id}/settings/ssl`, 'PATCH', {
90 | value: 'flexible',
91 | });
92 |
93 | await this.request(`/zones/${id}/settings/always_use_https`, 'PATCH', {
94 | value: 'on',
95 | });
96 | }
97 | })();
98 |
--------------------------------------------------------------------------------
/src/utils/FormatUtil.ts:
--------------------------------------------------------------------------------
1 | import {File} from '../models/FileModel';
2 | import {User} from '../models/UserModel';
3 | import {EmbedInterface} from './interfaces/EmbedInterface';
4 | import {extname} from 'path';
5 |
6 | /**
7 | * Format a file size to a human readable format.
8 | * @param {number} size The filesize in bytes.
9 | * @return {string} The formatted filesize.
10 | */
11 | function formatFilesize(size: number): string {
12 | if (size === 0) return '0 B';
13 |
14 | const sizes = ['B', 'KB', 'MB', 'GB'];
15 | const int = Math.floor(Math.log(size) / Math.log(1024));
16 |
17 | return `${(size / Math.pow(1024, int)).toFixed(2)} ${sizes[int]}`;
18 | }
19 |
20 | /**
21 | * Format a file size to a human readable format.
22 | * @param {string} filename The original filename.
23 | * @return {string} The formatted filesize.
24 | */
25 | function formatExtension(file: Express.Multer.File): string {
26 | let extension = extname(file.originalname);
27 | if (extension === '') {
28 | extension = '.png';
29 | file.mimetype = 'image/png';
30 | }
31 |
32 | return extension;
33 | }
34 |
35 | /**
36 | * Format a embed to fill out the templates.
37 | * @param {EmbedInterface} embed The embed settings.
38 | * @param {User} user The user who set the embed settings.
39 | * @param {File} file The file, if needed.
40 | * @return {EmbedInterface} The formatted embed.
41 | */
42 | function formatEmbed(
43 | embed: EmbedInterface,
44 | user: User,
45 | file: File
46 | ): EmbedInterface {
47 | for (const field of ['title', 'description', 'author']) {
48 | if (embed[field]) {
49 | if (embed[field] === 'default') {
50 | switch (field) {
51 | case 'title':
52 | embed[field] = file.filename;
53 | break;
54 | case 'description':
55 | embed[field] = `Uploaded at ${new Date(
56 | file.timestamp
57 | ).toLocaleString()} by ${user.username}`;
58 | break;
59 | case 'author':
60 | embed[field] = user.username;
61 | }
62 | } else {
63 | embed[field] = embed[field]
64 | .replace('{size}', file.size)
65 | .replace('{username}', user.username)
66 | .replace('{filename}', file.filename)
67 | .replace('{uploads}', user.uploads)
68 | .replace('{date}', file.timestamp.toLocaleDateString())
69 | .replace('{time}', file.timestamp.toLocaleTimeString())
70 | .replace('{timestamp}', file.timestamp.toLocaleString())
71 | .replace(
72 | '{fakeurl}',
73 | formatFakeUrl(user.settings.fakeUrl.url, user, file)
74 | );
75 |
76 | const TIMEZONE_REGEX = /{(time|timestamp):([^}]+)}/i;
77 | let match = embed[field].match(TIMEZONE_REGEX);
78 |
79 | while (match) {
80 | try {
81 | const formatted =
82 | match[1] === 'time'
83 | ? file.timestamp.toLocaleTimeString('en-US', {
84 | timeZone: match[2],
85 | })
86 | : file.timestamp.toLocaleString('en-US', {
87 | timeZone: match[2],
88 | });
89 |
90 | embed[field] = embed[field].replace(match[0], formatted);
91 | match = embed[field].match(TIMEZONE_REGEX);
92 | } catch (err) {
93 | break;
94 | }
95 | }
96 | }
97 | }
98 | }
99 |
100 | if (embed.randomColor)
101 | embed.color = `#${(((1 << 24) * Math.random()) | 0).toString(16)}`;
102 |
103 | return embed;
104 | }
105 |
106 | function formatFakeUrl(url: string, user: User, file: File) {
107 | return url
108 | .replace('{size}', file.size)
109 | .replace('{username}', user.username)
110 | .replace('{filename}', file.filename)
111 | .replace('{uploads}', user.uploads.toString())
112 | .replace('{date}', file.timestamp.toLocaleDateString())
113 | .replace('{time}', file.timestamp.toLocaleTimeString())
114 | .replace('{timestamp}', file.timestamp.toLocaleString())
115 | .replace('{fakeurl}', user.settings.fakeUrl.url);
116 | }
117 |
118 | export {formatFilesize, formatEmbed, formatExtension, formatFakeUrl};
119 |
--------------------------------------------------------------------------------
/src/utils/GenerateUtil.ts:
--------------------------------------------------------------------------------
1 | import {randomBytes} from 'crypto';
2 |
3 | /**
4 | * Generate a string.
5 | * @param {string} length The length of the string.
6 | * @return {string} The generated string.
7 | */
8 | function generateString(length: number): string {
9 | return randomBytes(length).toString('hex').slice(0, length);
10 | }
11 |
12 | /**
13 | * Generate a invite code.
14 | * @return {string} The invite code.
15 | */
16 | function generateInvite(): string {
17 | const key = randomBytes(80).toString('hex');
18 | return [key.slice(0, 10), key.slice(1, 12), key.slice(3, 9)].join('-');
19 | }
20 |
21 | /**
22 | * Generate a short url.
23 | * @return {string} The short url.
24 | */
25 | function generateInvisibleId(): string {
26 | let url = '';
27 | const invisibleCharacters = ['\u200B', '\u2060', '\u200C', '\u200D'].join('');
28 |
29 | for (let i = 0; i < 25; i++) {
30 | url += invisibleCharacters.charAt(
31 | Math.floor(Math.random() * invisibleCharacters.length)
32 | );
33 | }
34 |
35 | return url + '\u200B';
36 | }
37 |
38 | export {generateString, generateInvite, generateInvisibleId};
39 |
--------------------------------------------------------------------------------
/src/utils/IPLoggers.json:
--------------------------------------------------------------------------------
1 | [
2 | "viral.over-blog.com",
3 | "gyazo.in",
4 | "ps3cfw.com",
5 | "urlz.fr",
6 | "webpanel.space",
7 | "steamcommumity.com",
8 | "imgur.com.de",
9 | "fuglekos.com",
10 | "grabify.link",
11 | "leancoding.co",
12 | "stopify.co",
13 | "freegiftcards.co",
14 | "joinmy.site",
15 | "curiouscat.club",
16 | "catsnthings.fun",
17 | "catsnthings.com",
18 | "xn--yutube-iqc.com",
19 | "gyazo.nl",
20 | "yip.su",
21 | "iplogger.com",
22 | "iplogger.org",
23 | "iplogger.ru",
24 | "2no.co",
25 | "02ip.ru",
26 | "iplis.ru",
27 | "iplo.ru",
28 | "ezstat.ru",
29 | "whatstheirip.com",
30 | "hondachat.com",
31 | "bvog.com",
32 | "youramonkey.com",
33 | "pronosparadise.com",
34 | "freebooter.pro",
35 | "blasze.com",
36 | "blasze.tk",
37 | "ipgrab.org",
38 | "gyazos.com",
39 | "discord.kim"
40 | ]
--------------------------------------------------------------------------------
/src/utils/Intervals.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The intervals array.
3 | */
4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
5 | let intervals: Array<{id?: any; uuid: string}> = [];
6 |
7 | /**
8 | * Delete a interval.
9 | * @param {string} uuid The user's uuid.
10 | */
11 | function delInterval(uuid: string) {
12 | intervals = intervals.filter(i => i.uuid !== uuid);
13 | }
14 |
15 | export {intervals, delInterval};
16 |
--------------------------------------------------------------------------------
/src/utils/LoggingUtil.ts:
--------------------------------------------------------------------------------
1 | import Axios from 'axios';
2 | import {Domain} from '../models/DomainModel';
3 | import {User} from '../models/UserModel';
4 |
5 | /**
6 | * Log a list of new domains to the domain notifications channel.
7 | * @param {Domain[]} domains The domain that was created.
8 | * @param {User} donatedby The donator
9 | */
10 | async function logDomains(domains: Domain[], donatedby: User) {
11 | const grammar =
12 | domains.length > 1
13 | ? `**${domains.length}** new domains have`
14 | : 'A new domain has';
15 | const domainList = domains
16 | .map(d => (d.wildcard ? '*.' : '') + d.name)
17 | .join(',\n');
18 |
19 | await Axios.post(process.env.WEBHOOK_URL, {
20 | embeds: [
21 | {
22 | description: `${grammar} been added, DNS records have automatically been updated.`,
23 | fields: [
24 | {
25 | name: 'Domains',
26 | value: `\`\`\`${domainList}\`\`\``,
27 | },
28 | {
29 | name: 'Donated By:',
30 | value: !donatedby
31 | ? `Official Domain${domains.length > 1 ? 's' : ''}`
32 | : `${donatedby.username} (<@${donatedby.discord.id}>)`,
33 | },
34 | ],
35 | },
36 | ],
37 | });
38 | }
39 |
40 | /**
41 | * Log a single custom domain to the webhook in the server.
42 | * @param {Domain} domain The domain.
43 | */
44 | async function logCustomDomain(domain: Domain) {
45 | await Axios.post(process.env.CUSTOM_DOMAIN_WEBHOOK, {
46 | embeds: [
47 | {
48 | title: 'A new domain has been added',
49 | fields: [
50 | {
51 | name: 'Name',
52 | value: `[${domain.name}](https://${domain.name})`,
53 | },
54 | {
55 | name: 'Wildcard',
56 | value: domain.wildcard ? 'Yes' : 'No',
57 | },
58 | {
59 | name: 'Donator',
60 | value: domain.donatedBy,
61 | },
62 | {
63 | name: 'Private',
64 | value: domain.userOnly ? 'Yes' : 'No',
65 | },
66 | ],
67 | },
68 | ],
69 | });
70 | }
71 |
72 | /**
73 | * Log a possible alt to the discord.
74 | * @param {User[]} relatedAlts The users.
75 | * @param alt
76 | * @param register
77 | */
78 | async function logPossibleAlts(
79 | relatedAlts: User[],
80 | alt: User,
81 | register: boolean
82 | ) {
83 | const altsList = relatedAlts
84 | .map(d => d.username + (d.blacklisted.status ? ' (Blacklisted)' : ''))
85 | .join(', ');
86 | await Axios.post(process.env.CUSTOM_DOMAIN_WEBHOOK, {
87 | embeds: [
88 | {
89 | title: `A new possible account has ${
90 | register ? 'registered' : 'logged in'
91 | }`,
92 | fields: [
93 | {
94 | name: 'Username:',
95 | value: alt.username,
96 | inline: true,
97 | },
98 | {
99 | name: 'UID:',
100 | value: `${alt.uid}`,
101 | inline: true,
102 | },
103 | {
104 | name: 'Uploads:',
105 | value: alt.uploads,
106 | inline: true,
107 | },
108 | {
109 | name: 'Discord:',
110 | value: alt.discord.id ? `<@${alt.discord.id}>` : 'Not linked',
111 | inline: true,
112 | },
113 | {
114 | name: 'Relative accounts:',
115 | value: `\`\`\`${altsList}\`\`\``,
116 | },
117 | {
118 | name: 'UUID:',
119 | value: `\`\`\`${alt._id}\`\`\``,
120 | },
121 | ],
122 | thumbnail: {
123 | url: alt.discord.avatar,
124 | },
125 | },
126 | ],
127 | });
128 | }
129 |
130 | export {logDomains, logCustomDomain, logPossibleAlts};
131 |
--------------------------------------------------------------------------------
/src/utils/MulterUtil.ts:
--------------------------------------------------------------------------------
1 | import {Request} from 'express';
2 | import {formatExtension} from './FormatUtil';
3 | import {generateString} from './GenerateUtil';
4 | import {s3} from './S3Util';
5 | import multer, {Multer} from 'multer';
6 | import MulterS3 from 'multer-s3';
7 | import DomainModel from '../models/DomainModel';
8 |
9 | /**
10 | * The Multer configuration.
11 | */
12 | const upload: Multer = multer({
13 | storage: MulterS3({
14 | s3,
15 | contentType: MulterS3.AUTO_CONTENT_TYPE,
16 | acl: 'public-read',
17 | bucket: process.env.S3_BUCKET,
18 | key: async (req: Request, file: Express.Multer.File, cb) => {
19 | if (!req.user) return;
20 |
21 | const key = req.user._id;
22 | const {longUrl, domain} = req.user.settings;
23 | const document = await DomainModel.findOne({name: domain.name});
24 | const filename =
25 | (longUrl ? generateString(17) : generateString(7)) +
26 | formatExtension(file);
27 |
28 | if (document.userOnly) {
29 | file.userOnlyDomain = true;
30 | }
31 |
32 | file.filename = filename;
33 | file.key = `${key}/${filename}`;
34 |
35 | cb(null, `${key}/${filename}`);
36 | },
37 | }),
38 | limits: {
39 | fileSize: 104857600,
40 | },
41 | });
42 |
43 | export {upload};
44 |
--------------------------------------------------------------------------------
/src/utils/OAuthUtil.ts:
--------------------------------------------------------------------------------
1 | import Axios, {Method} from 'axios';
2 | import {AuthorizationInterface} from './interfaces/AuthorizationInterface';
3 | import {DiscordUserInterface} from './interfaces/DiscordUserInterface';
4 | import {stringify} from 'querystring';
5 | import {User} from '../models/UserModel';
6 |
7 | export class OAuth {
8 | /**
9 | * The user's access token & refresh token.
10 | */
11 | authorization: AuthorizationInterface;
12 |
13 | /**
14 | * The user's basic information.
15 | */
16 | user: DiscordUserInterface;
17 |
18 | /**
19 | * The OAuth2 grant code.
20 | */
21 | code: string;
22 |
23 | constructor(code: string) {
24 | this.code = code;
25 | }
26 |
27 | /**
28 | * Send a request to the discord api.
29 | * @param {string} endpoint The endpoint to send a request to.
30 | * @param {Method} method The request method.
31 | * @param {object} body The request body.
32 | * @param {object} headers The request headers.
33 | */
34 | request = async (
35 | endpoint: string,
36 | method: Method,
37 | body?: object | string,
38 | headers?: object
39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
40 | ): Promise => {
41 | try {
42 | const baseUrl = 'https://discord.com/api';
43 | const {data} = await Axios({
44 | url: `${baseUrl}${endpoint}`,
45 | method,
46 | headers: headers ? headers : null,
47 | data: body ? body : null,
48 | });
49 |
50 | return data;
51 | } catch (err) {
52 | throw new Error(
53 | err.response.data.error_description || err.response.data.message
54 | );
55 | }
56 | };
57 |
58 | /**
59 | * Verify that an OAuth grant code is valid.
60 | * @param {string} request The request type, defaults to login.
61 | */
62 | validate = async (request = 'login'): Promise => {
63 | this.authorization = await this.request(
64 | '/oauth2/token',
65 | 'POST',
66 | stringify({
67 | client_id: process.env.DISCORD_CLIENT_ID,
68 | client_secret: process.env.DISCORD_CLIENT_SECRET,
69 | redirect_uri:
70 | request !== 'login'
71 | ? process.env.DISCORD_LINK_REDIRECT_URI
72 | : process.env.DISCORD_LOGIN_REDIRECT_URI,
73 | grant_type: 'authorization_code',
74 | code: this.code,
75 | }),
76 | {
77 | 'Content-Type': 'application/x-www-form-urlencoded',
78 | }
79 | );
80 | };
81 |
82 | /**
83 | * Get a user's basic information.
84 | * @return {DiscordUserInterface} The user's info.
85 | */
86 | getUser = async (): Promise => {
87 | this.user = await this.request('/users/@me', 'GET', null, {
88 | Authorization: `Bearer ${this.authorization.access_token}`,
89 | });
90 |
91 | return this.user;
92 | };
93 | /**
94 | * Add a user to the discord server.
95 | * @param {User} user The user to add.
96 | */
97 | addGuildMember = async (user: User): Promise => {
98 | const whitelistedRole = process.env.DISCORD_ROLES;
99 |
100 | let data: any = JSON.stringify({
101 | access_token: this.authorization.access_token,
102 | nick: user.username,
103 | });
104 | try {
105 | await this.request(
106 | `/guilds/${process.env.DISCORD_SERVER_ID}/members/${this.user.id}`,
107 | 'PUT',
108 | data,
109 | {
110 | Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
111 | 'Content-Type': 'application/json',
112 | 'Content-Length': data.length,
113 | }
114 | );
115 |
116 | data = JSON.stringify({
117 | access_token: this.authorization.access_token,
118 | });
119 |
120 | await this.request(
121 | `/guilds/${process.env.DISCORD_SERVER_ID}/members/${this.user.id}/roles/${whitelistedRole}`,
122 | 'PUT',
123 | data,
124 | {
125 | Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
126 | }
127 | );
128 | } catch (e) {
129 | console.log(e.stack);
130 | }
131 |
132 | if (user.discord.id && user.discord.id !== this.user.id) {
133 | try {
134 | data = await this.request(
135 | `/guilds/${process.env.DISCORD_SERVER_ID}/members/${user.discord.id}`,
136 | 'GET',
137 | null,
138 | {
139 | Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
140 | 'Content-Type': 'application/json',
141 | }
142 | );
143 |
144 | data = JSON.stringify({
145 | nick: user.username,
146 | });
147 | await this.request(
148 | `/guilds/${process.env.DISCORD_SERVER_ID}/members/${this.user.id}`,
149 | 'PATCH',
150 | data,
151 | {
152 | Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
153 | }
154 | );
155 |
156 | if (data.roles.includes(whitelistedRole))
157 | await this.request(
158 | `/guilds/${process.env.DISCORD_SERVER_ID}/members/${user.discord.id}/roles/${whitelistedRole}`,
159 | 'DELETE',
160 | null,
161 | {
162 | Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
163 | 'Content-Type': 'application/json',
164 | }
165 | );
166 | if (user.premium) {
167 | await this.request(
168 | `/guilds/${process.env.DISCORD_SERVER_ID}/members/${user.discord.id}/roles/821327907250241536`,
169 | 'PUT',
170 | null,
171 | {
172 | Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
173 | }
174 | );
175 | }
176 | } catch (err) {
177 | console.error(err);
178 | }
179 | }
180 | };
181 | }
182 | async function addPremium(user: User) {
183 | await this.request(
184 | `/guilds/${process.env.DISCORD_SERVER_ID}/members/${user.discord.id}/roles/821327907250241536`,
185 | 'PUT',
186 | null,
187 | {
188 | Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
189 | }
190 | );
191 | }
192 | export {addPremium};
193 |
--------------------------------------------------------------------------------
/src/utils/S3Util.ts:
--------------------------------------------------------------------------------
1 | import {Endpoint, S3} from 'aws-sdk';
2 | import DomainModel from '../models/DomainModel';
3 | import {User} from '../models/UserModel';
4 | import CounterModel from '../models/CounterModel';
5 | import Axios, {Method} from 'axios';
6 |
7 | /**
8 | * The aws-S3 session.
9 | */
10 | const s3 = new S3({
11 | accessKeyId: process.env.S3_ACCESS_KEY_ID,
12 | secretAccessKey: process.env.S3_SECRET_KEY,
13 | endpoint: process.env.S3_ENDPOINT,
14 | s3ForcePathStyle: true, // needed with minio?
15 | signatureVersion: 'v4',
16 | });
17 | // const s3 = new S3({
18 | // credentials: {
19 | // secretAccessKey: process.env.S3_SECRET_KEY,
20 | // accessKeyId: process.env.S3_ACCESS_KEY_ID,
21 | // },
22 | // endpoint: process.env.S3_ENDPOINT,
23 | // });
24 |
25 | // the function below is terrible, disgusting, and long, I know, I couldn't really think of any either way to do it and I wanted to release quickly, sorry!
26 | async function updateStorage() {
27 | try {
28 | const params = {
29 | Bucket: process.env.S3_BUCKET,
30 | };
31 | let storageUsed = 0;
32 | const objects = await s3.listObjectsV2(params).promise();
33 | for (const object of objects.Contents) {
34 | storageUsed += object.Size;
35 | }
36 | await CounterModel.findByIdAndUpdate('counter', {
37 | storageUsed: storageUsed,
38 | });
39 | setTimeout(async () => {
40 | await this.updateStorage();
41 | }, 300000);
42 | } catch (err) {
43 | new Error(err);
44 | }
45 | }
46 |
47 | /**
48 | * Wipe a user's files.
49 | * @param {user} user The user's files to wipe.
50 | * @param {string} dir The directory to delete.
51 | */
52 | async function wipeFiles(user: User, dir = `${user._id}/`) {
53 | const domains = await DomainModel.find({userOnly: true, donatedBy: user._id});
54 | let count = 0;
55 |
56 | // eslint-disable-next-line no-constant-condition
57 | while (true) {
58 | const params = {
59 | Bucket: process.env.S3_BUCKET,
60 | Prefix: dir,
61 | };
62 |
63 | if (domains.length !== 0)
64 | for (const domain of domains) {
65 | if (domain.userOnly) {
66 | params.Prefix = `${domain.name}/`;
67 |
68 | const domainObjects = await s3.listObjectsV2(params).promise();
69 |
70 | if (domainObjects.Contents.length !== 0) {
71 | const deleteParams = {
72 | Bucket: process.env.S3_BUCKET,
73 | Delete: {
74 | Objects: [],
75 | },
76 | };
77 |
78 | for (const {Key} of domainObjects.Contents) {
79 | deleteParams.Delete.Objects.push({Key});
80 | }
81 |
82 | const deleted = await s3.deleteObjects(deleteParams).promise();
83 | count += (deleted.Deleted as AWS.S3.DeletedObjects).length;
84 | }
85 | }
86 | }
87 |
88 | params.Prefix = `${user._id}/`;
89 |
90 | const objects = await s3.listObjectsV2(params).promise();
91 |
92 | if (objects.Contents.length !== 0) {
93 | const deleteParams = {
94 | Bucket: process.env.S3_BUCKET,
95 | Delete: {
96 | Objects: [],
97 | },
98 | };
99 |
100 | for (const {Key} of objects.Contents) {
101 | deleteParams.Delete.Objects.push({Key});
102 | }
103 |
104 | const deleted = await s3.deleteObjects(deleteParams).promise();
105 | count += (deleted.Deleted as AWS.S3.DeletedObjects).length;
106 | }
107 |
108 | if (!objects.IsTruncated) return count;
109 | }
110 | }
111 |
112 | export {s3, wipeFiles, updateStorage};
113 |
--------------------------------------------------------------------------------
/src/utils/SafetyUtils.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable eqeqeq */
2 | import Axios from 'axios';
3 | import proxy_check from 'proxycheck-node.js';
4 | import ipLoggers from './IPLoggers.json';
5 |
6 | const lookup = require('safe-browse-url-lookup')({
7 | apiKey: 'AIzaSyAv7Ir63bdIx03t5WVJzZlnhYUtKBrDFUQ',
8 | });
9 | const check = new proxy_check({api_key: '71x722-t44jz9-7043n4-13nb77'});
10 |
11 | //this is hella ugly IK
12 | async function isVPN(ip: string) {
13 | const ipintel = await Axios.get(
14 | `http://check.getipintel.net/check.php?ip=${ip}&contact=hello@higure.wtf`,
15 | {transformResponse: []}
16 | );
17 | if (Number(ipintel.data) > 0.99) {
18 | return true;
19 | }
20 | const result = (await check.check(ip, {vpn: true, risk: 1}))[ip];
21 | if (result.risk < 33 && result.proxy === 'yes' && result.type != 'VPN') {
22 | return true;
23 | } else if (result.risk < 66 && result.risk > 34 && result.proxy != 'no') {
24 | return true;
25 | } else if (result.risk > 67) {
26 | return true;
27 | }
28 | return false;
29 | }
30 |
31 | async function isMalicious(url: string) {
32 | try {
33 | // eslint-disable-next-line node/no-unsupported-features/node-builtins
34 | const domain = new URL(url).hostname.replace('www.', '');
35 | for (const ipLogger of ipLoggers) {
36 | if (domain.match(new RegExp(ipLogger, 'i'))) return true;
37 | }
38 | return await lookup.checkSingle('http://' + domain + '/');
39 | } catch (e) {
40 | return false;
41 | }
42 | }
43 |
44 | export {isVPN, isMalicious};
45 |
--------------------------------------------------------------------------------
/src/utils/interfaces/AuthorizationInterface.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 |
3 | export interface AuthorizationInterface {
4 | access_token: string;
5 | expires_in: number;
6 | refresh_token: string;
7 | scope: string;
8 | token_type: string;
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/interfaces/DiscordUserInterface.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 |
3 | export interface DiscordUserInterface {
4 | id: string;
5 | username: string;
6 | avatar: string;
7 | discriminator: number;
8 | public_flags: number;
9 | flags: number;
10 | locale: string;
11 | mfa_enabled: boolean;
12 | premium_type: number;
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/interfaces/EmbedInterface.ts:
--------------------------------------------------------------------------------
1 | export interface EmbedInterface {
2 | enabled: boolean;
3 | color: string;
4 | title: string;
5 | description: string;
6 | author: string;
7 | randomColor: boolean;
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "typeRoots": [
4 | "./typings",
5 | "./node_modules/@types"
6 | ],
7 | "module": "CommonJS",
8 | "target": "es2018",
9 | "outDir": "build",
10 | "lib": [
11 | "ESNext",
12 | "ESNext.Array",
13 | "ESNext.AsyncIterable",
14 | "ESNext.Intl",
15 | "ESNext.Symbol",
16 | "DOM"
17 | ],
18 | "sourceMap": false,
19 | "inlineSourceMap": true,
20 | "inlineSources": true,
21 | "incremental": true,
22 | "experimentalDecorators": true,
23 | "emitDecoratorMetadata": true,
24 | "esModuleInterop": true,
25 | "resolveJsonModule": true
26 | },
27 | "include": [
28 | "src/**/*", "./typings"
29 | ],
30 | "exclude": [
31 | "node_modules"
32 | ],
33 | }
--------------------------------------------------------------------------------
/typings/index.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import {User} from '../src/models/UserModel';
3 | import {OAuth} from '../src/utils/OAuthUtil';
4 |
5 | declare global {
6 | namespace Express {
7 | export interface Request {
8 | user: User;
9 | discord: OAuth;
10 | }
11 | }
12 |
13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
14 | namespace Express.Multer {
15 | export interface File {
16 | key: string;
17 | userOnlyDomain: boolean;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------