commit fa4483c1dbd59bda4670e475e5ac3bc467577120 Author: sneedmaster Date: Sun Sep 28 12:28:02 2025 +0200 Probably a good idea to have a git repo actually diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..464bfca Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..fcbb8ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +dist +node_modules +public/css +public/js +public/uploads +public/banners +config/site.json +config/assets diff --git a/.prettierrc b/.prettierrc new file mode 100755 index 0000000..438397a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "plugins": [ + "prettier-plugin-css-order", + "@trivago/prettier-plugin-sort-imports" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100755 index 0000000..8f39472 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "editor.formatOnSave": true +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100755 index 0000000..0ad25db --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100755 index 0000000..a199144 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# czchan + +Český anonymní imageboard. diff --git a/config/board_defaults.json b/config/board_defaults.json new file mode 100644 index 0000000..d7e5e8b --- /dev/null +++ b/config/board_defaults.json @@ -0,0 +1 @@ +{ "private": false, "unlisted": false } diff --git a/config/locale/countries.json b/config/locale/countries.json new file mode 100755 index 0000000..cfe891f --- /dev/null +++ b/config/locale/countries.json @@ -0,0 +1,251 @@ +{ + "af": "Afghánistán", + "ax": "Alandy", + "al": "Albánie", + "dz": "Alžírsko", + "as": "Americká Samoa", + "vi": "Americké Panenské ostrovy", + "ad": "Andorra", + "ao": "Angola", + "ai": "Anguilla", + "aq": "Antarktida", + "ag": "Antigua a Barbuda", + "ar": "Argentina", + "am": "Arménie", + "aw": "Aruba", + "au": "Austrálie", + "az": "Ázerbájdžán", + "bs": "Bahamy", + "bh": "Bahrajn", + "bd": "Bangladéš", + "bb": "Barbados", + "be": "Belgie", + "bz": "Belize", + "by": "Bělorusko", + "bj": "Benin", + "bm": "Bermudy", + "bt": "Bhútán", + "bo": "Bolívie", + "bq": "Bonaire, Svatý Eustach a Saba", + "ba": "Bosna a Hercegovina", + "bw": "Botswana", + "bv": "Bouvetův ostrov", + "br": "Brazílie", + "io": "Britské indickooceánské území", + "vg": "Britské Panenské ostrovy", + "bn": "Brunej", + "bg": "Bulharsko", + "bf": "Burkina Faso", + "bi": "Burundi", + "ck": "Cookovy ostrovy", + "cw": "Curaçao", + "td": "Čad", + "me": "Černá Hora", + "cz": "Česko", + "cn": "Čína", + "dk": "Dánsko", + "dm": "Dominika", + "do": "Dominikánská republika", + "dj": "Džibutsko", + "eg": "Egypt", + "ec": "Ekvádor", + "er": "Eritrea", + "ee": "Estonsko", + "et": "Etiopie", + "fo": "Faerské ostrovy", + "fk": "Falklandy (Malvíny)", + "fj": "Fidži", + "ph": "Filipíny", + "fi": "Finsko", + "fr": "Francie", + "gf": "Francouzská Guyana", + "tf": "Francouzská jižní a antarktická území", + "pf": "Francouzská Polynésie", + "ga": "Gabon", + "gm": "Gambie", + "gh": "Ghana", + "gi": "Gibraltar", + "gd": "Grenada", + "gl": "Grónsko", + "ge": "Gruzie", + "gp": "Guadeloupe", + "gu": "Guam", + "gt": "Guatemala", + "gn": "Guinea", + "gw": "Guinea-Bissau", + "gg": "Guernsey", + "gy": "Guyana", + "ht": "Haiti", + "hm": "Heardův ostrov a McDonaldovy ostrovy", + "hn": "Honduras", + "hk": "Hongkong", + "cl": "Chile", + "hr": "Chorvatsko", + "in": "Indie", + "id": "Indonésie", + "iq": "Irák", + "ir": "Írán", + "ie": "Irsko", + "is": "Island", + "it": "Itálie", + "il": "Izrael", + "jm": "Jamajka", + "jp": "Japonsko", + "ye": "Jemen", + "je": "Jersey", + "za": "Jihoafrická republika", + "gs": "Jižní Georgie a Jižní Sandwichovy ostrovy", + "kr": "Jižní Korea", + "ss": "Jižní Súdán", + "jo": "Jordánsko", + "ky": "Kajmanské ostrovy", + "kh": "Kambodža", + "cm": "Kamerun", + "ca": "Kanada", + "cv": "Kapverdy", + "qa": "Katar", + "kz": "Kazachstán", + "ke": "Keňa", + "ki": "Kiribati", + "cc": "Kokosové ostrovy", + "co": "Kolumbie", + "km": "Komory", + "cg": "Kongo", + "cd": "Konžská demokratická republika", + "cr": "Kostarika", + "cu": "Kuba", + "kw": "Kuvajt", + "cy": "Kypr", + "kg": "Kyrgyzstán", + "la": "Laos", + "ls": "Lesotho", + "lb": "Libanon", + "lr": "Libérie", + "ly": "Libye", + "li": "Lichtenštejnsko", + "lt": "Litva", + "lv": "Lotyšsko", + "lu": "Lucembursko", + "mo": "Macao", + "mg": "Madagaskar", + "hu": "Maďarsko", + "my": "Malajsie", + "mw": "Malawi", + "mv": "Maledivy", + "ml": "Mali", + "mt": "Malta", + "im": "Ostrov Man", + "ma": "Maroko", + "mh": "Marshallovy ostrovy", + "mq": "Martinik", + "mu": "Mauricius", + "mr": "Mauritánie", + "yt": "Mayotte", + "um": "Menší odlehlé ostrovy USA", + "mx": "Mexiko", + "fm": "Mikronésie", + "md": "Moldavsko", + "mc": "Monako", + "mn": "Mongolsko", + "ms": "Montserrat", + "mz": "Mosambik", + "mm": "Myanmar", + "na": "Namibie", + "nr": "Nauru", + "de": "Německo", + "np": "Nepál", + "ne": "Niger", + "ng": "Nigérie", + "ni": "Nikaragua", + "nu": "Niue", + "nl": "Nizozemsko", + "nf": "Norfolk", + "no": "Norsko", + "nc": "Nová Kaledonie", + "nz": "Nový Zéland", + "om": "Omán", + "pk": "Pákistán", + "pw": "Palau", + "ps": "Palestina", + "pa": "Panama", + "pg": "Papua Nová Guinea", + "py": "Paraguay", + "pe": "Peru", + "pn": "Pitcairnovy ostrovy", + "ci": "Pobřeží slonoviny", + "pl": "Polsko", + "pr": "Portoriko", + "pt": "Portugalsko", + "at": "Rakousko", + "re": "Réunion", + "gq": "Rovníková Guinea", + "ro": "Rumunsko", + "ru": "Rusko", + "rw": "Rwanda", + "gr": "Řecko", + "pm": "Saint Pierre a Miquelon", + "sv": "Salvador", + "ws": "Samoa", + "sm": "San Marino", + "sa": "Saúdská Arábie", + "sn": "Senegal", + "kp": "Severní Korea", + "mk": "Severní Makedonie", + "mp": "Severní Mariany", + "sc": "Seychely", + "sl": "Sierra Leone", + "sg": "Singapur", + "sk": "Slovensko", + "si": "Slovinsko", + "so": "Somálsko", + "ae": "Spojené arabské emiráty", + "gb": "Spojené království Velké Británie a Severního Irska", + "us": "Spojené státy americké", + "rs": "Srbsko", + "lk": "Srí Lanka", + "cf": "Středoafrická republika", + "sd": "Súdán", + "sr": "Surinam", + "sh": "Svatá Helena, Ascension a Tristan da Cunha", + "lc": "Svatá Lucie", + "bl": "Svatý Bartoloměj", + "kn": "Svatý Kryštof a Nevis", + "mf": "Svatý Martin (francouzská část)", + "sx": "Svatý Martin (nizozemská část)", + "st": "Svatý Tomáš a Princův ostrov", + "vc": "Svatý Vincenc a Grenadiny", + "sz": "Svazijsko", + "sy": "Sýrie", + "sb": "Šalomounovy ostrovy", + "es": "Španělsko", + "sj": "Špicberky a Jan Mayen", + "se": "Švédsko", + "ch": "Švýcarsko", + "tj": "Tádžikistán", + "tz": "Tanzanie", + "th": "Thajsko", + "tw": "Tchaj-wan", + "tg": "Togo", + "tk": "Tokelau", + "to": "Tonga", + "tt": "Trinidad a Tobago", + "tn": "Tunisko", + "tr": "Turecko", + "tm": "Turkmenistán", + "tc": "Turks a Caicos", + "tv": "Tuvalu", + "ug": "Uganda", + "ua": "Ukrajina", + "uy": "Uruguay", + "uz": "Uzbekistán", + "cx": "Vánoční ostrov", + "vu": "Vanuatu", + "va": "Vatikán", + "ve": "Venezuela", + "vn": "Vietnam", + "tl": "Východní Timor", + "wf": "Wallis a Futuna", + "zm": "Zambie", + "eh": "Západní Sahara", + "zw": "Zimbabwe" +} diff --git a/config/locale/meme_flags.json b/config/locale/meme_flags.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/config/locale/meme_flags.json @@ -0,0 +1 @@ +{} diff --git a/config/locale/permissions.json b/config/locale/permissions.json new file mode 100644 index 0000000..29ebe3e --- /dev/null +++ b/config/locale/permissions.json @@ -0,0 +1,33 @@ +{ + "MANAGE_USERS": "Spravovat uživatele", + "MANAGE_BANNERS": "Spravovat bannery", + "ALL_BANNERS": "Spravovat bannery (+)", + "MANAGE_BANS": "Spravovat bany", + "ALL_BANS": "Spravovat bany (+)", + "MANAGE_BOARDS": "Spravovat nástěnky", + "ALL_BOARDS": "Spravovat nástěnky (+)", + "MANAGE_NEWS": "Spravovat novinky", + "ALL_NEWS": "Spravovat novinky (+)", + "MANAGE_REPORTS": "Spravovat hlášení", + "MANAGE_POSTS": "Spravovat příspěvky", + "DELETE_POSTS": "Mazat příspěvky", + "DELETE_FILES": "Mazat soubory", + "SPOILER_POSTS": "Spoilerovat příspěvky", + "MOVE_POSTS": "Přesouvat příspěvky", + "EDIT_POSTS": "Upravovat příspěvky", + "VIEW_IPS": "Zobrazovat IP adresy", + "VIEW_LOGS": "Zobrazovat záznamy", + "VIEW_METADATA": "Zobrazovat metadata", + "VIEW_PRIVATE_BOARDS": "Zobrazovat soukromé nástěnky", + "CAPCODE": "Capcode", + "CUSTOM_CAPCODE": "Vlastní capcode", + "BYPASS_BAN": "Obcházet bany", + "BYPASS_LOCK": "Obcházet zámky", + "BYPASS_BLOCK": "Obcházet blokace", + "BYPASS_FILTER": "Obcházet filtry", + "BYPASS_RATELIMIT": "Obcházet limity", + "ALL_FIELDS": "Používat všechna pole", + "ADVANCED_MARKUP": "Používat rozšířený markup", + "MANAGE_SITE_ASSETS": "Spravovat assety", + "MANAGE_SITE_CONFIG": "Spravovat nastavení stránky" +} diff --git a/config/roles.json b/config/roles.json new file mode 100755 index 0000000..1e21acb --- /dev/null +++ b/config/roles.json @@ -0,0 +1,93 @@ +{ + "janny": { + "rank": 3, + "capcode": "Uklízeč:ffd700:janny.png", + "permissions": [ + "MANAGE_REPORTS", + "BAN_USERS", + "DELETE_POSTS", + "DELETE_FILES", + "USE_CAPCODE", + "PRIVATE_BOARDS" + ] + }, + "mod": { + "rank": 2, + "capcode": "Moderátor:800080:mod.png", + "permissions": [ + "MANAGE_POSTS", + "MANAGE_REPORTS", + "BAN_USERS", + "UNBAN_USERS", + "DELETE_POSTS", + "DELETE_FILES", + "MOVE_POSTS", + "VIEW_IPS", + "USE_CAPCODE", + "PRIVATE_BOARDS" + ] + }, + "manager": { + "rank": 1, + "capcode": "Manažer:4169e1:manager.png", + "permissions": [ + "MANAGE_USERS", + "MANAGE_BOARDS", + "MANAGE_BANNERS", + "MANAGE_POSTS", + "MANAGE_REPORTS", + "MANAGE_NEWS", + "BAN_USERS", + "UNBAN_USERS", + "DELETE_POSTS", + "DELETE_FILES", + "MOVE_POSTS", + "EDIT_POSTS", + "VIEW_IPS", + "VIEW_METADATA", + "VIEW_LOGS", + "USE_CAPCODE", + "CUSTOM_CAPCODE", + "BYPASS_BAN", + "BYPASS_LOCK", + "BYPASS_BLOCK", + "BYPASS_FILTER", + "BYPASS_RATELIMIT", + "ALL_FIELDS", + "PRIVATE_BOARDS", + "ADVANCED_MARKUP" + ] + }, + "admin": { + "rank": 0, + "capcode": "Admin:008000:admin.png", + "permissions": [ + "MANAGE_USERS", + "MANAGE_BOARDS", + "MANAGE_BANNERS", + "MANAGE_POSTS", + "MANAGE_REPORTS", + "MANAGE_BANS", + "MANAGE_NEWS", + "BAN_USERS", + "UNBAN_USERS", + "DELETE_POSTS", + "DELETE_FILES", + "MOVE_POSTS", + "EDIT_POSTS", + "VIEW_IPS", + "VIEW_METADATA", + "VIEW_LOGS", + "USE_CAPCODE", + "CUSTOM_CAPCODE", + "BYPASS_BAN", + "BYPASS_LOCK", + "BYPASS_BLOCK", + "BYPASS_FILTER", + "BYPASS_RATELIMIT", + "ALL_FIELDS", + "PRIVATE_BOARDS", + "ADVANCED_MARKUP" + ] + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100755 index 0000000..9753682 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2741 @@ +{ + "name": "czchan", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "czchan", + "version": "1.0.0", + "license": "SEE LICENSE IN LICENSE.txt", + "dependencies": { + "@kitajs/html": "4.2.9", + "@kitajs/ts-html-plugin": "4.1.2", + "async-mutex": "0.5.0", + "bcrypt": "6.0.0", + "cookie-parser": "1.4.7", + "express": "5.1.0", + "formidable": "3.5.4", + "iovalkey": "0.3.3", + "jsonwebtoken": "9.0.2", + "pg": "8.16.3", + "postgres-migrations": "5.3.0", + "prettier": "3.6.2", + "read": "4.1.0", + "sass": "1.92.1", + "superjson": "2.2.2", + "typescript": "5.9.2", + "uuid": "13.0.0", + "zod": "4.1.7" + }, + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "5.2.2", + "@types/bcrypt": "6.0.0", + "@types/cookie-parser": "1.4.9", + "@types/express": "5.0.3", + "@types/formidable": "3.4.5", + "@types/jsonwebtoken": "9.0.10", + "@types/node": "24.3.1", + "@types/pg": "8.15.5", + "prettier": "3.6.2", + "prettier-plugin-css-order": "2.1.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@iovalkey/commands": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@iovalkey/commands/-/commands-0.1.0.tgz", + "integrity": "sha512-/B9W4qKSSITDii5nkBCHyPkIkAi+ealUtr1oqBJsLxjSRLka4pxun2VvMNSmcwgAMxgXtQfl0qRv7TE+udPJzg==", + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kitajs/html": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@kitajs/html/-/html-4.2.9.tgz", + "integrity": "sha512-FDHHf5Mi5nR0D+Btq86IV1O9XfsePVCiC5rwU4PXjw2aHja16FmIiwLZBO0CS16rJxKkibjMldyRLAW2ni2mzA==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/kitajs/html?sponsor=1" + } + }, + "node_modules/@kitajs/ts-html-plugin": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@kitajs/ts-html-plugin/-/ts-html-plugin-4.1.2.tgz", + "integrity": "sha512-XE9iIe93TELBdQSvNC3xxXOPDhkcK7on4Oi2HUKhln3jAc5hzn1o33uzjHCYhLeW36r/LXCT70beoXRCFcuTxQ==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "tslib": "^2.8.1", + "yargs": "^17.7.2" + }, + "bin": { + "ts-html-plugin": "dist/cli.js", + "xss-scan": "dist/cli.js" + }, + "funding": { + "url": "https://github.com/kitajs/html?sponsor=1" + }, + "peerDependencies": { + "@kitajs/html": "^4.2.5", + "typescript": "^5.6.2" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz", + "integrity": "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/traverse": "^7.26.7", + "@babel/types": "^7.26.7", + "javascript-natural-sort": "^0.7.1", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">18.12" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x", + "prettier-plugin-svelte": "3.x", + "svelte": "4.x || 5.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "svelte": { + "optional": true + } + } + }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", + "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/formidable": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-3.4.5.tgz", + "integrity": "sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/pg": { + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", + "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "optional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", + "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/iovalkey": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/iovalkey/-/iovalkey-0.3.3.tgz", + "integrity": "sha512-4rTJX6Q5wTYEvxboXi8DsEiUo+OvqJGtLYOSGm37KpdRXsG5XJjbVtYKGJpPSWP+QT7rWscA4vsrdmzbEbenpw==", + "license": "MIT", + "dependencies": { + "@iovalkey/commands": "^0.1.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "optional": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-less": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-6.0.0.tgz", + "integrity": "sha512-FPX16mQLyEjLzEuuJtxA8X3ejDLNGGEG503d2YGZR5Ask1SpDN8KmZUMpzCvyalWRywAn1n1VOA5dcqfCLo5rg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "postcss": "^8.3.5" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-migrations": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/postgres-migrations/-/postgres-migrations-5.3.0.tgz", + "integrity": "sha512-gnTHWJZVWbW8T3mXIxJm1JRU853TqBVWkhgfsTJr7zqT3VexjRmJj9kNi96rVhfTezDU4FVW6pf701kLOZiKIA==", + "license": "MIT", + "dependencies": { + "pg": "^8.6.0", + "sql-template-strings": "^2.2.2" + }, + "bin": { + "pg-validate-migrations": "dist/bin/validate.js" + }, + "engines": { + "node": ">10.17.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-css-order": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-css-order/-/prettier-plugin-css-order-2.1.2.tgz", + "integrity": "sha512-vomxPjHI6pOMYcBuouSJHxxQClJXaUpU9rsV9IAO2wrSTZILRRlrxAAR8t9UF6wtczLkLfNRFUwM+ZbGXOONUA==", + "dev": true, + "license": "ISC", + "dependencies": { + "css-declaration-sorter": "^7.1.1", + "postcss-less": "^6.0.0", + "postcss-scss": "^4.0.9" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "prettier": "3.x" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/read": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/read/-/read-4.1.0.tgz", + "integrity": "sha512-uRfX6K+f+R8OOrYScaM3ixPY4erg69f8DN6pgTvMcA9iRc8iDhwrA4m3Yu8YYKsXJgVvum+m8PkRboZwwuLzYA==", + "license": "ISC", + "dependencies": { + "mute-stream": "^2.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.92.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.92.1.tgz", + "integrity": "sha512-ffmsdbwqb3XeyR8jJR6KelIXARM9bFQe8A6Q3W4Klmwy5Ckd5gz7jgUNHo4UOqutU5Sk1DtKLbpDP0nLCg1xqQ==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sql-template-strings": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/sql-template-strings/-/sql-template-strings-2.2.2.tgz", + "integrity": "sha512-UXhXR2869FQaD+GMly8jAMCRZ94nU5KcrFetZfWEMd+LVVG6y0ExgHAhatEcKZ/wk8YcKPdi+hiD2wm75lq3/Q==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.7.tgz", + "integrity": "sha512-6qi6UYyzAl7W9uV29KvcSFXqK4QCYNYUz2YASPNBWpJE1RY6R1nArmmFPgGY/CBYWzpeMw3EOER+DR9a05O4IA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100755 index 0000000..e75dcbf --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "czchan", + "version": "1.0.0", + "description": "Český anonymní imageboard", + "main": "./src/main.ts", + "scripts": { + "test": "echo \"Léky teď\"", + "backend": "npx xss-scan && npx tsc", + "frontend": "npx sass ./web/styles:./public/css", + "build": "npm run backend && npm run frontend", + "start": "node ./dist/src/main.js", + "create-admin": "node ./dist/scripts/create_admin.js" + }, + "repository": { + "type": "git", + "url": "https://git.czchan.org/sneedmaster/czchan" + }, + "author": "sneedmaster ", + "license": "AGPL-3.0-only", + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "5.2.2", + "@types/bcrypt": "6.0.0", + "@types/cookie-parser": "1.4.9", + "@types/express": "5.0.3", + "@types/formidable": "3.4.5", + "@types/jsonwebtoken": "9.0.10", + "@types/node": "24.4.0", + "@types/pg": "8.15.5", + "prettier": "3.6.2", + "prettier-plugin-css-order": "2.1.2" + }, + "dependencies": { + "@kitajs/html": "4.2.9", + "@kitajs/ts-html-plugin": "4.1.2", + "async-mutex": "0.5.0", + "bcrypt": "6.0.0", + "cookie-parser": "1.4.7", + "express": "5.1.0", + "formidable": "3.5.4", + "iovalkey": "0.3.3", + "jsonwebtoken": "9.0.2", + "pg": "8.16.3", + "postgres-migrations": "5.3.0", + "prettier": "3.6.2", + "read": "4.1.0", + "sass": "1.92.1", + "typescript": "5.9.2", + "superjson": "2.2.2", + "uuid": "13.0.0", + "zod": "4.1.8" + } +} diff --git a/public/assets/capcode_icons/admin.png b/public/assets/capcode_icons/admin.png new file mode 100755 index 0000000..6745b2a Binary files /dev/null and b/public/assets/capcode_icons/admin.png differ diff --git a/public/assets/capcode_icons/janny.png b/public/assets/capcode_icons/janny.png new file mode 100755 index 0000000..d1eeb9d Binary files /dev/null and b/public/assets/capcode_icons/janny.png differ diff --git a/public/assets/capcode_icons/manager.png b/public/assets/capcode_icons/manager.png new file mode 100755 index 0000000..45b0a38 Binary files /dev/null and b/public/assets/capcode_icons/manager.png differ diff --git a/public/assets/capcode_icons/mod.png b/public/assets/capcode_icons/mod.png new file mode 100755 index 0000000..3384519 Binary files /dev/null and b/public/assets/capcode_icons/mod.png differ diff --git a/public/assets/capcode_icons/system.png b/public/assets/capcode_icons/system.png new file mode 100755 index 0000000..f0aaf79 Binary files /dev/null and b/public/assets/capcode_icons/system.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100755 index 0000000..6585a85 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/img/country_flags/ad.png b/public/img/country_flags/ad.png new file mode 100755 index 0000000..625ca84 Binary files /dev/null and b/public/img/country_flags/ad.png differ diff --git a/public/img/country_flags/ae.png b/public/img/country_flags/ae.png new file mode 100755 index 0000000..ef3a1ec Binary files /dev/null and b/public/img/country_flags/ae.png differ diff --git a/public/img/country_flags/af.png b/public/img/country_flags/af.png new file mode 100755 index 0000000..a4742e2 Binary files /dev/null and b/public/img/country_flags/af.png differ diff --git a/public/img/country_flags/ag.png b/public/img/country_flags/ag.png new file mode 100755 index 0000000..556d550 Binary files /dev/null and b/public/img/country_flags/ag.png differ diff --git a/public/img/country_flags/ai.png b/public/img/country_flags/ai.png new file mode 100755 index 0000000..74ed29d Binary files /dev/null and b/public/img/country_flags/ai.png differ diff --git a/public/img/country_flags/al.png b/public/img/country_flags/al.png new file mode 100755 index 0000000..92354cb Binary files /dev/null and b/public/img/country_flags/al.png differ diff --git a/public/img/country_flags/am.png b/public/img/country_flags/am.png new file mode 100755 index 0000000..344a2a8 Binary files /dev/null and b/public/img/country_flags/am.png differ diff --git a/public/img/country_flags/an.png b/public/img/country_flags/an.png new file mode 100755 index 0000000..633e4b8 Binary files /dev/null and b/public/img/country_flags/an.png differ diff --git a/public/img/country_flags/ao.png b/public/img/country_flags/ao.png new file mode 100755 index 0000000..bcbd1d6 Binary files /dev/null and b/public/img/country_flags/ao.png differ diff --git a/public/img/country_flags/ar.png b/public/img/country_flags/ar.png new file mode 100755 index 0000000..e5ef8f1 Binary files /dev/null and b/public/img/country_flags/ar.png differ diff --git a/public/img/country_flags/as.png b/public/img/country_flags/as.png new file mode 100755 index 0000000..32f30e4 Binary files /dev/null and b/public/img/country_flags/as.png differ diff --git a/public/img/country_flags/at.png b/public/img/country_flags/at.png new file mode 100755 index 0000000..0f15f34 Binary files /dev/null and b/public/img/country_flags/at.png differ diff --git a/public/img/country_flags/au.png b/public/img/country_flags/au.png new file mode 100755 index 0000000..a01389a Binary files /dev/null and b/public/img/country_flags/au.png differ diff --git a/public/img/country_flags/aw.png b/public/img/country_flags/aw.png new file mode 100755 index 0000000..a3579c2 Binary files /dev/null and b/public/img/country_flags/aw.png differ diff --git a/public/img/country_flags/ax.png b/public/img/country_flags/ax.png new file mode 100755 index 0000000..1eea80a Binary files /dev/null and b/public/img/country_flags/ax.png differ diff --git a/public/img/country_flags/az.png b/public/img/country_flags/az.png new file mode 100755 index 0000000..4ee9fe5 Binary files /dev/null and b/public/img/country_flags/az.png differ diff --git a/public/img/country_flags/ba.png b/public/img/country_flags/ba.png new file mode 100755 index 0000000..c774992 Binary files /dev/null and b/public/img/country_flags/ba.png differ diff --git a/public/img/country_flags/bb.png b/public/img/country_flags/bb.png new file mode 100755 index 0000000..0df19c7 Binary files /dev/null and b/public/img/country_flags/bb.png differ diff --git a/public/img/country_flags/bd.png b/public/img/country_flags/bd.png new file mode 100755 index 0000000..076a8bf Binary files /dev/null and b/public/img/country_flags/bd.png differ diff --git a/public/img/country_flags/be.png b/public/img/country_flags/be.png new file mode 100755 index 0000000..d86ebc8 Binary files /dev/null and b/public/img/country_flags/be.png differ diff --git a/public/img/country_flags/bf.png b/public/img/country_flags/bf.png new file mode 100755 index 0000000..ab5ce8f Binary files /dev/null and b/public/img/country_flags/bf.png differ diff --git a/public/img/country_flags/bg.png b/public/img/country_flags/bg.png new file mode 100755 index 0000000..0469f06 Binary files /dev/null and b/public/img/country_flags/bg.png differ diff --git a/public/img/country_flags/bh.png b/public/img/country_flags/bh.png new file mode 100755 index 0000000..ea8ce68 Binary files /dev/null and b/public/img/country_flags/bh.png differ diff --git a/public/img/country_flags/bi.png b/public/img/country_flags/bi.png new file mode 100755 index 0000000..5cc2e30 Binary files /dev/null and b/public/img/country_flags/bi.png differ diff --git a/public/img/country_flags/bj.png b/public/img/country_flags/bj.png new file mode 100755 index 0000000..1cc8b45 Binary files /dev/null and b/public/img/country_flags/bj.png differ diff --git a/public/img/country_flags/bm.png b/public/img/country_flags/bm.png new file mode 100755 index 0000000..c0c7aea Binary files /dev/null and b/public/img/country_flags/bm.png differ diff --git a/public/img/country_flags/bn.png b/public/img/country_flags/bn.png new file mode 100755 index 0000000..8fb0984 Binary files /dev/null and b/public/img/country_flags/bn.png differ diff --git a/public/img/country_flags/bo.png b/public/img/country_flags/bo.png new file mode 100755 index 0000000..ce7ba52 Binary files /dev/null and b/public/img/country_flags/bo.png differ diff --git a/public/img/country_flags/br.png b/public/img/country_flags/br.png new file mode 100755 index 0000000..9b1a553 Binary files /dev/null and b/public/img/country_flags/br.png differ diff --git a/public/img/country_flags/bs.png b/public/img/country_flags/bs.png new file mode 100755 index 0000000..639fa6c Binary files /dev/null and b/public/img/country_flags/bs.png differ diff --git a/public/img/country_flags/bt.png b/public/img/country_flags/bt.png new file mode 100755 index 0000000..1d512df Binary files /dev/null and b/public/img/country_flags/bt.png differ diff --git a/public/img/country_flags/bv.png b/public/img/country_flags/bv.png new file mode 100755 index 0000000..160b6b5 Binary files /dev/null and b/public/img/country_flags/bv.png differ diff --git a/public/img/country_flags/bw.png b/public/img/country_flags/bw.png new file mode 100755 index 0000000..fcb1039 Binary files /dev/null and b/public/img/country_flags/bw.png differ diff --git a/public/img/country_flags/by.png b/public/img/country_flags/by.png new file mode 100755 index 0000000..504774e Binary files /dev/null and b/public/img/country_flags/by.png differ diff --git a/public/img/country_flags/bz.png b/public/img/country_flags/bz.png new file mode 100755 index 0000000..be63ee1 Binary files /dev/null and b/public/img/country_flags/bz.png differ diff --git a/public/img/country_flags/ca.png b/public/img/country_flags/ca.png new file mode 100755 index 0000000..1f20419 Binary files /dev/null and b/public/img/country_flags/ca.png differ diff --git a/public/img/country_flags/cc.png b/public/img/country_flags/cc.png new file mode 100755 index 0000000..aed3d3b Binary files /dev/null and b/public/img/country_flags/cc.png differ diff --git a/public/img/country_flags/cd.png b/public/img/country_flags/cd.png new file mode 100755 index 0000000..5e48942 Binary files /dev/null and b/public/img/country_flags/cd.png differ diff --git a/public/img/country_flags/cf.png b/public/img/country_flags/cf.png new file mode 100755 index 0000000..da687bd Binary files /dev/null and b/public/img/country_flags/cf.png differ diff --git a/public/img/country_flags/cg.png b/public/img/country_flags/cg.png new file mode 100755 index 0000000..a859792 Binary files /dev/null and b/public/img/country_flags/cg.png differ diff --git a/public/img/country_flags/ch.png b/public/img/country_flags/ch.png new file mode 100755 index 0000000..242ec01 Binary files /dev/null and b/public/img/country_flags/ch.png differ diff --git a/public/img/country_flags/ci.png b/public/img/country_flags/ci.png new file mode 100755 index 0000000..3f2c62e Binary files /dev/null and b/public/img/country_flags/ci.png differ diff --git a/public/img/country_flags/ck.png b/public/img/country_flags/ck.png new file mode 100755 index 0000000..746d3d6 Binary files /dev/null and b/public/img/country_flags/ck.png differ diff --git a/public/img/country_flags/cl.png b/public/img/country_flags/cl.png new file mode 100755 index 0000000..29c6d61 Binary files /dev/null and b/public/img/country_flags/cl.png differ diff --git a/public/img/country_flags/cm.png b/public/img/country_flags/cm.png new file mode 100755 index 0000000..f65c5bd Binary files /dev/null and b/public/img/country_flags/cm.png differ diff --git a/public/img/country_flags/cn.png b/public/img/country_flags/cn.png new file mode 100755 index 0000000..8914414 Binary files /dev/null and b/public/img/country_flags/cn.png differ diff --git a/public/img/country_flags/co.png b/public/img/country_flags/co.png new file mode 100755 index 0000000..a118ff4 Binary files /dev/null and b/public/img/country_flags/co.png differ diff --git a/public/img/country_flags/cr.png b/public/img/country_flags/cr.png new file mode 100755 index 0000000..c7a3731 Binary files /dev/null and b/public/img/country_flags/cr.png differ diff --git a/public/img/country_flags/cs.png b/public/img/country_flags/cs.png new file mode 100755 index 0000000..8254790 Binary files /dev/null and b/public/img/country_flags/cs.png differ diff --git a/public/img/country_flags/cu.png b/public/img/country_flags/cu.png new file mode 100755 index 0000000..083f1d6 Binary files /dev/null and b/public/img/country_flags/cu.png differ diff --git a/public/img/country_flags/cv.png b/public/img/country_flags/cv.png new file mode 100755 index 0000000..a63f7ea Binary files /dev/null and b/public/img/country_flags/cv.png differ diff --git a/public/img/country_flags/cx.png b/public/img/country_flags/cx.png new file mode 100755 index 0000000..48e31ad Binary files /dev/null and b/public/img/country_flags/cx.png differ diff --git a/public/img/country_flags/cy.png b/public/img/country_flags/cy.png new file mode 100755 index 0000000..5b1ad6c Binary files /dev/null and b/public/img/country_flags/cy.png differ diff --git a/public/img/country_flags/cz.png b/public/img/country_flags/cz.png new file mode 100755 index 0000000..c8403dd Binary files /dev/null and b/public/img/country_flags/cz.png differ diff --git a/public/img/country_flags/de.png b/public/img/country_flags/de.png new file mode 100755 index 0000000..ac4a977 Binary files /dev/null and b/public/img/country_flags/de.png differ diff --git a/public/img/country_flags/dj.png b/public/img/country_flags/dj.png new file mode 100755 index 0000000..582af36 Binary files /dev/null and b/public/img/country_flags/dj.png differ diff --git a/public/img/country_flags/dk.png b/public/img/country_flags/dk.png new file mode 100755 index 0000000..e2993d3 Binary files /dev/null and b/public/img/country_flags/dk.png differ diff --git a/public/img/country_flags/dm.png b/public/img/country_flags/dm.png new file mode 100755 index 0000000..5fbffcb Binary files /dev/null and b/public/img/country_flags/dm.png differ diff --git a/public/img/country_flags/do.png b/public/img/country_flags/do.png new file mode 100755 index 0000000..5a04932 Binary files /dev/null and b/public/img/country_flags/do.png differ diff --git a/public/img/country_flags/dz.png b/public/img/country_flags/dz.png new file mode 100755 index 0000000..335c239 Binary files /dev/null and b/public/img/country_flags/dz.png differ diff --git a/public/img/country_flags/ec.png b/public/img/country_flags/ec.png new file mode 100755 index 0000000..0caa0b1 Binary files /dev/null and b/public/img/country_flags/ec.png differ diff --git a/public/img/country_flags/ee.png b/public/img/country_flags/ee.png new file mode 100755 index 0000000..0c82efb Binary files /dev/null and b/public/img/country_flags/ee.png differ diff --git a/public/img/country_flags/eg.png b/public/img/country_flags/eg.png new file mode 100755 index 0000000..8a3f7a1 Binary files /dev/null and b/public/img/country_flags/eg.png differ diff --git a/public/img/country_flags/eh.png b/public/img/country_flags/eh.png new file mode 100755 index 0000000..90a1195 Binary files /dev/null and b/public/img/country_flags/eh.png differ diff --git a/public/img/country_flags/er.png b/public/img/country_flags/er.png new file mode 100755 index 0000000..13065ae Binary files /dev/null and b/public/img/country_flags/er.png differ diff --git a/public/img/country_flags/es.png b/public/img/country_flags/es.png new file mode 100755 index 0000000..c2de2d7 Binary files /dev/null and b/public/img/country_flags/es.png differ diff --git a/public/img/country_flags/et.png b/public/img/country_flags/et.png new file mode 100755 index 0000000..2e893fa Binary files /dev/null and b/public/img/country_flags/et.png differ diff --git a/public/img/country_flags/fam.png b/public/img/country_flags/fam.png new file mode 100755 index 0000000..cf50c75 Binary files /dev/null and b/public/img/country_flags/fam.png differ diff --git a/public/img/country_flags/fi.png b/public/img/country_flags/fi.png new file mode 100755 index 0000000..14ec091 Binary files /dev/null and b/public/img/country_flags/fi.png differ diff --git a/public/img/country_flags/fj.png b/public/img/country_flags/fj.png new file mode 100755 index 0000000..cee9988 Binary files /dev/null and b/public/img/country_flags/fj.png differ diff --git a/public/img/country_flags/fk.png b/public/img/country_flags/fk.png new file mode 100755 index 0000000..ceaeb27 Binary files /dev/null and b/public/img/country_flags/fk.png differ diff --git a/public/img/country_flags/fm.png b/public/img/country_flags/fm.png new file mode 100755 index 0000000..066bb24 Binary files /dev/null and b/public/img/country_flags/fm.png differ diff --git a/public/img/country_flags/fo.png b/public/img/country_flags/fo.png new file mode 100755 index 0000000..cbceb80 Binary files /dev/null and b/public/img/country_flags/fo.png differ diff --git a/public/img/country_flags/fr.png b/public/img/country_flags/fr.png new file mode 100755 index 0000000..8332c4e Binary files /dev/null and b/public/img/country_flags/fr.png differ diff --git a/public/img/country_flags/ga.png b/public/img/country_flags/ga.png new file mode 100755 index 0000000..0e0d434 Binary files /dev/null and b/public/img/country_flags/ga.png differ diff --git a/public/img/country_flags/gb.png b/public/img/country_flags/gb.png new file mode 100755 index 0000000..ff701e1 Binary files /dev/null and b/public/img/country_flags/gb.png differ diff --git a/public/img/country_flags/gd.png b/public/img/country_flags/gd.png new file mode 100755 index 0000000..9ab57f5 Binary files /dev/null and b/public/img/country_flags/gd.png differ diff --git a/public/img/country_flags/ge.png b/public/img/country_flags/ge.png new file mode 100755 index 0000000..728d970 Binary files /dev/null and b/public/img/country_flags/ge.png differ diff --git a/public/img/country_flags/gf.png b/public/img/country_flags/gf.png new file mode 100755 index 0000000..8332c4e Binary files /dev/null and b/public/img/country_flags/gf.png differ diff --git a/public/img/country_flags/gh.png b/public/img/country_flags/gh.png new file mode 100755 index 0000000..4e2f896 Binary files /dev/null and b/public/img/country_flags/gh.png differ diff --git a/public/img/country_flags/gi.png b/public/img/country_flags/gi.png new file mode 100755 index 0000000..e76797f Binary files /dev/null and b/public/img/country_flags/gi.png differ diff --git a/public/img/country_flags/gl.png b/public/img/country_flags/gl.png new file mode 100755 index 0000000..ef12a73 Binary files /dev/null and b/public/img/country_flags/gl.png differ diff --git a/public/img/country_flags/gm.png b/public/img/country_flags/gm.png new file mode 100755 index 0000000..0720b66 Binary files /dev/null and b/public/img/country_flags/gm.png differ diff --git a/public/img/country_flags/gn.png b/public/img/country_flags/gn.png new file mode 100755 index 0000000..ea660b0 Binary files /dev/null and b/public/img/country_flags/gn.png differ diff --git a/public/img/country_flags/gp.png b/public/img/country_flags/gp.png new file mode 100755 index 0000000..dbb086d Binary files /dev/null and b/public/img/country_flags/gp.png differ diff --git a/public/img/country_flags/gq.png b/public/img/country_flags/gq.png new file mode 100755 index 0000000..ebe20a2 Binary files /dev/null and b/public/img/country_flags/gq.png differ diff --git a/public/img/country_flags/gr.png b/public/img/country_flags/gr.png new file mode 100755 index 0000000..8651ade Binary files /dev/null and b/public/img/country_flags/gr.png differ diff --git a/public/img/country_flags/gs.png b/public/img/country_flags/gs.png new file mode 100755 index 0000000..7ef0bf5 Binary files /dev/null and b/public/img/country_flags/gs.png differ diff --git a/public/img/country_flags/gt.png b/public/img/country_flags/gt.png new file mode 100755 index 0000000..c43a70d Binary files /dev/null and b/public/img/country_flags/gt.png differ diff --git a/public/img/country_flags/gu.png b/public/img/country_flags/gu.png new file mode 100755 index 0000000..92f37c0 Binary files /dev/null and b/public/img/country_flags/gu.png differ diff --git a/public/img/country_flags/gw.png b/public/img/country_flags/gw.png new file mode 100755 index 0000000..b37bcf0 Binary files /dev/null and b/public/img/country_flags/gw.png differ diff --git a/public/img/country_flags/gy.png b/public/img/country_flags/gy.png new file mode 100755 index 0000000..22cbe2f Binary files /dev/null and b/public/img/country_flags/gy.png differ diff --git a/public/img/country_flags/hk.png b/public/img/country_flags/hk.png new file mode 100755 index 0000000..d5c380c Binary files /dev/null and b/public/img/country_flags/hk.png differ diff --git a/public/img/country_flags/hm.png b/public/img/country_flags/hm.png new file mode 100755 index 0000000..a01389a Binary files /dev/null and b/public/img/country_flags/hm.png differ diff --git a/public/img/country_flags/hn.png b/public/img/country_flags/hn.png new file mode 100755 index 0000000..96f8388 Binary files /dev/null and b/public/img/country_flags/hn.png differ diff --git a/public/img/country_flags/hr.png b/public/img/country_flags/hr.png new file mode 100755 index 0000000..696b515 Binary files /dev/null and b/public/img/country_flags/hr.png differ diff --git a/public/img/country_flags/ht.png b/public/img/country_flags/ht.png new file mode 100755 index 0000000..416052a Binary files /dev/null and b/public/img/country_flags/ht.png differ diff --git a/public/img/country_flags/hu.png b/public/img/country_flags/hu.png new file mode 100755 index 0000000..7baafe4 Binary files /dev/null and b/public/img/country_flags/hu.png differ diff --git a/public/img/country_flags/id.png b/public/img/country_flags/id.png new file mode 100755 index 0000000..c6bc0fa Binary files /dev/null and b/public/img/country_flags/id.png differ diff --git a/public/img/country_flags/ie.png b/public/img/country_flags/ie.png new file mode 100755 index 0000000..26baa31 Binary files /dev/null and b/public/img/country_flags/ie.png differ diff --git a/public/img/country_flags/il.png b/public/img/country_flags/il.png new file mode 100755 index 0000000..2ca772d Binary files /dev/null and b/public/img/country_flags/il.png differ diff --git a/public/img/country_flags/in.png b/public/img/country_flags/in.png new file mode 100755 index 0000000..e4d7e81 Binary files /dev/null and b/public/img/country_flags/in.png differ diff --git a/public/img/country_flags/io.png b/public/img/country_flags/io.png new file mode 100755 index 0000000..3e74b6a Binary files /dev/null and b/public/img/country_flags/io.png differ diff --git a/public/img/country_flags/iq.png b/public/img/country_flags/iq.png new file mode 100755 index 0000000..878a351 Binary files /dev/null and b/public/img/country_flags/iq.png differ diff --git a/public/img/country_flags/ir.png b/public/img/country_flags/ir.png new file mode 100755 index 0000000..c5fd136 Binary files /dev/null and b/public/img/country_flags/ir.png differ diff --git a/public/img/country_flags/is.png b/public/img/country_flags/is.png new file mode 100755 index 0000000..b8f6d0f Binary files /dev/null and b/public/img/country_flags/is.png differ diff --git a/public/img/country_flags/it.png b/public/img/country_flags/it.png new file mode 100755 index 0000000..89692f7 Binary files /dev/null and b/public/img/country_flags/it.png differ diff --git a/public/img/country_flags/jm.png b/public/img/country_flags/jm.png new file mode 100755 index 0000000..7be119e Binary files /dev/null and b/public/img/country_flags/jm.png differ diff --git a/public/img/country_flags/jo.png b/public/img/country_flags/jo.png new file mode 100755 index 0000000..11bd497 Binary files /dev/null and b/public/img/country_flags/jo.png differ diff --git a/public/img/country_flags/jp.png b/public/img/country_flags/jp.png new file mode 100755 index 0000000..325fbad Binary files /dev/null and b/public/img/country_flags/jp.png differ diff --git a/public/img/country_flags/ke.png b/public/img/country_flags/ke.png new file mode 100755 index 0000000..51879ad Binary files /dev/null and b/public/img/country_flags/ke.png differ diff --git a/public/img/country_flags/kg.png b/public/img/country_flags/kg.png new file mode 100755 index 0000000..0a818f6 Binary files /dev/null and b/public/img/country_flags/kg.png differ diff --git a/public/img/country_flags/kh.png b/public/img/country_flags/kh.png new file mode 100755 index 0000000..30f6bb1 Binary files /dev/null and b/public/img/country_flags/kh.png differ diff --git a/public/img/country_flags/ki.png b/public/img/country_flags/ki.png new file mode 100755 index 0000000..2dcce4b Binary files /dev/null and b/public/img/country_flags/ki.png differ diff --git a/public/img/country_flags/km.png b/public/img/country_flags/km.png new file mode 100755 index 0000000..812b2f5 Binary files /dev/null and b/public/img/country_flags/km.png differ diff --git a/public/img/country_flags/kn.png b/public/img/country_flags/kn.png new file mode 100755 index 0000000..febd5b4 Binary files /dev/null and b/public/img/country_flags/kn.png differ diff --git a/public/img/country_flags/kp.png b/public/img/country_flags/kp.png new file mode 100755 index 0000000..d3d509a Binary files /dev/null and b/public/img/country_flags/kp.png differ diff --git a/public/img/country_flags/kr.png b/public/img/country_flags/kr.png new file mode 100755 index 0000000..9c0a78e Binary files /dev/null and b/public/img/country_flags/kr.png differ diff --git a/public/img/country_flags/kw.png b/public/img/country_flags/kw.png new file mode 100755 index 0000000..96546da Binary files /dev/null and b/public/img/country_flags/kw.png differ diff --git a/public/img/country_flags/ky.png b/public/img/country_flags/ky.png new file mode 100755 index 0000000..15c5f8e Binary files /dev/null and b/public/img/country_flags/ky.png differ diff --git a/public/img/country_flags/kz.png b/public/img/country_flags/kz.png new file mode 100755 index 0000000..45a8c88 Binary files /dev/null and b/public/img/country_flags/kz.png differ diff --git a/public/img/country_flags/la.png b/public/img/country_flags/la.png new file mode 100755 index 0000000..e28acd0 Binary files /dev/null and b/public/img/country_flags/la.png differ diff --git a/public/img/country_flags/lb.png b/public/img/country_flags/lb.png new file mode 100755 index 0000000..d0d452b Binary files /dev/null and b/public/img/country_flags/lb.png differ diff --git a/public/img/country_flags/lc.png b/public/img/country_flags/lc.png new file mode 100755 index 0000000..a47d065 Binary files /dev/null and b/public/img/country_flags/lc.png differ diff --git a/public/img/country_flags/li.png b/public/img/country_flags/li.png new file mode 100755 index 0000000..6469909 Binary files /dev/null and b/public/img/country_flags/li.png differ diff --git a/public/img/country_flags/lk.png b/public/img/country_flags/lk.png new file mode 100755 index 0000000..088aad6 Binary files /dev/null and b/public/img/country_flags/lk.png differ diff --git a/public/img/country_flags/lr.png b/public/img/country_flags/lr.png new file mode 100755 index 0000000..89a5bc7 Binary files /dev/null and b/public/img/country_flags/lr.png differ diff --git a/public/img/country_flags/ls.png b/public/img/country_flags/ls.png new file mode 100755 index 0000000..33fdef1 Binary files /dev/null and b/public/img/country_flags/ls.png differ diff --git a/public/img/country_flags/lt.png b/public/img/country_flags/lt.png new file mode 100755 index 0000000..c8ef0da Binary files /dev/null and b/public/img/country_flags/lt.png differ diff --git a/public/img/country_flags/lu.png b/public/img/country_flags/lu.png new file mode 100755 index 0000000..4cabba9 Binary files /dev/null and b/public/img/country_flags/lu.png differ diff --git a/public/img/country_flags/lv.png b/public/img/country_flags/lv.png new file mode 100755 index 0000000..49b6998 Binary files /dev/null and b/public/img/country_flags/lv.png differ diff --git a/public/img/country_flags/ly.png b/public/img/country_flags/ly.png new file mode 100755 index 0000000..b163a9f Binary files /dev/null and b/public/img/country_flags/ly.png differ diff --git a/public/img/country_flags/ma.png b/public/img/country_flags/ma.png new file mode 100755 index 0000000..f386770 Binary files /dev/null and b/public/img/country_flags/ma.png differ diff --git a/public/img/country_flags/mc.png b/public/img/country_flags/mc.png new file mode 100755 index 0000000..1aa830f Binary files /dev/null and b/public/img/country_flags/mc.png differ diff --git a/public/img/country_flags/md.png b/public/img/country_flags/md.png new file mode 100755 index 0000000..4e92c18 Binary files /dev/null and b/public/img/country_flags/md.png differ diff --git a/public/img/country_flags/me.png b/public/img/country_flags/me.png new file mode 100755 index 0000000..ac72535 Binary files /dev/null and b/public/img/country_flags/me.png differ diff --git a/public/img/country_flags/mg.png b/public/img/country_flags/mg.png new file mode 100755 index 0000000..d2715b3 Binary files /dev/null and b/public/img/country_flags/mg.png differ diff --git a/public/img/country_flags/mh.png b/public/img/country_flags/mh.png new file mode 100755 index 0000000..fb523a8 Binary files /dev/null and b/public/img/country_flags/mh.png differ diff --git a/public/img/country_flags/mk.png b/public/img/country_flags/mk.png new file mode 100755 index 0000000..db173aa Binary files /dev/null and b/public/img/country_flags/mk.png differ diff --git a/public/img/country_flags/ml.png b/public/img/country_flags/ml.png new file mode 100755 index 0000000..2cec8ba Binary files /dev/null and b/public/img/country_flags/ml.png differ diff --git a/public/img/country_flags/mm.png b/public/img/country_flags/mm.png new file mode 100755 index 0000000..f464f67 Binary files /dev/null and b/public/img/country_flags/mm.png differ diff --git a/public/img/country_flags/mn.png b/public/img/country_flags/mn.png new file mode 100755 index 0000000..9396355 Binary files /dev/null and b/public/img/country_flags/mn.png differ diff --git a/public/img/country_flags/mo.png b/public/img/country_flags/mo.png new file mode 100755 index 0000000..deb801d Binary files /dev/null and b/public/img/country_flags/mo.png differ diff --git a/public/img/country_flags/mp.png b/public/img/country_flags/mp.png new file mode 100755 index 0000000..298d588 Binary files /dev/null and b/public/img/country_flags/mp.png differ diff --git a/public/img/country_flags/mq.png b/public/img/country_flags/mq.png new file mode 100755 index 0000000..010143b Binary files /dev/null and b/public/img/country_flags/mq.png differ diff --git a/public/img/country_flags/mr.png b/public/img/country_flags/mr.png new file mode 100755 index 0000000..319546b Binary files /dev/null and b/public/img/country_flags/mr.png differ diff --git a/public/img/country_flags/ms.png b/public/img/country_flags/ms.png new file mode 100755 index 0000000..d4cbb43 Binary files /dev/null and b/public/img/country_flags/ms.png differ diff --git a/public/img/country_flags/mt.png b/public/img/country_flags/mt.png new file mode 100755 index 0000000..00af948 Binary files /dev/null and b/public/img/country_flags/mt.png differ diff --git a/public/img/country_flags/mu.png b/public/img/country_flags/mu.png new file mode 100755 index 0000000..b7fdce1 Binary files /dev/null and b/public/img/country_flags/mu.png differ diff --git a/public/img/country_flags/mv.png b/public/img/country_flags/mv.png new file mode 100755 index 0000000..5073d9e Binary files /dev/null and b/public/img/country_flags/mv.png differ diff --git a/public/img/country_flags/mw.png b/public/img/country_flags/mw.png new file mode 100755 index 0000000..13886e9 Binary files /dev/null and b/public/img/country_flags/mw.png differ diff --git a/public/img/country_flags/mx.png b/public/img/country_flags/mx.png new file mode 100755 index 0000000..5bc58ab Binary files /dev/null and b/public/img/country_flags/mx.png differ diff --git a/public/img/country_flags/my.png b/public/img/country_flags/my.png new file mode 100755 index 0000000..9034cba Binary files /dev/null and b/public/img/country_flags/my.png differ diff --git a/public/img/country_flags/mz.png b/public/img/country_flags/mz.png new file mode 100755 index 0000000..76405e0 Binary files /dev/null and b/public/img/country_flags/mz.png differ diff --git a/public/img/country_flags/na.png b/public/img/country_flags/na.png new file mode 100755 index 0000000..63358c6 Binary files /dev/null and b/public/img/country_flags/na.png differ diff --git a/public/img/country_flags/nc.png b/public/img/country_flags/nc.png new file mode 100755 index 0000000..2cad283 Binary files /dev/null and b/public/img/country_flags/nc.png differ diff --git a/public/img/country_flags/ne.png b/public/img/country_flags/ne.png new file mode 100755 index 0000000..d85f424 Binary files /dev/null and b/public/img/country_flags/ne.png differ diff --git a/public/img/country_flags/nf.png b/public/img/country_flags/nf.png new file mode 100755 index 0000000..f9bcdda Binary files /dev/null and b/public/img/country_flags/nf.png differ diff --git a/public/img/country_flags/ng.png b/public/img/country_flags/ng.png new file mode 100755 index 0000000..3eea2e0 Binary files /dev/null and b/public/img/country_flags/ng.png differ diff --git a/public/img/country_flags/ni.png b/public/img/country_flags/ni.png new file mode 100755 index 0000000..3969aaa Binary files /dev/null and b/public/img/country_flags/ni.png differ diff --git a/public/img/country_flags/nl.png b/public/img/country_flags/nl.png new file mode 100755 index 0000000..fe44791 Binary files /dev/null and b/public/img/country_flags/nl.png differ diff --git a/public/img/country_flags/no.png b/public/img/country_flags/no.png new file mode 100755 index 0000000..160b6b5 Binary files /dev/null and b/public/img/country_flags/no.png differ diff --git a/public/img/country_flags/np.png b/public/img/country_flags/np.png new file mode 100755 index 0000000..aeb058b Binary files /dev/null and b/public/img/country_flags/np.png differ diff --git a/public/img/country_flags/nr.png b/public/img/country_flags/nr.png new file mode 100755 index 0000000..705fc33 Binary files /dev/null and b/public/img/country_flags/nr.png differ diff --git a/public/img/country_flags/nu.png b/public/img/country_flags/nu.png new file mode 100755 index 0000000..c3ce4ae Binary files /dev/null and b/public/img/country_flags/nu.png differ diff --git a/public/img/country_flags/nz.png b/public/img/country_flags/nz.png new file mode 100755 index 0000000..10d6306 Binary files /dev/null and b/public/img/country_flags/nz.png differ diff --git a/public/img/country_flags/om.png b/public/img/country_flags/om.png new file mode 100755 index 0000000..2ffba7e Binary files /dev/null and b/public/img/country_flags/om.png differ diff --git a/public/img/country_flags/pa.png b/public/img/country_flags/pa.png new file mode 100755 index 0000000..9b2ee9a Binary files /dev/null and b/public/img/country_flags/pa.png differ diff --git a/public/img/country_flags/pe.png b/public/img/country_flags/pe.png new file mode 100755 index 0000000..62a0497 Binary files /dev/null and b/public/img/country_flags/pe.png differ diff --git a/public/img/country_flags/pf.png b/public/img/country_flags/pf.png new file mode 100755 index 0000000..771a0f6 Binary files /dev/null and b/public/img/country_flags/pf.png differ diff --git a/public/img/country_flags/pg.png b/public/img/country_flags/pg.png new file mode 100755 index 0000000..10d6233 Binary files /dev/null and b/public/img/country_flags/pg.png differ diff --git a/public/img/country_flags/ph.png b/public/img/country_flags/ph.png new file mode 100755 index 0000000..b89e159 Binary files /dev/null and b/public/img/country_flags/ph.png differ diff --git a/public/img/country_flags/pk.png b/public/img/country_flags/pk.png new file mode 100755 index 0000000..e9df70c Binary files /dev/null and b/public/img/country_flags/pk.png differ diff --git a/public/img/country_flags/pl.png b/public/img/country_flags/pl.png new file mode 100755 index 0000000..d413d01 Binary files /dev/null and b/public/img/country_flags/pl.png differ diff --git a/public/img/country_flags/pm.png b/public/img/country_flags/pm.png new file mode 100755 index 0000000..ba91d2c Binary files /dev/null and b/public/img/country_flags/pm.png differ diff --git a/public/img/country_flags/pn.png b/public/img/country_flags/pn.png new file mode 100755 index 0000000..aa9344f Binary files /dev/null and b/public/img/country_flags/pn.png differ diff --git a/public/img/country_flags/pr.png b/public/img/country_flags/pr.png new file mode 100755 index 0000000..82d9130 Binary files /dev/null and b/public/img/country_flags/pr.png differ diff --git a/public/img/country_flags/ps.png b/public/img/country_flags/ps.png new file mode 100755 index 0000000..f5f5477 Binary files /dev/null and b/public/img/country_flags/ps.png differ diff --git a/public/img/country_flags/pt.png b/public/img/country_flags/pt.png new file mode 100755 index 0000000..ece7980 Binary files /dev/null and b/public/img/country_flags/pt.png differ diff --git a/public/img/country_flags/pw.png b/public/img/country_flags/pw.png new file mode 100755 index 0000000..6178b25 Binary files /dev/null and b/public/img/country_flags/pw.png differ diff --git a/public/img/country_flags/py.png b/public/img/country_flags/py.png new file mode 100755 index 0000000..cb8723c Binary files /dev/null and b/public/img/country_flags/py.png differ diff --git a/public/img/country_flags/qa.png b/public/img/country_flags/qa.png new file mode 100755 index 0000000..ed4c621 Binary files /dev/null and b/public/img/country_flags/qa.png differ diff --git a/public/img/country_flags/re.png b/public/img/country_flags/re.png new file mode 100755 index 0000000..8332c4e Binary files /dev/null and b/public/img/country_flags/re.png differ diff --git a/public/img/country_flags/ro.png b/public/img/country_flags/ro.png new file mode 100755 index 0000000..57e74a6 Binary files /dev/null and b/public/img/country_flags/ro.png differ diff --git a/public/img/country_flags/rs.png b/public/img/country_flags/rs.png new file mode 100755 index 0000000..9439a5b Binary files /dev/null and b/public/img/country_flags/rs.png differ diff --git a/public/img/country_flags/ru.png b/public/img/country_flags/ru.png new file mode 100755 index 0000000..47da421 Binary files /dev/null and b/public/img/country_flags/ru.png differ diff --git a/public/img/country_flags/rw.png b/public/img/country_flags/rw.png new file mode 100755 index 0000000..5356491 Binary files /dev/null and b/public/img/country_flags/rw.png differ diff --git a/public/img/country_flags/sa.png b/public/img/country_flags/sa.png new file mode 100755 index 0000000..b4641c7 Binary files /dev/null and b/public/img/country_flags/sa.png differ diff --git a/public/img/country_flags/sb.png b/public/img/country_flags/sb.png new file mode 100755 index 0000000..a9937cc Binary files /dev/null and b/public/img/country_flags/sb.png differ diff --git a/public/img/country_flags/sc.png b/public/img/country_flags/sc.png new file mode 100755 index 0000000..39ee371 Binary files /dev/null and b/public/img/country_flags/sc.png differ diff --git a/public/img/country_flags/sd.png b/public/img/country_flags/sd.png new file mode 100755 index 0000000..eaab69e Binary files /dev/null and b/public/img/country_flags/sd.png differ diff --git a/public/img/country_flags/se.png b/public/img/country_flags/se.png new file mode 100755 index 0000000..1994653 Binary files /dev/null and b/public/img/country_flags/se.png differ diff --git a/public/img/country_flags/sg.png b/public/img/country_flags/sg.png new file mode 100755 index 0000000..dd34d61 Binary files /dev/null and b/public/img/country_flags/sg.png differ diff --git a/public/img/country_flags/sh.png b/public/img/country_flags/sh.png new file mode 100755 index 0000000..4b1d2a2 Binary files /dev/null and b/public/img/country_flags/sh.png differ diff --git a/public/img/country_flags/si.png b/public/img/country_flags/si.png new file mode 100755 index 0000000..bb1476f Binary files /dev/null and b/public/img/country_flags/si.png differ diff --git a/public/img/country_flags/sj.png b/public/img/country_flags/sj.png new file mode 100755 index 0000000..160b6b5 Binary files /dev/null and b/public/img/country_flags/sj.png differ diff --git a/public/img/country_flags/sk.png b/public/img/country_flags/sk.png new file mode 100755 index 0000000..7ccbc82 Binary files /dev/null and b/public/img/country_flags/sk.png differ diff --git a/public/img/country_flags/sl.png b/public/img/country_flags/sl.png new file mode 100755 index 0000000..12d812d Binary files /dev/null and b/public/img/country_flags/sl.png differ diff --git a/public/img/country_flags/sm.png b/public/img/country_flags/sm.png new file mode 100755 index 0000000..3df2fdc Binary files /dev/null and b/public/img/country_flags/sm.png differ diff --git a/public/img/country_flags/sn.png b/public/img/country_flags/sn.png new file mode 100755 index 0000000..eabb71d Binary files /dev/null and b/public/img/country_flags/sn.png differ diff --git a/public/img/country_flags/so.png b/public/img/country_flags/so.png new file mode 100755 index 0000000..4a1ea4b Binary files /dev/null and b/public/img/country_flags/so.png differ diff --git a/public/img/country_flags/sr.png b/public/img/country_flags/sr.png new file mode 100755 index 0000000..5eff927 Binary files /dev/null and b/public/img/country_flags/sr.png differ diff --git a/public/img/country_flags/st.png b/public/img/country_flags/st.png new file mode 100755 index 0000000..2978557 Binary files /dev/null and b/public/img/country_flags/st.png differ diff --git a/public/img/country_flags/sv.png b/public/img/country_flags/sv.png new file mode 100755 index 0000000..2498799 Binary files /dev/null and b/public/img/country_flags/sv.png differ diff --git a/public/img/country_flags/sy.png b/public/img/country_flags/sy.png new file mode 100755 index 0000000..f5ce30d Binary files /dev/null and b/public/img/country_flags/sy.png differ diff --git a/public/img/country_flags/sz.png b/public/img/country_flags/sz.png new file mode 100755 index 0000000..914ee86 Binary files /dev/null and b/public/img/country_flags/sz.png differ diff --git a/public/img/country_flags/tc.png b/public/img/country_flags/tc.png new file mode 100755 index 0000000..8fc1156 Binary files /dev/null and b/public/img/country_flags/tc.png differ diff --git a/public/img/country_flags/td.png b/public/img/country_flags/td.png new file mode 100755 index 0000000..667f21f Binary files /dev/null and b/public/img/country_flags/td.png differ diff --git a/public/img/country_flags/tf.png b/public/img/country_flags/tf.png new file mode 100755 index 0000000..80529a4 Binary files /dev/null and b/public/img/country_flags/tf.png differ diff --git a/public/img/country_flags/tg.png b/public/img/country_flags/tg.png new file mode 100755 index 0000000..3aa00ad Binary files /dev/null and b/public/img/country_flags/tg.png differ diff --git a/public/img/country_flags/th.png b/public/img/country_flags/th.png new file mode 100755 index 0000000..dd8ba91 Binary files /dev/null and b/public/img/country_flags/th.png differ diff --git a/public/img/country_flags/tj.png b/public/img/country_flags/tj.png new file mode 100755 index 0000000..617bf64 Binary files /dev/null and b/public/img/country_flags/tj.png differ diff --git a/public/img/country_flags/tk.png b/public/img/country_flags/tk.png new file mode 100755 index 0000000..67b8c8c Binary files /dev/null and b/public/img/country_flags/tk.png differ diff --git a/public/img/country_flags/tl.png b/public/img/country_flags/tl.png new file mode 100755 index 0000000..77da181 Binary files /dev/null and b/public/img/country_flags/tl.png differ diff --git a/public/img/country_flags/tm.png b/public/img/country_flags/tm.png new file mode 100755 index 0000000..828020e Binary files /dev/null and b/public/img/country_flags/tm.png differ diff --git a/public/img/country_flags/tn.png b/public/img/country_flags/tn.png new file mode 100755 index 0000000..183cdd3 Binary files /dev/null and b/public/img/country_flags/tn.png differ diff --git a/public/img/country_flags/to.png b/public/img/country_flags/to.png new file mode 100755 index 0000000..f89b8ba Binary files /dev/null and b/public/img/country_flags/to.png differ diff --git a/public/img/country_flags/tr.png b/public/img/country_flags/tr.png new file mode 100755 index 0000000..be32f77 Binary files /dev/null and b/public/img/country_flags/tr.png differ diff --git a/public/img/country_flags/tt.png b/public/img/country_flags/tt.png new file mode 100755 index 0000000..2a11c1e Binary files /dev/null and b/public/img/country_flags/tt.png differ diff --git a/public/img/country_flags/tv.png b/public/img/country_flags/tv.png new file mode 100755 index 0000000..28274c5 Binary files /dev/null and b/public/img/country_flags/tv.png differ diff --git a/public/img/country_flags/tw.png b/public/img/country_flags/tw.png new file mode 100755 index 0000000..f31c654 Binary files /dev/null and b/public/img/country_flags/tw.png differ diff --git a/public/img/country_flags/tz.png b/public/img/country_flags/tz.png new file mode 100755 index 0000000..c00ff79 Binary files /dev/null and b/public/img/country_flags/tz.png differ diff --git a/public/img/country_flags/ua.png b/public/img/country_flags/ua.png new file mode 100755 index 0000000..09563a2 Binary files /dev/null and b/public/img/country_flags/ua.png differ diff --git a/public/img/country_flags/ug.png b/public/img/country_flags/ug.png new file mode 100755 index 0000000..33f4aff Binary files /dev/null and b/public/img/country_flags/ug.png differ diff --git a/public/img/country_flags/um.png b/public/img/country_flags/um.png new file mode 100755 index 0000000..c1dd965 Binary files /dev/null and b/public/img/country_flags/um.png differ diff --git a/public/img/country_flags/us.png b/public/img/country_flags/us.png new file mode 100755 index 0000000..10f451f Binary files /dev/null and b/public/img/country_flags/us.png differ diff --git a/public/img/country_flags/uy.png b/public/img/country_flags/uy.png new file mode 100755 index 0000000..31d948a Binary files /dev/null and b/public/img/country_flags/uy.png differ diff --git a/public/img/country_flags/uz.png b/public/img/country_flags/uz.png new file mode 100755 index 0000000..fef5dc1 Binary files /dev/null and b/public/img/country_flags/uz.png differ diff --git a/public/img/country_flags/va.png b/public/img/country_flags/va.png new file mode 100755 index 0000000..b31eaf2 Binary files /dev/null and b/public/img/country_flags/va.png differ diff --git a/public/img/country_flags/vc.png b/public/img/country_flags/vc.png new file mode 100755 index 0000000..8fa17b0 Binary files /dev/null and b/public/img/country_flags/vc.png differ diff --git a/public/img/country_flags/ve.png b/public/img/country_flags/ve.png new file mode 100755 index 0000000..00c90f9 Binary files /dev/null and b/public/img/country_flags/ve.png differ diff --git a/public/img/country_flags/vg.png b/public/img/country_flags/vg.png new file mode 100755 index 0000000..4156907 Binary files /dev/null and b/public/img/country_flags/vg.png differ diff --git a/public/img/country_flags/vi.png b/public/img/country_flags/vi.png new file mode 100755 index 0000000..ed26915 Binary files /dev/null and b/public/img/country_flags/vi.png differ diff --git a/public/img/country_flags/vn.png b/public/img/country_flags/vn.png new file mode 100755 index 0000000..ec7cd48 Binary files /dev/null and b/public/img/country_flags/vn.png differ diff --git a/public/img/country_flags/vu.png b/public/img/country_flags/vu.png new file mode 100755 index 0000000..b3397bc Binary files /dev/null and b/public/img/country_flags/vu.png differ diff --git a/public/img/country_flags/wf.png b/public/img/country_flags/wf.png new file mode 100755 index 0000000..9f95587 Binary files /dev/null and b/public/img/country_flags/wf.png differ diff --git a/public/img/country_flags/ws.png b/public/img/country_flags/ws.png new file mode 100755 index 0000000..c169508 Binary files /dev/null and b/public/img/country_flags/ws.png differ diff --git a/public/img/country_flags/xx.png b/public/img/country_flags/xx.png new file mode 100755 index 0000000..ba351ca Binary files /dev/null and b/public/img/country_flags/xx.png differ diff --git a/public/img/country_flags/ye.png b/public/img/country_flags/ye.png new file mode 100755 index 0000000..468dfad Binary files /dev/null and b/public/img/country_flags/ye.png differ diff --git a/public/img/country_flags/yt.png b/public/img/country_flags/yt.png new file mode 100755 index 0000000..c298f37 Binary files /dev/null and b/public/img/country_flags/yt.png differ diff --git a/public/img/country_flags/za.png b/public/img/country_flags/za.png new file mode 100755 index 0000000..57c58e2 Binary files /dev/null and b/public/img/country_flags/za.png differ diff --git a/public/img/country_flags/zm.png b/public/img/country_flags/zm.png new file mode 100755 index 0000000..c25b07b Binary files /dev/null and b/public/img/country_flags/zm.png differ diff --git a/public/img/country_flags/zw.png b/public/img/country_flags/zw.png new file mode 100755 index 0000000..53c9725 Binary files /dev/null and b/public/img/country_flags/zw.png differ diff --git a/public/img/default_banner.png b/public/img/default_banner.png new file mode 100644 index 0000000..e7cfb21 Binary files /dev/null and b/public/img/default_banner.png differ diff --git a/public/img/icons/bumplock.png b/public/img/icons/bumplock.png new file mode 100755 index 0000000..2c4e279 Binary files /dev/null and b/public/img/icons/bumplock.png differ diff --git a/public/img/icons/lock.png b/public/img/icons/lock.png new file mode 100755 index 0000000..af561c1 Binary files /dev/null and b/public/img/icons/lock.png differ diff --git a/public/img/icons/loop.png b/public/img/icons/loop.png new file mode 100644 index 0000000..0de2656 Binary files /dev/null and b/public/img/icons/loop.png differ diff --git a/public/img/icons/sticky.png b/public/img/icons/sticky.png new file mode 100755 index 0000000..21bfc96 Binary files /dev/null and b/public/img/icons/sticky.png differ diff --git a/public/img/icons/unlisted.png b/public/img/icons/unlisted.png new file mode 100755 index 0000000..5235753 Binary files /dev/null and b/public/img/icons/unlisted.png differ diff --git a/public/img/logo.png b/public/img/logo.png new file mode 100755 index 0000000..4fa86ea Binary files /dev/null and b/public/img/logo.png differ diff --git a/public/img/spoiler.png b/public/img/spoiler.png new file mode 100755 index 0000000..9eb148b Binary files /dev/null and b/public/img/spoiler.png differ diff --git a/public/img/theme/bg-yotsuba.png b/public/img/theme/bg-yotsuba.png new file mode 100755 index 0000000..b01ef64 Binary files /dev/null and b/public/img/theme/bg-yotsuba.png differ diff --git a/scripts/create_admin.ts b/scripts/create_admin.ts new file mode 100755 index 0000000..77c7fe8 --- /dev/null +++ b/scripts/create_admin.ts @@ -0,0 +1,36 @@ +import { loadConfig } from "../src/config"; +import { initDb, postgres } from "../src/db"; +import { createUser } from "../src/db/user"; +import bcrypt from "bcrypt"; +import { read } from "read"; + +(async () => { + const config = await loadConfig(); + + await initDb(config); + + const username = await read({ + prompt: "Username: ", + }); + + const password = await read({ + prompt: "Password: ", + replace: "*", + silent: true, + }); + + const hashedPassword = await bcrypt.hash(password, 10); + + await createUser( + username, + hashedPassword, + config.roles["admin"].rank, + config.roles["admin"].capcode, + config.roles["admin"].permissions, + true, + ); + + console.info(`Created new admin user '${username}'.`); + + await postgres.end(); +})(); diff --git a/scripts/migrations/01_schema.sql b/scripts/migrations/01_schema.sql new file mode 100755 index 0000000..04b9532 --- /dev/null +++ b/scripts/migrations/01_schema.sql @@ -0,0 +1,76 @@ +CREATE TABLE users ( + username VARCHAR(32) NOT NULL PRIMARY KEY, + password VARCHAR(64) NOT NULL, + rank INT NOT NULL, + capcode VARCHAR(72) NOT NULL, + permissions JSONB NOT NULL, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE boards ( + id VARCHAR(16) NOT NULL PRIMARY KEY, + name VARCHAR(32) NOT NULL, + description VARCHAR(128) NOT NULL, + config JSONB NOT NULL, + created_by VARCHAR(32) NOT NULL, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE posts ( + board VARCHAR(16) NOT NULL REFERENCES boards (id), + id BIGINT NOT NULL, + thread BIGINT DEFAULT NULL, + subject VARCHAR(256) DEFAULT NULL, + name VARCHAR(32) NOT NULL, + user_id VARCHAR(6) NOT NULL, + tripcode VARCHAR(12) DEFAULT NULL, + capcode VARCHAR(72) DEFAULT NULL, + email VARCHAR(256) DEFAULT NULL, + content TEXT NOT NULL, + content_unformatted TEXT NOT NULL, + files JSONB NOT NULL, + password VARCHAR(64) NOT NULL, + ip INET NOT NULL, + bumps INT NOT NULL DEFAULT 0, + replies INT NOT NULL DEFAULT 0, + sticky INT NOT NULL DEFAULT 0, + locked BOOLEAN NOT NULL DEFAULT false, + bumplocked BOOLEAN NOT NULL DEFAULT false, + looped BOOLEAN NOT NULL DEFAULT false, + reported TIMESTAMPTZ DEFAULT NULL, + reports JSONB NOT NULL DEFAULT '[]'::jsonb, + bumped TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + -- Optional fields, non-core functionality + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + UNIQUE (board, id), + PRIMARY KEY (board, id) +); + +CREATE TABLE bans ( + id SERIAL NOT NULL PRIMARY KEY, + ip_range INET NOT NULL, + reason TEXT NOT NULL, + board VARCHAR(16) DEFAULT NULL REFERENCES boards (id), + appealable BOOLEAN NOT NULL, + appeal TEXT DEFAULT NULL, + expires TIMESTAMPTZ DEFAULT NULL, + created_by VARCHAR(32) DEFAULT NULL, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE banners ( + id SERIAL NOT NULL PRIMARY KEY, + banner JSONB NOT NULL, + created_by VARCHAR(32) DEFAULT NULL, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE news ( + id SERIAL NOT NULL PRIMARY KEY, + subject VARCHAR(256) NOT NULL, + content TEXT NOT NULL, + content_unformatted TEXT NOT NULL, + created_by VARCHAR(32) DEFAULT NULL, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..eae8f87 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/cache.ts b/src/cache.ts new file mode 100755 index 0000000..5f0676d --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,89 @@ +import { Config } from "./config"; +import { postgres } from "./db"; +import { User, Banner, Board, Post } from "./db/schema"; +import { createHash } from "crypto"; +import Valkey from "iovalkey"; +import SuperJSON from "superjson"; + +let valkey: Valkey; + +const initCache = async (config: Config) => { + try { + valkey = new Valkey(config.site.valkey); + } catch (err) { + console.error(`Připojení k Valkey selhalo: ${err}`); + process.exitCode = 1; + return; + } + + valkey.on("error", (err) => { + console.error(`Chyba ve Valkey: ${err}`); + }); + + await populateCache(config); +}; + +const populateCache = async (config: Config) => { + await valkey.flushdb(); + await valkey.set("config", JSON.stringify(config)); + + // Fetch data + const users: User[] = (await postgres.query("SELECT * FROM users")).rows; + + const boards: Board[] = ( + await postgres.query("SELECT * FROM boards ORDER BY id ASC") + ).rows; + + const banners: Banner[] = ( + await postgres.query("SELECT * FROM banners ORDER BY id ASC") + ).rows; + + // Cache data + for (const user of users) { + await valkey.hset("users", { + [user.username]: SuperJSON.stringify(user), + }); + } + + for (const board of boards) { + await valkey.hset("boards", { [board.id]: SuperJSON.stringify(board) }); + + if (!(board.config.unlisted || board.config.private)) { + await valkey.rpush("boards:listed", board.id); + } + + // CURRENT aggregates. This will be used to calculate the number of pages and PPH, TPH, PPD, TPD... + + const boardPosts: Post[] = ( + await postgres.query("SELECT * FROM posts WHERE board = $1", [board.id]) + ).rows; + + for (const post of boardPosts) { + const timestamp = post.created.getTime(); + const cacheId = `${board.id}/${post.id}`; + + await valkey.zadd(`posts:total`, timestamp, cacheId); + await valkey.zadd(`posts:board:${board.id}`, timestamp, cacheId); + await valkey.sadd("posters:total", post.ip); + await valkey.sadd(`posters:board:${board.id}`, post.ip); + + const hash = createHash("sha256") + .update(post.content_unformatted) + .digest("hex"); + + await valkey.zadd(`posts:ip:${post.ip}`, timestamp, cacheId); + await valkey.zadd(`posts:content:${hash}`, timestamp, cacheId); + + if (!post.thread) { + await valkey.zadd("threads:total", timestamp, cacheId); + await valkey.zadd(`threads:board:${board.id}`, timestamp, cacheId); + } + } + } + + for (const banner of banners) { + await valkey.zadd("banners", banner.id, SuperJSON.stringify(banner)); + } +}; + +export { valkey, initCache }; diff --git a/src/components/board_list.tsx b/src/components/board_list.tsx new file mode 100755 index 0000000..5ef81b7 --- /dev/null +++ b/src/components/board_list.tsx @@ -0,0 +1,21 @@ +import { TemplateCtx } from "../ctx"; +import { PropsWithChildren } from "@kitajs/html"; + +const BoardLinks = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => { + return ( + <> + [ + {ctx.listedBoards.map((board, index) => ( + <> + + {board} + + {index === ctx.listedBoards.length - 1 ? "" : " / "} + + ))} + ] + + ); +}; + +export default BoardLinks; diff --git a/src/components/capcode.tsx b/src/components/capcode.tsx new file mode 100755 index 0000000..acfdda0 --- /dev/null +++ b/src/components/capcode.tsx @@ -0,0 +1,20 @@ +import { parseCapcode } from "../db/schema"; +import { PropsWithChildren } from "@kitajs/html"; + +const Capcode = ({ capcode }: PropsWithChildren<{ capcode: string }>) => { + const capcodeObj = parseCapcode(capcode); + + return ( + + ## {capcodeObj.text} + {capcodeObj.icon !== null && ( + + )} + + ); +}; + +export default Capcode; diff --git a/src/components/datetime.tsx b/src/components/datetime.tsx new file mode 100755 index 0000000..b2e83be --- /dev/null +++ b/src/components/datetime.tsx @@ -0,0 +1,12 @@ +import { PropsWithChildren } from "@kitajs/html"; + +const Datetime = ({ date }: PropsWithChildren<{ date: Date | null }>) => + date ? ( + + ) : ( + Nikdy + ); + +export default Datetime; diff --git a/src/components/form.tsx b/src/components/form.tsx new file mode 100644 index 0000000..8bb7338 --- /dev/null +++ b/src/components/form.tsx @@ -0,0 +1,48 @@ +import { PropsWithChildren } from "@kitajs/html"; + +const Form = ({ + submit, + action, + center = false, + disabled = false, + children, +}: PropsWithChildren<{ + submit: string; + action: string; + disabled?: boolean; + center?: boolean; +}>) => { + return ( + + + {children} + {disabled ? ( + "" + ) : ( + + + + )} + +
+ +
+ ); +}; + +const FormField = ({ + label, + wrapper = false, + children, +}: PropsWithChildren<{ label: string; wrapper?: boolean }>) => { + return ( + + {label} + {children} + + ); +}; + +export { Form, FormField }; diff --git a/src/components/mod_nav.tsx b/src/components/mod_nav.tsx new file mode 100755 index 0000000..12777a9 --- /dev/null +++ b/src/components/mod_nav.tsx @@ -0,0 +1,43 @@ +import { TemplateCtx } from "../ctx"; +import { canUser, CzchanPerm } from "../permissions"; +import { PropsWithChildren } from "@kitajs/html"; + +const ModNav = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => { + return ( + + ); +}; + +export default ModNav; diff --git a/src/components/page.tsx b/src/components/page.tsx new file mode 100755 index 0000000..15472fa --- /dev/null +++ b/src/components/page.tsx @@ -0,0 +1,12 @@ +import { type PropsWithChildren } from "@kitajs/html"; + +const Page = ({ children }: PropsWithChildren) => { + return ( + <> + {""} + {children} + + ); +}; + +export default Page; diff --git a/src/components/page_body.tsx b/src/components/page_body.tsx new file mode 100755 index 0000000..05815aa --- /dev/null +++ b/src/components/page_body.tsx @@ -0,0 +1,54 @@ +import pjson from "../../package.json"; +import { TemplateCtx } from "../ctx"; +import BoardLinks from "./board_list"; +import { type PropsWithChildren } from "@kitajs/html"; + +const PageBody = ({ + ctx, + container, + children, +}: PropsWithChildren<{ ctx: TemplateCtx; container?: boolean }>) => { + return ( + +
+
+ +
+
+ {container ?
{children}
: children} +
+
+ +
+
+ + ); +}; + +export default PageBody; diff --git a/src/components/page_head.tsx b/src/components/page_head.tsx new file mode 100755 index 0000000..123465e --- /dev/null +++ b/src/components/page_head.tsx @@ -0,0 +1,30 @@ +import { TemplateCtx } from "../ctx"; +import { type PropsWithChildren } from "@kitajs/html"; + +const PageHead = ({ + ctx, + title, + description, + children, +}: PropsWithChildren<{ + ctx: TemplateCtx; + title: string; + description: string; +}>) => { + return ( + + + + {title} + + + + {children} + + ); +}; + +export default PageHead; diff --git a/src/config.ts b/src/config.ts new file mode 100755 index 0000000..0745350 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,99 @@ +import { valkey } from "./cache"; +import { BoardConfig } from "./db/schema"; +import { CzchanError } from "./error"; +import { CzchanPerm } from "./permissions"; +import { readFile, writeFile } from "fs/promises"; + +type Config = { + site: { + server: { + port: number; + }; + postgres: string; + valkey: string; + secrets: { + jwt: string; + }; + globalTheme: string; + }; + boardDefaults: BoardConfig; + roles: { + [key: string]: { + rank: number; + capcode: string; + permissions: CzchanPerm[]; + }; + }; + // We use asset maps so we can pick appropriate resources (sometimes at random) from the backend without unneccesary fs reads. + // As opposed to banners, which are stored in the database, these assets are typically only managed by the site admin. + // A dedicated permission and UI to manage these assets MAY be added in the future. + // BANNERS ARE SOMETHING COMPLETELY DIFFERENT, TRUST ME CHUD + assets: { + capcodeIcons: string[]; + banImages: string[]; + // By status code + errorImages: { [key: string]: string[] }; + memeFlags: string[]; + }; + // Will be used for localization in the future. Currently used for assigning names to things we have a lot of. + locale: { + countries: { [key: string]: string }; + permissions: { [key: string]: string }; + memeFlags: { [key: string]: string }; + }; +}; + +const jsonConf = async (file: string) => { + const buffer = await readFile(file); + const json = buffer.toString(); + const config = JSON.parse(json); + + return config; +}; + +const loadConfig = async (): Promise => { + return { + site: await jsonConf("./config/site.json"), + boardDefaults: await jsonConf("./config/board_defaults.json"), + roles: await jsonConf("./config/roles.json"), + assets: { + capcodeIcons: await jsonConf("./config/assets/capcode_icons.json"), + banImages: await jsonConf("./config/assets/ban_images.json"), + errorImages: await jsonConf("./config/assets/error_images.json"), + memeFlags: await jsonConf("./config/assets/meme_flags.json"), + }, + locale: { + countries: await jsonConf("./config/assets/meme_flags.json"), + permissions: await jsonConf("./config/locale/permissions.json"), + memeFlags: await jsonConf("./config/locale/meme_flags.json"), + }, + }; +}; + +const getConfig = async (): Promise => { + const json = await valkey.get("config"); + + if (!json) { + throw new CzchanError("Konfigurace není načtená", 500); + } + + return JSON.parse(json); +}; + +const setConfig = async (config: Config) => { + await valkey.set("config", JSON.stringify(config)); + + await writeFile("../config/site.json", JSON.stringify(config.site, null, 2)); + await writeFile( + "../config/board_defaults.json", + JSON.stringify(config.boardDefaults, null, 2), + ); + await writeFile( + "../config/roles.json", + JSON.stringify(config.roles, null, 2), + ); + + // Asset maps and locales don't have a web UI yet +}; + +export { Config, loadConfig, getConfig, setConfig }; diff --git a/src/ctx.ts b/src/ctx.ts new file mode 100755 index 0000000..7fc02e6 --- /dev/null +++ b/src/ctx.ts @@ -0,0 +1,39 @@ +import { valkey } from "./cache"; +import { Config } from "./config"; +import { User } from "./db/schema"; +import { readUser } from "./db/user"; +import { Request } from "express"; +import jwt from "jsonwebtoken"; + +// Common object with session data n' sheeit +type TemplateCtx = { + user: User | null; + listedBoards: string[]; + config: Config; +}; + +const getCtx = async ( + req: Request, + attemptAuth: boolean = true, +): Promise => { + const token = req.cookies.auth; + const config = req.config; + + let user = req.user || null; + + if (token && attemptAuth && !user) { + try { + const payload = jwt.verify(token, config.site.secrets.jwt); + const username = payload["sub"] as string; + user = await readUser(username); + } catch { + user = null; + } + } + + const listedBoards = await valkey.lrange("boards:listed", 0, -1); + + return { user, listedBoards, config }; +}; + +export { getCtx, TemplateCtx }; diff --git a/src/db.ts b/src/db.ts new file mode 100755 index 0000000..c783a43 --- /dev/null +++ b/src/db.ts @@ -0,0 +1,25 @@ +import { Config } from "./config"; +import { Client as PgClient } from "pg"; +import { migrate } from "postgres-migrations"; + +let postgres: PgClient; + +const initDb = async (config: Config) => { + postgres = new PgClient({ connectionString: config.site.postgres }); + + try { + await postgres.connect(); + } catch (err) { + console.error(`Připojení k PostgreSQL selhalo: ${err}`); + process.exitCode = 1; + return; + } + + await migrate({ client: postgres }, "./scripts/migrations"); + + postgres.on("error", (err) => { + console.error(`Chyba v PostgreSQL: ${err}`); + }); +}; + +export { postgres, initDb }; diff --git a/src/db/ban.ts b/src/db/ban.ts new file mode 100755 index 0000000..c157eb1 --- /dev/null +++ b/src/db/ban.ts @@ -0,0 +1,54 @@ +import { postgres } from "../db"; +import { Ban } from "./schema"; + +// Creates a ban (bans an IP) +const createBan = async ( + ip_range: string, + board: string | null, + issued_by: string, + appealable: boolean, + expires: Date | null, +) => { + await postgres.query( + "INSERT INTO bans (ip_range, board, issued_by, appealable, expires) VALUES ($1::inet, $2, $3, $4, $5)", + [ip_range, board, issued_by, appealable, expires], + ); +}; + +// "Reads" a ban (checks if an IP is banned) +const readBan = async (ip: string, board: string): Promise => { + const result = await postgres.query( + "SELECT * FROM bans WHERE expired > CURRENT_TIMESTAMP AND ip_range >> $1::inet AND (board = $2 OR board IS NULL)", + [ip, board], + ); + + if (result.rowCount == 0) { + return null; + } else { + return result.rows[0]; + } +}; + +// Reads all bans +const readBans = async (): Promise => { + const result = await postgres.query( + "SELECT * FROM bans ORDER BY created DESC", + ); + + return result.rows; +}; + +// Appeals a ban +const updateBanAppeal = async (id: number, appeal: string) => { + await postgres.query("UPDATE bans SET appeal = $1 WHERE id = $2", [ + id, + appeal, + ]); +}; + +// Deletes a ban (unbans an IP) +const deleteBan = async (id: number) => { + await postgres.query("DELETE FROM bans WHERE id = $1", [id]); +}; + +export { createBan, readBan, readBans, updateBanAppeal, deleteBan }; diff --git a/src/db/banner.ts b/src/db/banner.ts new file mode 100755 index 0000000..c814dac --- /dev/null +++ b/src/db/banner.ts @@ -0,0 +1,44 @@ +import { valkey } from "../cache"; +import { postgres } from "../db"; +import { Banner, File } from "./schema"; +import { QueryResult } from "pg"; +import SuperJSON from "superjson"; + +// Adds a banner +const createBanner = async (file: File) => { + const result: QueryResult = await postgres.query( + "INSERT INTO banners (banner) VALUES ($1) RETURNING *", + [file], + ); + + const banner = result.rows[0]; + + await valkey.zadd("banners", banner.id, SuperJSON.stringify(banner)); +}; + +// Reads all banners +const readBanners = async (): Promise => { + const banners = (await valkey.zrange("banners", "0", "-1")).map( + (json) => SuperJSON.parse(json) as Banner, + ); + + return banners; +}; + +// Reads a random banner +const readRanomBanner = async (): Promise => { + const banner = await valkey.zrandmember("banners"); + + if (!banner) { + return null; + } + + return SuperJSON.parse(banner); +}; + +const deleteBanner = async (id: number) => { + await postgres.query("DELETE FROM banners WHERE id = $1", [id]); + await valkey.zremrangebyscore("banners", id, id); +}; + +export { createBanner, readBanners, readRanomBanner, deleteBanner }; diff --git a/src/db/board.ts b/src/db/board.ts new file mode 100755 index 0000000..f0f5650 --- /dev/null +++ b/src/db/board.ts @@ -0,0 +1,151 @@ +import { valkey } from "../cache"; +import { Config } from "../config"; +import { postgres } from "../db"; +import { CzchanError } from "../error"; +import { deletePost, readAllBoardThreads } from "./post"; +import { Board, BoardConfig } from "./schema"; +import { QueryResult } from "pg"; +import SuperJSON from "superjson"; + +// Creates a board +const createBoard = async ( + config: Config, + id: string, + name: string, + description: string, + setup: boolean = false, +) => { + const result: QueryResult = await postgres.query( + "INSERT INTO boards (id, name, description, config) VALUES ($1, $2, $3, $4) RETURNING *", + [id, name, description, config.boardDefaults], + ); + + // Sequence for post IDs + // I know what I'm doing. Okay, chud? + await postgres.query(`CREATE SEQUENCE posts_${id} AS BIGINT`); + + if (!setup) { + const board = result.rows[0]; + + await valkey.hset("boards", { [board.id]: SuperJSON.stringify(board) }); + + if (!(config.boardDefaults.unlisted || config.boardDefaults.private)) { + await valkey.rpush("boards:listed", board.id); + await valkey.sort("boards:listed", "ALPHA", "ASC"); + } + } +}; + +// Reads a board +const readBoard = async (id: string): Promise => { + const result = await valkey.hget("boards", id); + + if (!result) { + throw new CzchanError(`Nástěnka /${id}/ neexistuje.`, 404); + } + + return SuperJSON.parse(result); +}; + +// Lists all boards +const readBoards = async (): Promise => { + const boards = (await valkey.hvals("boards")) + .map((json) => SuperJSON.parse(json) as Board) + // Alphabetically by ID + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); + + return boards; +}; + +// Gets the next post's ID +const readNextId = async (board: string): Promise => { + const result: QueryResult<{ id: number }> = await postgres.query( + `SELECT id FROM nextval(posts_${board}) AS id`, + ); + + const id = result.rows[0].id; + + return id; +}; + +// Updates the board's name +const updateBoardName = async (id: string, name: string) => { + const result: QueryResult = await postgres.query( + "UPDATE boards SET name = $1 WHERE id = $2 RETURNING *", + [name, id], + ); + + const board = result.rows[0]; + + await valkey.hset("boards", id, SuperJSON.stringify(board)); +}; + +// Updates the board's description +const updateBoardDescription = async (id: string, description: string) => { + const result: QueryResult = await postgres.query( + "UPDATE boards SET description = $1 WHERE id = $2 RETURNING *", + [description, id], + ); + + const board = result.rows[0]; + + await valkey.hset("boards", id, SuperJSON.stringify(board)); +}; + +// Updates the board's config +const updateBoardConfig = async (id: string, config: BoardConfig) => { + const result: QueryResult = await postgres.query( + "UPDATE boards SET config = $1 WHERE id = $2 RETURNING *", + [config, id], + ); + + const board = result.rows[0]; + + await valkey.hset("boards", id, SuperJSON.stringify(board)); + await valkey.lrem("boards:listed", 1, id); + + if (!(config.unlisted || config.private)) { + await valkey.rpush("boards:listed", id); + await valkey.sort("boards:listed", "ALPHA", "ASC"); + } +}; + +// Deletes a board +const deleteBoard = async (id: string) => { + // Delete posts thread by thread, reply by reply, like a heckin' good boy + const posts = await readAllBoardThreads(id); + + for (const post of posts) { + await deletePost(post); + } + + // Purge bans + await postgres.query("DELETE FROM bans WHERE board = $1", [id]); + + // Delete cross-quotes of the board + const quote = `>>>/${id}/`; + await postgres.query("UPDATE posts SET content = replace(content, $1, $2)", [ + quote, + deadQuote, + ]); + + // Delete the board and sequence + await postgres.query("DELETE FROM boards WHERE id = $1", [id]); + await postgres.query(`DROP SEQUENCE posts_${id}`); + + await valkey.hdel("boards", id); + await valkey.del(`posts:board:${id}`); + await valkey.del(`threads:board:${id}`); +}; + +export { + createBoard, + readBoard, + readBoards, + readNextId, + updateBoardName, + updateBoardDescription, + updateBoardConfig, + deleteBoard, +}; diff --git a/src/db/news.ts b/src/db/news.ts new file mode 100755 index 0000000..11d4068 --- /dev/null +++ b/src/db/news.ts @@ -0,0 +1,43 @@ +import { postgres } from "../db"; +import { News } from "./schema"; + +// Adds news +const createNews = async ( + title: string, + content: string, + content_unformatted: string, + author: string, +) => { + await postgres.query( + "INSERT INTO news (title, content, content_unformatted, author) VALUES ($1, $2, $3, $4)", + [title, content, content_unformatted, author], + ); +}; + +// Reads all news +const readNews = async (): Promise => { + const result = await postgres.query( + "SELECT * FROM news ORDER BY created DESC", + ); + + return result.rows; +}; + +// Updates news +const updateNews = async ( + id: number, + content: string, + content_unformatted: string, +) => { + await postgres.query( + "UPDATE news SET content = $1, content_unformatted = $2 WHERE id = $3", + [content, content_unformatted, id], + ); +}; + +// Deletes news +const deleteNews = async (id: number) => { + await postgres.query("DELETE FROM news WHERE id = $1", [id]); +}; + +export { createNews, readNews, updateNews, deleteNews }; diff --git a/src/db/post.ts b/src/db/post.ts new file mode 100755 index 0000000..0fc985f --- /dev/null +++ b/src/db/post.ts @@ -0,0 +1,367 @@ +import { valkey } from "../cache"; +import { postgres } from "../db"; +import { CzchanError } from "../error"; +import { File, Post } from "./schema"; +import { createHash } from "crypto"; +import { QueryResult } from "pg"; + +// Creates a post +const createPost = async ( + board: string, + id: number, + thread: number | null, + subject: string | null, + name: string | null, + user_id: string, + tripcode: string | null, + capcode: string | null, + email: string | null, + content: string, + content_unformatted: string, + files: File[], + password: string, + ip: string, + metadata: object, +) => { + const result: QueryResult = await postgres.query( + "INSERT INTO posts " + + "(board, id, thread, subject, name, user_id, tripcode, capcode, email, content, content_unformatted, files, password, ip, metadata) " + + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING *", + [ + board, + id, + thread, + subject, + name, + user_id, + tripcode, + capcode, + email, + content, + content_unformatted, + files, + password, + ip, + metadata, + ], + ); + + const post = result.rows[0]; + const timestamp = post.created.getTime(); + const cacheId = `${board}/${id}`; + + await valkey.zadd("posts:total", timestamp, cacheId); + await valkey.zadd(`posts:board:${board}`, timestamp, cacheId); + await valkey.sadd("posters:total", ip); + await valkey.sadd(`posters:board:${board}`, ip); + + const hash = createHash("sha256").update(content_unformatted).digest("hex"); + + await valkey.zadd(`posts:ip:${ip}`, timestamp, cacheId); + await valkey.zadd(`posts:content:${hash}`, timestamp, cacheId); + + if (!thread) { + await valkey.zadd("threads:total", timestamp, cacheId); + await valkey.zadd(`threads:board:${board}`, timestamp, cacheId); + } else { + await postgres.query( + "UPDATE posts SET replies = replies + 1 WHERE board = $1 AND id = $2", + [board, thread], + ); + } +}; + +// Reports the post +const createPostReport = async (board: string, id: number, report: Report) => { + await postgres.query( + "UPDATE posts SET reported = CURRENT_TIMESTAMP, reports = reports || $1::jsonb WHERE board = $2 AND id = $3", + [[report], board, id], + ); +}; + +// Reads a post +const readPost = async (board: string, id: number): Promise => { + const result = await postgres.query( + "SELECT * FROM posts WHERE board = $1 AND id = $2", + [board, id], + ); + + if (result.rowCount === 0) { + throw new CzchanError(`Příspěvek >>>/${board}/${id} neexistuje.`, 404); + } + + return result.rows[0]; +}; + +// Reads replies to a thread +const readPostReplies = async (board: string, id: number): Promise => { + const result = await postgres.query( + "SELECT * FROM posts WHERE board = $1 AND thread = $2", + [board, id], + ); + + return result.rows; +}; + +// Reads a page of threads from a board +const readBoardThreads = async ( + board: string, + page: number, +): Promise => { + const result = await postgres.query( + "SELECT * FROM posts WHERE thread IS NULL AND board = $1 ORDER BY sticky DESC, bumped DESC LIMIT 10 OFFSET $2", + [board, (page - 1) * 10], + ); + + return result.rows; +}; + +// Reads all threads from a board, oldest to newest +const readAllBoardThreads = async (board: string): Promise => { + const result = await postgres.query( + "SELECT * FROM posts WHERE thread IS NULL AND board = $1 ORDER BY created DESC", + [board], + ); + + return result.rows; +}; + +// Reads a page of threads from multiple boards +const readOverboardThreads = async ( + page: number, + boards: string[] = [], +): Promise => { + const result = await (boards.length !== 0 + ? postgres.query( + "SELECT * FROM posts WHERE thread IS NULL ORDER BY bumped DESC LIMIT 10 OFFSET $1 WHERE board = ANY($2)", + [(page - 1) * 10, boards], + ) + : postgres.query( + "SELECT * FROM posts WHERE thread IS NULL ORDER BY bumped DESC LIMIT 10 OFFSET $1", + [(page - 1) * 10], + )); + + return result.rows; +}; + +// Reads all threads from a board +const readBoardCatalog = async (board: string): Promise => { + const result = await postgres.query( + "SELECT * FROM posts WHERE thread IS NULL AND board = $1 ORDER BY sticky DESC, bumped DESC", + [board], + ); + + return result.rows; +}; + +// Reads all threads from multiple boards +const readOverboardCatalog = async (boards: string[] = []): Promise => { + const result = await (boards.length !== 0 + ? postgres.query( + "SELECT * FROM posts WHERE thread IS NULL ORDER BY bumped DESC WHERE board = ANY($2)", + [boards], + ) + : postgres.query( + "SELECT * FROM posts WHERE thread IS NULL ORDER BY bumped DESC", + )); + + return result.rows; +}; + +// Reads posts containing a substring on a specific board +const readBoardQueryPosts = async ( + board: string, + query: string, + page: number, +): Promise => { + const result = await postgres.query( + "SELECT * FROM posts WHERE board = $1 AND LOWER(content_unformatted) LIKE LOWER($2) ORDER BY created DESC LIMIT 10 OFFSET $3", + [board, query, (page - 1) * 10], + ); + + return result.rows; +}; + +// Reads posts containing a substring +const readOverboardQueryPosts = async ( + query: string, + page: number, + boards: string[] = [], +): Promise => { + const result = await (boards.length !== 0 + ? postgres.query( + "SELECT * FROM posts WHERE LOWER(content_unformatted) LIKE LOWER($1) ORDER BY created DESC LIMIT 10 OFFSET $2 AND board = ANY($2)", + [query, (page - 1) * 10, boards], + ) + : postgres.query( + "SELECT * FROM posts WHERE LOWER(content_unformatted) LIKE LOWER($1) ORDER BY created DESC LIMIT 10 OFFSET $2", + [query, (page - 1) * 10], + )); + + return result.rows; +}; + +// Reads a page of reported posts +const readReportedPosts = async (page: number): Promise => { + const result = await postgres.query( + "SELECT * FROM posts WHERE reports != '[]'::jsonb ORDER BY jsonb_array_length(reports), reported DESC LIMIT 10 OFFSET $1", + [(page - 1) * 10], + ); + + return result.rows; +}; + +// Reads a page of posts by a specific IP +const readIpPosts = async (ip: string, page: number): Promise => { + const result = await postgres.query( + "SELECT * FROM posts WHERE ip = $1 ORDER BY created DESC LIMIT 10 OFFSET $2", + [ip, (page - 1) * 10], + ); + + return result.rows; +}; + +// Changes the post's content (useful for trolling by jannies) +const updatePostContent = async ( + board: string, + id: number, + content: string, + content_unformatted: string, +) => { + await postgres.query( + "UPDATE posts SET content = $1, content_unformatted = $2 WHERE board = $3 AND id = $4", + [content, content_unformatted, board, id], + ); + + // We're not updating the hash in Redis because that's there for rate limiting plebs +}; + +// Sets the thread's sticky priority +const updatePostSticky = async (board: string, id: number, sticky: number) => { + await postgres.query( + "UPDATE posts SET sticky = $1 WHERE board = $2 AND id = $3", + [sticky, board, id], + ); +}; + +// Locks/unlocks the thread +const updatePostLocked = async (board: string, id: number) => { + await postgres.query( + "UPDATE posts SET locked = NOT locked WHERE board = $1 AND id = $2", + [board, id], + ); +}; + +// Bumplocks/unlocks the thread +const updatePostBumplocked = async (board: string, id: number) => { + await postgres.query( + "UPDATE posts SET bumplocked = NOT bumplocked WHERE board = $1 AND id = $2", + [board, id], + ); +}; + +// Loops/unloops the thread +const updatePostLooped = async (board: string, id: number) => { + await postgres.query( + "UPDATE posts SET looped = NOT looped WHERE board = $1 AND id = $2", + [board, id], + ); +}; + +// Bumps the post +const updatePostBump = async (board: string, id: number) => { + await postgres.query( + "UPDATE posts SET bumps = bumps + 1, bumped = CURRENT_TIMESTAMP WHERE board = $1 AND id = $2", + [board, id], + ); +}; + +// Deletes the post +const deletePost = async (post: Post) => { + const cacheId = `${post.board}/${post.id}`; + + // If this is the OP + if (!post.thread) { + await valkey.zrem("threads:total", cacheId); + await valkey.zrem(`threads:board:${post.board}`, cacheId); + + const replies: QueryResult = await postgres.query( + "SELECT * FROM posts WHERE board = $1 AND thread = $2", + [post.board, post.id], + ); + + for (const reply of replies.rows) { + // Le epic recursion + await deletePost(reply); + } + } + + await postgres.query("DELETE FROM posts WHERE board = $1 AND id = $2", [ + post.board, + post.id, + ]); + + // Deactivate quotes of this post + const quote = `>>${post.id}`; + const deadQuote = `>>${post.id}`; + + await postgres.query("UPDATE posts SET content = replace(content, $1, $2)", [ + quote, + deadQuote, + ]); + + // Deactivate cross-quotes of this post + const crossQuote = `>>$gt/${post.board}/${post.id}/`; + const deadCrossQuote = `>>$gt;/${post.board}/${post.id}`; + + await postgres.query("UPDATE posts SET content = replace(content, $1, $2)", [ + crossQuote, + deadCrossQuote, + ]); + + await valkey.zrem("posts:total", cacheId); + await valkey.zrem(`posts:board:${post.board}`, cacheId); + + const hash = createHash("sha256") + .update(post.content_unformatted) + .digest("hex"); + + await valkey.zrem(`posts:ip:${post.ip}`, cacheId); + await valkey.zrem(`posts:content:${hash}`, cacheId); +}; + +// Deletes reports +const deletePostReports = async (board: string, id: number) => { + await postgres.query( + "UPDATE posts SET reports = '[]'::jsonb WHERE board = $1 AND id = $2", + [board, id], + ); +}; + +export { + createPost, + createPostReport, + readPost, + readPostReplies, + readBoardThreads, + readAllBoardThreads, + readOverboardThreads, + readBoardCatalog, + readOverboardCatalog, + readBoardQueryPosts, + readOverboardQueryPosts, + readReportedPosts, + readIpPosts, + updatePostContent, + updatePostSticky, + updatePostLocked, + updatePostBumplocked, + updatePostLooped, + updatePostBump, + deletePost, + deletePostReports, +}; diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100755 index 0000000..42b7879 --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,133 @@ +import { CzchanPerm } from "../permissions"; + +// JSON types +type File = { + original_filename: string; + url: string; + size: number; + width: number; + height: number; + format: string; + hashes: { + [key: string]: string; + }; + thumb: { + url: string; + size: number; + width: number; + height: number; + format: string; + } | null; +}; + +type BoardConfig = { + // Non-exhaustive + private: boolean; + unlisted: boolean; +}; + +// Schema +type User = { + username: string; + password: string; + rank: number; + capcode: string; + permissions: CzchanPerm[]; + created: Date; +}; + +type Board = { + id: string; + name: string; + description: string; + config: BoardConfig; + created_by: string; + created: Date; +}; + +type Post = { + board: string; + id: number; + thread: number | null; + name: string | null; + user_id: string; + tripcode: string | null; + capcode: string | null; + email: string | null; + content: string; + content_unformatted: string; + files: File[]; + password: string; + ip: string; + bumps: number; + replies: number; + sticky: number; + locked: boolean; + bumplocked: boolean; + looped: boolean; + reported: Date; + reports: { reason: string; ip: string }[]; + bumped: Date; + created: Date; + metadata: object; +}; + +type Ban = { + id: number; + ip_range: string; + reason: string; + board: string | null; + appealable: boolean; + appeal: string | null; + expires: Date | null; + created_by: string; + created: Date; +}; + +type Banner = { + id: number; + banner: File; + created_by: string; + created: Date; +}; + +type News = { + id: number; + subject: string; + content: string; + content_unformatted: string; + created_by: string; + created: Date; +}; + +// Utility +const encodeCapcode = (capcode: { + text: string; + color: string; + icon: string | null; +}): string => { + if (capcode.icon) { + return `${capcode.text}:${capcode.color}:${capcode.icon}`; + } else { + return `${capcode.text}:${capcode.color}`; + } +}; + +const parseCapcode = ( + capcode: string, +): { + text: string; + color: string; + icon: string | null; +} => { + const params = capcode.split(":"); + + return { + text: params[0], + color: params[1], + icon: params[2] || null, + }; +}; + +export type { File, BoardConfig, User, Board, Post, Ban, Banner, News }; +export { encodeCapcode, parseCapcode }; diff --git a/src/db/user.ts b/src/db/user.ts new file mode 100755 index 0000000..c7d4924 --- /dev/null +++ b/src/db/user.ts @@ -0,0 +1,120 @@ +import { valkey } from "../cache"; +import { postgres } from "../db"; +import { CzchanError } from "../error"; +import { CzchanPerm } from "../permissions"; +import { User } from "./schema"; +import { QueryResult } from "pg"; +import SuperJSON from "superjson"; + +// Creates an user (bcrypt-hashed password) +const createUser = async ( + username: string, + password: string, + rank: number, + capcode: string, + permissions: CzchanPerm[], + setup: boolean = false, +) => { + const result: QueryResult = await postgres.query( + "INSERT INTO users (username, password, rank, capcode, permissions) VALUES ($1, $2, $3, $4, $5) RETURNING *", + [username, password, rank, capcode, JSON.stringify(permissions)], + ); + + if (!setup) { + const user = result.rows[0]; + await valkey.hset("users", { + [user.username]: SuperJSON.stringify(user), + }); + } +}; + +// Reads an user +const readUser = async (username: string): Promise => { + const result = await valkey.hget("users", username); + + if (!result) { + throw new CzchanError(`Uživatel "${username}" neexistuje.`, 404); + } + + return SuperJSON.parse(result); +}; + +// Lists all users +const readUsers = async (): Promise => { + const users = (await valkey.hvals("users")) + .map((json) => SuperJSON.parse(json) as User) + .sort( + // rank ASC, created ASC + (a, b) => a.rank - b.rank || a.created.getTime() - b.created.getTime(), + ); + + return users; +}; + +// Updates the user's rank +const updateUserRank = async (username: string, rank: number) => { + const result = await postgres.query( + "UPDATE users SET rank = $1 WHERE username = $2 RETURNING *", + [rank, username], + ); + + const user = result.rows[0]; + + await valkey.hset("users", username, SuperJSON.stringify(user)); +}; + +// Updates the user's capcode +const updateUserCapcode = async (username: string, capcode: string) => { + const result = await postgres.query( + "UPDATE users SET capcode = $1 WHERE username = $2 RETURNING *", + [capcode, username], + ); + + const user = result.rows[0]; + + await valkey.hset("users", username, SuperJSON.stringify(user)); +}; + +// Updates the user's permissions +const updateUserPermissions = async ( + username: string, + permissions: CzchanPerm[], +) => { + const result = await postgres.query( + "UPDATE users SET permissions = $1 WHERE username = $2 RETURNING *", + [JSON.stringify(permissions), username], + ); + + const user = result.rows[0]; + + await valkey.hset("users", username, SuperJSON.stringify(user)); +}; + +// Changes the password (bcrypt) +const updateUserPassword = async (username: string, newPassword: string) => { + const result: QueryResult = await postgres.query( + "UPDATE users SET password = $1 WHERE username = $2 RETURNING *", + [newPassword, username], + ); + + const user = result.rows[0]; + + await valkey.hset("users", username, SuperJSON.stringify(user)); +}; + +// Deletes an user +const deleteUser = async (username: string) => { + await postgres.query("DELETE FROM users WHERE username = $1", [username]); + await valkey.hdel("users", username); +}; + +export { + createUser, + readUser, + readUsers, + updateUserRank, + updateUserCapcode, + updateUserPermissions, + updateUserPassword, + deleteUser, +}; diff --git a/src/error.tsx b/src/error.tsx new file mode 100755 index 0000000..060f264 --- /dev/null +++ b/src/error.tsx @@ -0,0 +1,73 @@ +import Page from "./components/page"; +import PageBody from "./components/page_body"; +import PageHead from "./components/page_head"; +import { getCtx, TemplateCtx } from "./ctx"; +import { NextFunction, Request, Response } from "express"; +import z from "zod"; + +class CzchanError extends Error { + statusCode: number; + + constructor(message: string, statusCode: number) { + super(message); + this.name = "CzchanError"; + this.statusCode = statusCode; + } +} + +const ErrorPage = (ctx: TemplateCtx, errors: CzchanError[]) => ( + + + +
+
Chyba {errors[0].statusCode}
+
+ {errors.map((err) => ( +

{err.message}

+ ))} +
+
+
+
+); + +// Error wrapper, used many times. We're just gonna call it e, short for error. +// THIS IS THE ONLY ONE LETTER NAME I'LL USE HERE I PROMISE +// Turning this into middleware doesn't work so this is the second best thing +const e = + (fn: (req: Request, res: Response, next: NextFunction) => Promise) => + async (req: Request, res: Response, next: NextFunction) => { + try { + await fn(req, res, next); + } catch (err) { + const ctx = await getCtx(req); + + if (err instanceof CzchanError) { + // Native error + const html = ErrorPage(ctx, [err]); + res.status(err.statusCode).send(html); + } else if (err instanceof z.ZodError) { + // Zod error + const errors = err.issues.map( + (issue) => new CzchanError(issue.message, 400), + ); + + const html = ErrorPage(ctx, errors); + res.status(400).send(html); + } else { + // What the fuck + console.error("Interní chyba serveru", err); + + const html = ErrorPage(ctx, [ + new CzchanError( + "Interní chyba serveru. Jestli přetrvává, kontaktuj administrátora.", + 500, + ), + ]); + + res.status(500).send(html); + } + } + }; + +export { CzchanError, ErrorPage, e }; diff --git a/src/main.ts b/src/main.ts new file mode 100755 index 0000000..fc2de8b --- /dev/null +++ b/src/main.ts @@ -0,0 +1,42 @@ +import { initCache } from "./cache"; +import { loadConfig } from "./config"; +import { getCtx } from "./ctx"; +import { initDb } from "./db"; +import { CzchanError, ErrorPage } from "./error"; +import { routes } from "./routes"; +import cookieParser from "cookie-parser"; +import express, { Request, Response } from "express"; + +const main = async () => { + const initialConfig = await loadConfig(); + + await initDb(initialConfig); + await initCache(initialConfig); + + const app = express(); + + app.use("/public", express.static("./public")); + app.use("/LICENSE.txt", express.static("./LICENSE.txt")); + app.use("/favicon.ico", express.static("./public/favicon.ico")); + app.use(cookieParser()); + + const router = await routes(); + app.use("/", router); + + app.use(async (req: Request, res: Response) => { + const ctx = await getCtx(req, false); + const html = ErrorPage(ctx, [ + new CzchanError("Tato stránka neexistuje.", 404), + ]); + + res.status(404).send(html); + }); + + app.listen(initialConfig.site.server.port, () => { + console.info( + `Server běží na http://localhost:${initialConfig.site.server.port}`, + ); + }); +}; + +main(); diff --git a/src/permissions.ts b/src/permissions.ts new file mode 100755 index 0000000..f44a5f8 --- /dev/null +++ b/src/permissions.ts @@ -0,0 +1,77 @@ +import { User } from "./db/schema"; + +enum CzchanPerm { + // Admin content permissions + MANAGE_USERS = "MANAGE_USERS", + MANAGE_BANNERS = "MANAGE_BANNERS", + ALL_BANNERS = "ALL_BANNERS", + MANAGE_BANS = "MANAGE_BANS", + ALL_BANS = "ALL_BANS", + MANAGE_BOARDS = "MANAGE_BOARDS", + ALL_BOARDS = "ALL_BOARDS", + MANAGE_NEWS = "MANAGE_NEWS", + ALL_NEWS = "ALL_NEWS", + // Post permissions + MANAGE_REPORTS = "MANAGE_REPORTS", + MANAGE_POSTS = "MANAGE_POSTS", + DELETE_POSTS = "DELETE_POSTS", + DELETE_FILES = "DELETE_FILES", + SPOILER_POSTS = "SPOILER_POSTS", + MOVE_POSTS = "MOVE_POSTS", + EDIT_POSTS = "EDIT_POSTS", + // Cognitohazards + VIEW_IPS = "VIEW_IPS", + VIEW_LOGS = "VIEW_LOGS", + VIEW_METADATA = "VIEW_METADATA", + VIEW_PRIVATE_BOARDS = "VIEW_PRIVATE_BOARDS", + // Capcode + CAPCODE = "CAPCODE", + CUSTOM_CAPCODE = "CUSTOM_CAPCODE", + // Bypasses + BYPASS_BAN = "BYPASS_BAN", + BYPASS_LOCK = "BYPASS_LOCK", + BYPASS_BLOCK = "BYPASS_BLOCK", + BYPASS_FILTER = "BYPASS_FILTER", + BYPASS_RATELIMIT = "BYPASS_RATELIMIT", + // For convenience + ALL_FIELDS = "ALL_FIELDS", + ADVANCED_MARKUP = "ADVANCED_MARKUP", + MANAGE_SITE_ASSETS = "MANAGE_SITE_ASSETS", + MANAGE_SITE_CONFIG = "MANAGE_SITE_CONFIG", +} + +const canUser = (user: User | null, perm: CzchanPerm): boolean => { + if (!user) { + return false; + } + + return user.rank === 0 || user.permissions.includes(perm); +}; + +const canUserApplyRole = ( + user: User | null, + role: { rank: number; capcode: string; permissions: CzchanPerm[] }, +) => { + if (!user) { + return false; + } + + return ( + user.rank === 0 || + (permSubset(user.permissions || [], role.permissions) && + user.rank < role.rank) + ); +}; + +const canUserActOn = (sub: User | null, obj: User): boolean => { + if (!sub) { + return false; + } + + return sub.rank === 0 || sub.rank < obj.rank; +}; + +const permSubset = (array: CzchanPerm[], target: CzchanPerm[]): boolean => + target.every((v) => array.includes(v)); + +export { CzchanPerm, canUser, canUserActOn, canUserApplyRole, permSubset }; diff --git a/src/routes.ts b/src/routes.ts new file mode 100755 index 0000000..c12d775 --- /dev/null +++ b/src/routes.ts @@ -0,0 +1,109 @@ +import { Config, getConfig } from "./config"; +import { User } from "./db/schema"; +import { readUser } from "./db/user"; +import { CzchanError, e } from "./error"; +import actionLogin from "./routes/actions/login"; +import actionLogout from "./routes/actions/logout"; +import index from "./routes/index"; +import login from "./routes/login"; +import modActionBoardActions from "./routes/mod/actions/board_actions"; +import modActionCreateBoard from "./routes/mod/actions/create_board"; +import modActionCreateUser from "./routes/mod/actions/create_user"; +import modActionUpdatePassword from "./routes/mod/actions/update_password"; +import modActionUpdatePermissions from "./routes/mod/actions/update_permissions"; +import modActionUserActions from "./routes/mod/actions/user_actions"; +import modBanners from "./routes/mod/banners"; +import modBans from "./routes/mod/bans"; +import modBoards from "./routes/mod/boards"; +import modIndex from "./routes/mod/index"; +import modLogs from "./routes/mod/logs"; +import modNews from "./routes/mod/news"; +import modPermissions from "./routes/mod/permissions"; +import modReports from "./routes/mod/reports"; +import modUsers from "./routes/mod/users"; +import { NextFunction, Request, Response, Router } from "express"; +import jwt from "jsonwebtoken"; + +const routes = async () => { + const router = Router(); + + // Middleware + router.use(e(configMiddleware)); + router.use("/mod/*splat", e(authMiddleware)); + // User display routes + router.get("/", e(index)); + router.get("/login", e(login)); + // User action routes + router.post("/actions/login", e(actionLogin)); + router.get("/actions/logout", e(actionLogout)); + // Mod display routes + router.get("/mod/", e(modIndex)); + router.get("/mod/users", e(modUsers)); + router.get("/mod/banners", e(modBanners)); + router.get("/mod/bans", e(modBans)); + router.get("/mod/boards", e(modBoards)); + router.get("/mod/reports", e(modReports)); + router.get("/mod/news", e(modNews)); + router.get("/mod/logs", e(modLogs)); + router.get("/mod/permissions/:user", e(modPermissions)); + // Mod action routes + router.post("/mod/actions/update_password", e(modActionUpdatePassword)); + router.post("/mod/actions/create_user", e(modActionCreateUser)); + router.post("/mod/actions/user_actions", e(modActionUserActions)); + router.post("/mod/actions/create_board", e(modActionCreateBoard)); + router.post("/mod/actions/board_actions", e(modActionBoardActions)); + router.post("/mod/actions/update_permissions", e(modActionUpdatePermissions)); + + return router; +}; + +declare global { + namespace Express { + interface Request { + user: User | null; + config: Config; + } + } +} + +const configMiddleware = async ( + req: Request, + _: Response, + next: NextFunction, +) => { + req.config = await getConfig(); + + next(); +}; + +const authMiddleware = async ( + req: Request, + _: Response, + next: NextFunction, +) => { + const token = req.cookies.auth; + const config = req.config; + + let user = null; + + if (!token) { + throw new CzchanError( + "Pro přístup k této stránce se musíš přihlásit.", + 401, + ); + } + + try { + const data = jwt.verify(token, config.site.secrets.jwt); + const username = data["sub"] as string; + user = await readUser(username); + } catch { + throw new CzchanError("Neplatný token. Zkus se znovu přihlásit.", 401); + } + + req.user = user; + + next(); +}; + +export { routes }; diff --git a/src/routes/actions/login.ts b/src/routes/actions/login.ts new file mode 100755 index 0000000..06bd407 --- /dev/null +++ b/src/routes/actions/login.ts @@ -0,0 +1,25 @@ +import { readUser } from "../../db/user"; +import { CzchanError } from "../../error"; +import { LoginForm } from "../../zod_schema"; +import bcrypt from "bcrypt"; +import { Request, Response } from "express"; +import formidable from "formidable"; +import jwt from "jsonwebtoken"; + +export default async (req: Request, res: Response) => { + const form = formidable({}); + const [fields, _] = await form.parse(req); + + const { username, password } = LoginForm.parse(fields); + + const user = await readUser(username); + const matches = await bcrypt.compare(password, user.password); + + if (!matches) { + throw new CzchanError("Nesprávné heslo.", 401); + } + + const token = jwt.sign({ sub: username }, req.config.site.secrets.jwt); + + res.cookie("auth", token).redirect("/mod/"); +}; diff --git a/src/routes/actions/logout.ts b/src/routes/actions/logout.ts new file mode 100755 index 0000000..a9f4c1b --- /dev/null +++ b/src/routes/actions/logout.ts @@ -0,0 +1,5 @@ +import { Request, Response } from "express"; + +export default async (_: Request, res: Response) => { + res.clearCookie("auth").redirect("/"); +}; diff --git a/src/routes/index.tsx b/src/routes/index.tsx new file mode 100755 index 0000000..9ac7b88 --- /dev/null +++ b/src/routes/index.tsx @@ -0,0 +1,30 @@ +import Page from "../components/page"; +import PageBody from "../components/page_body"; +import PageHead from "../components/page_head"; +import { TemplateCtx, getCtx } from "../ctx"; +import { Request, Response } from "express"; + +export default async (req: Request, res: Response) => { + const ctx = await getCtx(req); + const html = HomePage(ctx); + + res.send(html); +}; + +const HomePage = (ctx: TemplateCtx) => { + return ( + + + +
+ +

Český anonymní imageboard

+
+
+
+ ); +}; diff --git a/src/routes/login.tsx b/src/routes/login.tsx new file mode 100755 index 0000000..34fa440 --- /dev/null +++ b/src/routes/login.tsx @@ -0,0 +1,34 @@ +import { Form, FormField } from "../components/form"; +import Page from "../components/page"; +import PageBody from "../components/page_body"; +import PageHead from "../components/page_head"; +import { getCtx, TemplateCtx } from "../ctx"; +import { Request, Response } from "express"; + +export default async (req: Request, res: Response) => { + const ctx = await getCtx(req); + const html = Template(ctx); + + res.send(html); +}; + +const Template = (ctx: TemplateCtx) => { + return ( + + + +

Přihlásit se

+
+ + + + + + + +
+ +
+
+ ); +}; diff --git a/src/routes/mod/actions/board_actions.ts b/src/routes/mod/actions/board_actions.ts new file mode 100644 index 0000000..131ce51 --- /dev/null +++ b/src/routes/mod/actions/board_actions.ts @@ -0,0 +1,88 @@ +import { deleteBoard, readBoard, updateBoardName } from "../../../db/board"; +import { Board } from "../../../db/schema"; +import { CzchanError } from "../../../error"; +import { canUser, CzchanPerm } from "../../../permissions"; +import { BoardActionsForm, UpdateBoardForm } from "../../../zod_schema"; +import { Request, Response } from "express"; +import formidable from "formidable"; + +export default async (req: Request, res: Response) => { + if (canUser(req.user, CzchanPerm.MANAGE_BOARDS)) { + throw new CzchanError("K tomuto nemáš oprávnění.", 403); + } + + const form = formidable({}); + const [fields, _] = await form.parse(req); + const { boards, action } = BoardActionsForm.parse(fields); + + const boards_ = await Promise.all( + boards.map(async (id) => await readBoard(id)), + ); + + switch (action) { + case "$delete_board": + await deleteBoards(req, boards_); + break; + case "$update_board": + const { name, description } = UpdateBoardForm.parse(fields); + await updateBoards(req, boards_, name, description); + break; + } + + res.redirect("/mod/boards"); +}; + +const deleteBoards = async (req: Request, boards: Board[]) => { + // Validation + for (const board of boards) { + if ( + !( + req.user?.username === board.created_by || + canUser(req.user, CzchanPerm.ALL_BOARDS) + ) + ) { + throw new CzchanError( + `K odstranění nástěnky /${board.id}/ nemáš oprávnění.`, + 403, + ); + } + } + + // Action + for (const board of boards) { + await deleteBoard(board.id); + } +}; + +const updateBoards = async ( + req: Request, + boards: Board[], + name: string | null, + description: string | null, +) => { + // Validation + for (const board of boards) { + if ( + !( + req.user?.username === board.created_by || + canUser(req.user, CzchanPerm.ALL_BOARDS) + ) + ) { + throw new CzchanError( + `K upravení nástěnky /${board.id}/ nemáš oprávnění.`, + 403, + ); + } + } + + // Action + for (const board of boards) { + if (name) { + await updateBoardName(board.id, name); + } + + if (description) { + await updateBoardName(board.id, description); + } + } +}; diff --git a/src/routes/mod/actions/create_board.ts b/src/routes/mod/actions/create_board.ts new file mode 100644 index 0000000..234c8cc --- /dev/null +++ b/src/routes/mod/actions/create_board.ts @@ -0,0 +1,29 @@ +import { createBoard } from "../../../db/board"; +import { CzchanError } from "../../../error"; +import { canUser, CzchanPerm } from "../../../permissions"; +import { CreateBoardForm } from "../../../zod_schema"; +import { Request, Response } from "express"; +import formidable from "formidable"; +import { DatabaseError } from "pg"; + +export default async (req: Request, res: Response) => { + if (!canUser(req.user, CzchanPerm.MANAGE_BOARDS)) { + throw new CzchanError("K tomuto nemáš oprávnění.", 403); + } + + const form = formidable({}); + const [fields, _] = await form.parse(req); + const { id, name, description } = CreateBoardForm.parse(fields); + + try { + await createBoard(req.config, id, name, description); + } catch (e) { + if ((e as DatabaseError).constraint === "users_pkey") { + throw new CzchanError(`Nástěnka /${id}/ již existuje.`, 409); + } else { + throw e; + } + } + + res.redirect("/mod/boards"); +}; diff --git a/src/routes/mod/actions/create_user.ts b/src/routes/mod/actions/create_user.ts new file mode 100644 index 0000000..308c5a1 --- /dev/null +++ b/src/routes/mod/actions/create_user.ts @@ -0,0 +1,49 @@ +import { createUser } from "../../../db/user"; +import { CzchanError } from "../../../error"; +import { canUser, canUserApplyRole, CzchanPerm } from "../../../permissions"; +import { CreateUserForm } from "../../../zod_schema"; +import bcrypt from "bcrypt"; +import { Request, Response } from "express"; +import formidable from "formidable"; +import { DatabaseError } from "pg"; + +export default async (req: Request, res: Response) => { + if (!canUser(req.user, CzchanPerm.MANAGE_USERS)) { + throw new CzchanError("K tomuto nemáš oprávnění.", 403); + } + + const form = formidable({}); + const [fields, _] = await form.parse(req); + const { new_username, new_password, new_role } = CreateUserForm.parse(fields); + + const username_ = new_username; + const role_ = req.config.roles[new_role]; + + if (!role_) { + throw new CzchanError("Neplatná role.", 400); + } + + if (!canUserApplyRole(req.user, role_)) { + throw new CzchanError("K aplikaci této role nemáš oprávnění.", 403); + } + + const password_ = await bcrypt.hash(new_password, 10); + + try { + await createUser( + username_, + password_, + role_.rank, + role_.capcode, + role_.permissions, + ); + } catch (e) { + if ((e as DatabaseError).constraint === "users_pkey") { + throw new CzchanError(`Uživatel "${username_}" již existuje.`, 409); + } else { + throw e; + } + } + + res.redirect("/mod/users"); +}; diff --git a/src/routes/mod/actions/update_password.ts b/src/routes/mod/actions/update_password.ts new file mode 100644 index 0000000..0906337 --- /dev/null +++ b/src/routes/mod/actions/update_password.ts @@ -0,0 +1,27 @@ +import { User } from "../../../db/schema"; +import { updateUserPassword } from "../../../db/user"; +import { CzchanError } from "../../../error"; +import { UpdatePasswordForm } from "../../../zod_schema"; +import bcrypt from "bcrypt"; +import { Request, Response } from "express"; +import formidable from "formidable"; + +export default async (req: Request, res: Response) => { + const form = formidable({}); + const [fields, _] = await form.parse(req); + + const { old_password, new_password } = UpdatePasswordForm.parse(fields); + + const user = req.user as User; + const matches = await bcrypt.compare(old_password, user.password); + + if (!matches) { + throw new CzchanError("Nesprávné heslo.", 401); + } + + const new_password_ = await bcrypt.hash(new_password, 10); + + await updateUserPassword(user.username, new_password_); + + res.redirect("/mod/"); +}; diff --git a/src/routes/mod/actions/update_permissions.ts b/src/routes/mod/actions/update_permissions.ts new file mode 100644 index 0000000..3695830 --- /dev/null +++ b/src/routes/mod/actions/update_permissions.ts @@ -0,0 +1,46 @@ +import { readUser, updateUserPermissions } from "../../../db/user"; +import { CzchanError } from "../../../error"; +import { + canUser, + canUserActOn, + CzchanPerm, + permSubset, +} from "../../../permissions"; +import { UpdatePermissionsForm } from "../../../zod_schema"; +import { Request, Response } from "express"; +import formidable from "formidable"; + +export default async (req: Request, res: Response) => { + if (!canUser(req.user, CzchanPerm.MANAGE_USERS)) { + throw new CzchanError("K tomuto nemáš oprávnění.", 403); + } + + const form = formidable({}); + const [fields, _] = await form.parse(req); + const { user, permissions } = UpdatePermissionsForm.parse(fields); + + const user_ = await readUser(user); + + if ( + !( + req.user?.rank !== 0 || + permSubset(req.user?.permissions || [], permissions) + ) + ) { + throw new CzchanError( + `Aplikovaná oprávnění nesmí obsahovat jiná oprávnění, než tvá vlastní.`, + 403, + ); + } + + if (!canUserActOn(req.user, user_)) { + throw new CzchanError( + `K upravení oprávnění uživatele "${user_.username}" nemáš oprávnění.`, + 403, + ); + } + + await updateUserPermissions(user_.username, permissions); + + res.redirect(`/mod/users`); +}; diff --git a/src/routes/mod/actions/user_actions.ts b/src/routes/mod/actions/user_actions.ts new file mode 100644 index 0000000..64c7bee --- /dev/null +++ b/src/routes/mod/actions/user_actions.ts @@ -0,0 +1,213 @@ +import { encodeCapcode, parseCapcode, User } from "../../../db/schema"; +import { + deleteUser, + readUser, + updateUserCapcode, + updateUserPermissions, + updateUserRank, +} from "../../../db/user"; +import { CzchanError } from "../../../error"; +import { + canUser, + canUserActOn, + canUserApplyRole, + CzchanPerm, +} from "../../../permissions"; +import { + ApplyRoleForm, + UpdateCapcodeForm, + UpdateRankForm, + UserActionsForm, +} from "../../../zod_schema"; +import { Request, Response } from "express"; +import formidable from "formidable"; + +export default async (req: Request, res: Response) => { + const form = formidable({}); + const [fields, _] = await form.parse(req); + const { users, action } = UserActionsForm.parse(fields); + + const users_ = await Promise.all( + users.map(async (username) => await readUser(username)), + ); + + let selfDeletion = false; + + switch (action) { + case "$delete_user": + selfDeletion = await deleteUsers(req, users_); + break; + case "$update_rank": + const { rank } = UpdateRankForm.parse(fields); + await updateRank(req, users_, rank); + break; + case "$update_capcode": + const { capcode_text, capcode_color, capcode_icon } = + UpdateCapcodeForm.parse(fields); + + await updateCapcode( + req, + users_, + capcode_text, + capcode_color, + capcode_icon, + ); + break; + case "$apply_role": + const { role } = ApplyRoleForm.parse(fields); + await applyRole(req, users_, role); + break; + } + + if (selfDeletion) { + res.redirect("/mod/logout"); + } else { + res.redirect("/mod/users"); + } +}; + +const deleteUsers = async (req: Request, users: User[]): Promise => { + // Validation + for (const user of users) { + if ( + !( + (canUser(req.user, CzchanPerm.MANAGE_USERS) && + canUserActOn(req.user, user)) || + req.user?.username === user.username + ) + ) { + throw new CzchanError( + `K odstranění užvatele "${user.username}" nemáš oprávnění.`, + 403, + ); + } + } + + // Action + let selfDeletion = false; + + for (const user of users) { + if (req.user?.username === user.username) { + selfDeletion = true; + } + + await deleteUser(user.username); + } + + return selfDeletion; +}; + +const updateRank = async (req: Request, users: User[], rank: number) => { + // Validation + if (req.user?.rank === undefined || (rank !== 0 && req.user?.rank >= rank)) { + throw new CzchanError( + `Aplikovaná úroveň musí být nižší (numericky větší) než tvá vlastní.`, + 403, + ); + } + + for (const user of users) { + if ( + !( + canUser(req.user, CzchanPerm.MANAGE_USERS) && + canUserActOn(req.user, user) + ) + ) { + throw new CzchanError( + `K upravení úrovně uživatele "${user.username}" nemáš oprávnění.`, + 403, + ); + } + } + + // Action + for (const user of users) { + await updateUserRank(user.username, rank); + } +}; + +const updateCapcode = async ( + req: Request, + users: User[], + capcode_text: string | null, + capcode_color: string | null, + capcode_icon: string | null, +) => { + // Validation + for (const user of users) { + if ( + !( + (canUser(req.user, CzchanPerm.MANAGE_USERS) && + canUserActOn(req.user, user)) || + (canUser(req.user, CzchanPerm.CUSTOM_CAPCODE) && + req.user?.username === user.username) + ) + ) { + throw new CzchanError( + `K upravení capcodu uživatele "${user.username}" nemáš oprávnění.`, + 403, + ); + } + } + + // Action + for (const user of users) { + let { text, color, icon } = parseCapcode(user.capcode); + + if (capcode_text) { + text = capcode_text; + } + + if (capcode_color) { + color = capcode_color; + } + + if (!capcode_icon || capcode_icon === "$keep") { + icon = icon; + } else if (capcode_icon === "$none") { + icon = null; + } else { + icon = capcode_icon; + } + + const capcode = encodeCapcode({ text, color, icon }); + + await updateUserCapcode(user.username, capcode); + } +}; + +const applyRole = async (req: Request, users: User[], role: string) => { + // Validation + let role_ = req.config.roles[role]; + + if (!role_) { + throw new CzchanError("Neplatná role.", 400); + } + + if (!canUserApplyRole(req.user, role_)) { + throw new CzchanError("K aplikaci této role nemáš oprávnění.", 403); + } + + const { rank, capcode, permissions } = role_; + + for (const user of users) { + if ( + !( + canUser(req.user, CzchanPerm.MANAGE_USERS) && + canUserActOn(req.user, user) + ) + ) { + throw new CzchanError( + `K aplikaci rolí na uživatele "${user.username}" nemáš oprávnění.`, + 403, + ); + } + } + + // Action + for (const user of users) { + await updateUserRank(user.username, rank); + await updateUserCapcode(user.username, capcode); + await updateUserPermissions(user.username, permissions); + } +}; diff --git a/src/routes/mod/banners.tsx b/src/routes/mod/banners.tsx new file mode 100644 index 0000000..16719f1 --- /dev/null +++ b/src/routes/mod/banners.tsx @@ -0,0 +1,123 @@ +import Datetime from "../../components/datetime"; +import { Form, FormField } from "../../components/form"; +import ModNav from "../../components/mod_nav"; +import Page from "../../components/page"; +import PageBody from "../../components/page_body"; +import PageHead from "../../components/page_head"; +import { getCtx, TemplateCtx } from "../../ctx"; +import { readBanners } from "../../db/banner"; +import { Banner } from "../../db/schema"; +import { CzchanError } from "../../error"; +import { canUser, CzchanPerm } from "../../permissions"; +import { PropsWithChildren } from "@kitajs/html"; +import { Request, Response } from "express"; + +export default async (req: Request, res: Response) => { + const ctx = await getCtx(req); + + if (!canUser(ctx.user, CzchanPerm.MANAGE_BANNERS)) { + throw new CzchanError("K tomuto nemáš oprávnění.", 403); + } + + const banners = await readBanners(); + const html = Template(ctx, banners); + + res.send(html); +}; + +const Template = (ctx: TemplateCtx, banners: Banner[]) => { + return ( + + + +

Bannery

+
+ +
+
+ + {canUser(ctx.user, CzchanPerm.MANAGE_BANNERS) && ( + <> +
+ + + )} + + +
+
+
+ ); +}; + +const BannerTable = ({ + ctx, + banners, +}: PropsWithChildren<{ ctx: TemplateCtx; banners: Banner[] }>) => ( + + + + + + + + + + + {banners.map((banner) => ( + + ))} + {banners.length === 0 && ( + + + + )} + +
BannerVytvořilVytvořen
+ Nic tady není... +
+); + +const BannerRow = ({ + ctx, + banner, +}: PropsWithChildren<{ ctx: TemplateCtx; banner: Banner }>) => ( + + + + + + + + {banner.created_by} + + + + +); + +const CreateForm = () => ( + <> +
+

Vytvořit

+
+ + + + +
+ + +); diff --git a/src/routes/mod/bans.tsx b/src/routes/mod/bans.tsx new file mode 100755 index 0000000..39c18fd --- /dev/null +++ b/src/routes/mod/bans.tsx @@ -0,0 +1,120 @@ +import Datetime from "../../components/datetime"; +import ModNav from "../../components/mod_nav"; +import Page from "../../components/page"; +import PageBody from "../../components/page_body"; +import PageHead from "../../components/page_head"; +import { getCtx, TemplateCtx } from "../../ctx"; +import { readBans } from "../../db/ban"; +import { Ban } from "../../db/schema"; +import { CzchanError } from "../../error"; +import { canUser, CzchanPerm } from "../../permissions"; +import { PropsWithChildren } from "@kitajs/html"; +import { Request, Response } from "express"; + +export default async (req: Request, res: Response) => { + const ctx = await getCtx(req); + + if (!canUser(ctx.user, CzchanPerm.MANAGE_BANS)) { + throw new CzchanError("K tomuto nemáš oprávnění.", 403); + } + + const bans = await readBans(); + const html = Template(ctx, bans); + + res.send(html); +}; + +const Template = (ctx: TemplateCtx, bans: Ban[]) => { + return ( + + + +

Bany

+
+ +
+
+ +
+ + +
+
+
+ ); +}; + +const BanTable = ({ + ctx, + bans, +}: PropsWithChildren<{ ctx: TemplateCtx; bans: Ban[] }>) => ( + + + + + + + + + + + + + + {bans.map((ban) => ( + + ))} + {bans.length === 0 && ( + + + + )} + +
IPDůvodNástěnkaVytvořilVypršíVytvořen
+ Nic tady není... +
+); + +const BanRow = ({ + ctx, + ban, +}: PropsWithChildren<{ ctx: TemplateCtx; ban: Ban }>) => ( + + + {(() => { + return ( + + ); + })()} + + + {canUser(ctx.user, CzchanPerm.VIEW_IPS) ? ban.ip_range : Skrytá} + + + {ban.board === null ? ( + Všechny + ) : ( + {`/${ban.board}/`} + )} + + {ban.created_by} + + + + + + + +); diff --git a/src/routes/mod/boards.tsx b/src/routes/mod/boards.tsx new file mode 100755 index 0000000..b3dcd1d --- /dev/null +++ b/src/routes/mod/boards.tsx @@ -0,0 +1,146 @@ +import Datetime from "../../components/datetime"; +import { Form, FormField } from "../../components/form"; +import ModNav from "../../components/mod_nav"; +import Page from "../../components/page"; +import PageBody from "../../components/page_body"; +import PageHead from "../../components/page_head"; +import { getCtx, TemplateCtx } from "../../ctx"; +import { readBoards } from "../../db/board"; +import { Board } from "../../db/schema"; +import { CzchanError } from "../../error"; +import { canUser, CzchanPerm } from "../../permissions"; +import { PropsWithChildren } from "@kitajs/html"; +import { Request, Response } from "express"; + +export default async (req: Request, res: Response) => { + const ctx = await getCtx(req); + + if (!canUser(ctx.user, CzchanPerm.MANAGE_BOARDS)) { + throw new CzchanError("K tomuto nemáš oprávnění.", 403); + } + + const boards = await readBoards(); + const html = Template(ctx, boards); + + res.send(html); +}; + +const Template = (ctx: TemplateCtx, boards: Board[]) => { + return ( + + + +

Nástěnky

+
+ +
+
+ +
+ + + + +
+
+
+ ); +}; + +const BoardTable = ({ + ctx, + boards, +}: PropsWithChildren<{ ctx: TemplateCtx; boards: Board[] }>) => ( + + + + + + + + + + + + + {boards.map((board) => ( + + ))} + {boards.length === 0 && ( + + + + )} + +
IDJménoPopisNastaveníVytvořena
+ Nic tady není... +
+); + +const BoardRow = ({ + ctx, + board, +}: PropsWithChildren<{ ctx: TemplateCtx; board: Board }>) => ( + + + + + /{board.id}/ + {board.name} + {board.description} + + [Nastavení] + + + + + +); + +const UpdateForm = () => ( + <> +
+

Upravit

+

Prázdná pole nebudou změněna.

+
+ + + + + + +
+ +); + +const CreateForm = () => ( + <> +
+

Vytvořit

+
+ + + + + + + + + + +
+ + +); diff --git a/src/routes/mod/index.tsx b/src/routes/mod/index.tsx new file mode 100755 index 0000000..e455ee8 --- /dev/null +++ b/src/routes/mod/index.tsx @@ -0,0 +1,144 @@ +import Capcode from "../../components/capcode"; +import { Form, FormField } from "../../components/form"; +import ModNav from "../../components/mod_nav"; +import Page from "../../components/page"; +import PageBody from "../../components/page_body"; +import PageHead from "../../components/page_head"; +import { getCtx, TemplateCtx } from "../../ctx"; +import { readBoards } from "../../db/board"; +import { Board } from "../../db/schema"; +import { CzchanPerm, canUser } from "../../permissions"; +import { PropsWithChildren } from "@kitajs/html"; +import { Request, Response } from "express"; + +export default async (req: Request, res: Response) => { + const ctx = await getCtx(req); + const boards = await readBoards(); + const html = Template(ctx, boards); + + res.send(html); +}; + +const Template = (ctx: TemplateCtx, boards: Board[]) => { + return ( + + + +

Domov

+
+ +
+ +
+ + +
+
+
+ ); +}; + +const UserTable = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => ( + + + + + + + + + + + + + + + + + + + +
Jméno{ctx.user?.username}
Úroveň + {ctx.user?.rank} + {ctx.user?.rank === 0 && ( + <> + {" "} + (root) + + )} +
Capcode + +
Oprávnění + [{ctx.user?.permissions.length}]{" "} + [Zobrazit] +
+); + +const BoardTable = ({ + ctx, + boards, +}: PropsWithChildren<{ ctx: TemplateCtx; boards: Board[] }>) => ( + + + + + + + + {boards + .filter( + (board) => + !board.config.private || + canUser(ctx.user, CzchanPerm.VIEW_PRIVATE_BOARDS), + ) + .map((board) => ( + + ))} + {boards.length === 0 && ( + + + + )} + +
Nástěnky
+ Nic tady není... +
+); + +const BoardRow = ({ board }: PropsWithChildren<{ board: Board }>) => ( + + + + /{board.id}/ - {board.name} + + {board.config.unlisted && ( + + )} + {board.config.private && ( + + )} + + {board.description} + +); + +const PasswordChangeForm = () => ( + <> +
+

Změnit heslo

+
+ + + + + + + +
+ + +); diff --git a/src/routes/mod/logs.tsx b/src/routes/mod/logs.tsx new file mode 100755 index 0000000..a70b9bb --- /dev/null +++ b/src/routes/mod/logs.tsx @@ -0,0 +1,35 @@ +import ModNav from "../../components/mod_nav"; +import Page from "../../components/page"; +import PageBody from "../../components/page_body"; +import PageHead from "../../components/page_head"; +import { getCtx, TemplateCtx } from "../../ctx"; +import { CzchanError } from "../../error"; +import { canUser, CzchanPerm } from "../../permissions"; +import { Request, Response } from "express"; + +export default async (req: Request, res: Response) => { + const ctx = await getCtx(req); + + if (!canUser(ctx.user, CzchanPerm.VIEW_LOGS)) { + throw new CzchanError("K tomuto nemáš oprávnění.", 403); + } + + const html = Template(ctx); + + res.send(html); +}; + +const Template = (ctx: TemplateCtx) => { + return ( + + + +

Záznamy

+

TBI

+
+ +
+
+
+ ); +}; diff --git a/src/routes/mod/news.tsx b/src/routes/mod/news.tsx new file mode 100755 index 0000000..2366ca1 --- /dev/null +++ b/src/routes/mod/news.tsx @@ -0,0 +1,124 @@ +import Datetime from "../../components/datetime"; +import { Form, FormField } from "../../components/form"; +import ModNav from "../../components/mod_nav"; +import Page from "../../components/page"; +import PageBody from "../../components/page_body"; +import PageHead from "../../components/page_head"; +import { getCtx, TemplateCtx } from "../../ctx"; +import { readNews } from "../../db/news"; +import { News } from "../../db/schema"; +import { CzchanError } from "../../error"; +import { canUser, CzchanPerm } from "../../permissions"; +import { PropsWithChildren } from "@kitajs/html"; +import { Request, Response } from "express"; + +export default async (req: Request, res: Response) => { + const ctx = await getCtx(req); + + if (!canUser(ctx.user, CzchanPerm.MANAGE_NEWS)) { + throw new CzchanError("K tomuto nemáš oprávnění.", 403); + } + + const news = await readNews(); + const html = Template(ctx, news); + + res.send(html); +}; + +const Template = (ctx: TemplateCtx, news: News[]) => { + return ( + + + +

Novinky

+
+ +
+
+ +
+ + + +
+
+
+ ); +}; + +const NewsTable = ({ + ctx, + news, +}: PropsWithChildren<{ ctx: TemplateCtx; news: News[] }>) => ( + + + + + + + + + + + + {news.map((news) => ( + + ))} + {news.length === 0 && ( + + + + )} + +
PředmětObsahVytvořilVyvtvořeny
+ Nic tady není... +
+); + +const NewsRow = ({ + ctx, + news, +}: PropsWithChildren<{ ctx: TemplateCtx; news: News }>) => ( + + + + + {news.subject} + {news.created_by} + + [Upravit] + + + + + +); + +const CreateForm = () => ( + <> +
+

Vytvořit

+
+ + + + + + + +
+ + +); diff --git a/src/routes/mod/permissions.tsx b/src/routes/mod/permissions.tsx new file mode 100644 index 0000000..ada4001 --- /dev/null +++ b/src/routes/mod/permissions.tsx @@ -0,0 +1,77 @@ +import { Form, FormField } from "../../components/form"; +import Page from "../../components/page"; +import PageBody from "../../components/page_body"; +import PageHead from "../../components/page_head"; +import { getCtx, TemplateCtx } from "../../ctx"; +import { User } from "../../db/schema"; +import { readUser } from "../../db/user"; +import { canUser, canUserActOn, CzchanPerm } from "../../permissions"; +import { PropsWithChildren } from "@kitajs/html"; +import { Request, Response } from "express"; + +export default async (req: Request, res: Response) => { + const ctx = await getCtx(req); + const user = await readUser(req.params.user); + const html = Template(ctx, user); + + res.send(html); +}; + +const Template = (ctx: TemplateCtx, user: User) => ( + + + +

+ Oprávnění ({user.username}) +

+ {user.rank === 0 && ( +

+ Tento uživatel je root (rank = 0), takže na něj nemají změny oprávnění + efekt. +

+ )} + +
+
+
+); + +const UpdateForm = ({ + ctx, + user, +}: PropsWithChildren<{ ctx: TemplateCtx; user: User }>) => ( + <> +
+
+ + + {Object.values(CzchanPerm).map((perm) => ( + + + + ))} +
+ + +); diff --git a/src/routes/mod/reports.tsx b/src/routes/mod/reports.tsx new file mode 100755 index 0000000..d30b19e --- /dev/null +++ b/src/routes/mod/reports.tsx @@ -0,0 +1,35 @@ +import ModNav from "../../components/mod_nav"; +import Page from "../../components/page"; +import PageBody from "../../components/page_body"; +import PageHead from "../../components/page_head"; +import { getCtx, TemplateCtx } from "../../ctx"; +import { CzchanError } from "../../error"; +import { canUser, CzchanPerm } from "../../permissions"; +import { Request, Response } from "express"; + +export default async (req: Request, res: Response) => { + const ctx = await getCtx(req); + + if (!canUser(ctx.user, CzchanPerm.MANAGE_REPORTS)) { + throw new CzchanError("K tomuto nemáš oprávnění.", 403); + } + + const html = Template(ctx); + + res.send(html); +}; + +const Template = (ctx: TemplateCtx) => { + return ( + + + +

Hlášení

+

TBI

+
+ +
+
+
+ ); +}; diff --git a/src/routes/mod/users.tsx b/src/routes/mod/users.tsx new file mode 100755 index 0000000..ff75af6 --- /dev/null +++ b/src/routes/mod/users.tsx @@ -0,0 +1,231 @@ +import Capcode from "../../components/capcode"; +import Datetime from "../../components/datetime"; +import { Form, FormField } from "../../components/form"; +import ModNav from "../../components/mod_nav"; +import Page from "../../components/page"; +import PageBody from "../../components/page_body"; +import PageHead from "../../components/page_head"; +import { getCtx, TemplateCtx } from "../../ctx"; +import { User } from "../../db/schema"; +import { readUsers } from "../../db/user"; +import { + canUser, + canUserActOn, + canUserApplyRole, + CzchanPerm, +} from "../../permissions"; +import { PropsWithChildren } from "@kitajs/html"; +import { Request, Response } from "express"; + +export default async (req: Request, res: Response) => { + const ctx = await getCtx(req); + const users = await readUsers(); + const html = Template(ctx, users); + + res.send(html); +}; + +const Template = (ctx: TemplateCtx, users: User[]) => { + return ( + + + +

Uživatelé

+
+ +
+
+ +
+ + {canUser(ctx.user, CzchanPerm.MANAGE_USERS) && ( + + )} + {(canUser(ctx.user, CzchanPerm.MANAGE_USERS) || + canUser(ctx.user, CzchanPerm.CUSTOM_CAPCODE)) && ( + + )} + {canUser(ctx.user, CzchanPerm.MANAGE_USERS) && } + + {canUser(ctx.user, CzchanPerm.MANAGE_USERS) && } +
+
+
+ ); +}; + +const UserTable = ({ + ctx, + users, +}: PropsWithChildren<{ ctx: TemplateCtx; users: User[] }>) => ( + + + + + + + + + + + + + {users.map((user) => ( + + ))} + {users.length === 0 && ( + + + + )} + +
JménoÚroveňCapcodeOprávněníVytvořen
+ Nic tady není... +
+); + +const UserRow = ({ + ctx, + user, +}: PropsWithChildren<{ ctx: TemplateCtx; user: User }>) => ( + + + {(() => { + const permissible = + (canUser(ctx.user, CzchanPerm.MANAGE_USERS) && + canUserActOn(ctx.user, user)) || + ctx.user?.username === user.username; + return ( + + ); + })()} + + {user.username} + + {user.rank} + {user.rank === 0 && ( + <> + {" "} + (root) + + )} + + + + + + [{user.permissions.length}]{" "} + [Zobrazit] + + + + + +); + +const UpdateRankForm = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => ( + <> +
+

Upravit úroveň

+
+ + + +
+ +); + +const UpdateCapcodeForm = ({ + ctx, +}: PropsWithChildren<{ ctx: TemplateCtx }>) => ( + <> +
+

Upravit capcode

+

Prázdná pole nebudou změněna.

+
+ + + + + + + + + +
+ +); + +const RoleForm = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => ( + <> +
+

Aplikovat roli

+
+ + + +
+ +); + +const CreateForm = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => ( + <> +
+

Vytvořit

+
+ + + + + + + + + + +
+ + +); + +const RoleList = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => ( + <> + {Object.keys(ctx.config.roles) + .filter((role) => { + const role_ = ctx.config.roles[role]; + return canUserApplyRole(ctx.user, role_); + }) + .map((role) => ( + + ))} + +); diff --git a/src/zod_schema.ts b/src/zod_schema.ts new file mode 100644 index 0000000..e53991e --- /dev/null +++ b/src/zod_schema.ts @@ -0,0 +1,184 @@ +import { CzchanPerm } from "./permissions"; +import z, { ZodType } from "zod"; + +// Fields + +const zHttpValue = z + .string() + .trim() + .transform((value) => (value === "" ? null : value)); + +const zRequiredField = >(type: T) => + z + .array(zHttpValue, "Bylo vynecháno povinné pole.") + .transform((arr) => arr[0] || null) + .pipe(z.string("Povinné pole nesmí být prázdné.").pipe(type)); + +const zOptionalField = >(type: T) => + z + .optional( + z + .array(zHttpValue) + .transform((arr) => arr[0] || null) + .pipe(z.nullable(type)), + ) + .transform((value) => (value === undefined ? null : value)); + +const zMultiField = >(type: T) => + z + .optional(z.array(zHttpValue.pipe(type))) + .transform((value) => (value === undefined ? [] : value)); + +const zUsername = z + .string() + .regex( + /^[\w]{1,32}$/, + "Uživatelské jméno musí mít 1-32 znaků a může obsahovat pouze čísla, písmena (A-Z) a podtržítka (_).", + ); + +const zPassword = z + .string() + .regex( + /^[^\s]{8,256}$/, + "Heslo musí mít 8-256 znaků a nemůže obsahovat mezery.", + ); + +const zRank = z.coerce + .number("Úroveň musí být číslo.") + .pipe( + z.int("Úroveň musí být celé číslo.").min(0, "Úroveň musí být větší než 0."), + ); + +const zCapcodeText = z + .string() + .max(32, "Capcode musí mít maximálně 32 znaků") + .refine( + (string) => !string.includes(":"), + "Capcode nesmí obsahovat dvojtečku (:), jelikož se používá jako oddělovač.", + ); + +const zCapcodeIcon = z + .string() + .max( + 32, + "Jméno ikony musí mít maximálně 32 znaků. Pokud jsi administrátor, uprav jméno ikony.", + ) + .refine( + (string) => !string.includes(":"), + "Capcode nesmí obsahovat dvojtečku (:), jelikož se používá jako oddělovač. Pokud jsi administrátor, uprav jméno ikony.", + ); + +const zColor = z + .string() + .regex( + /^(?:[A-Fa-f0-9]{3}){1,2}$/, + "Barva musí být trojmístný nebo šestimístný hexadecimální kód. (BEZ #)", + ); + +const zRole = z.string(); + +const zPermission = z.enum(CzchanPerm); + +const zBoardId = z + .string() + .regex( + /^[\w]{1,16}$/, + "ID nástěnky musí mít 1-16 znaků a může obsahovat pouze čísla, písmena (A-Z) a podtržítka (_).", + ); + +const zBoardName = z + .string() + .max(32, "Jméno nástěnky musí mít maximálně 32 znaků."); + +const zBoardDescription = z + .string() + .max(256, "Popis nástěnky musí mít maximálně 256 znaků."); + +// Form-specific schema + +const zUserAction = z.enum( + ["$delete_user", "$update_rank", "$update_capcode", "$apply_role"], + "Neplatná akce.", +); + +const zBoardAction = z.enum( + ["$delete_board", "$update_board"], + "Neplatná akce.", +); + +// Forms + +// /actions/login +const LoginForm = z.object({ + username: zRequiredField(zUsername), + password: zRequiredField(zPassword), +}); + +// /mod/actions/update_password +const UpdatePasswordForm = z.object({ + old_password: zRequiredField(zPassword), + new_password: zRequiredField(zPassword), +}); + +// /mod/actions/create_user +const CreateUserForm = z.object({ + new_username: zRequiredField(zUsername), + new_password: zRequiredField(zPassword), + new_role: zRequiredField(zRole), +}); + +// /mod/actions/user_actions +const UserActionsForm = z.object({ + users: zMultiField(zUsername), + action: zRequiredField(zUserAction), +}); + +const UpdateRankForm = z.object({ + rank: zRequiredField(zRank), +}); + +const UpdateCapcodeForm = z.object({ + capcode_text: zOptionalField(zCapcodeText), + capcode_color: zOptionalField(zColor), + capcode_icon: zOptionalField(zCapcodeIcon), +}); + +const ApplyRoleForm = z.object({ role: zRequiredField(zRole) }); + +// /mod/actions/create_board +const CreateBoardForm = z.object({ + id: zRequiredField(zBoardId), + name: zRequiredField(zBoardName), + description: zRequiredField(zBoardDescription), +}); + +// /mod/actions/board_actions +const BoardActionsForm = z.object({ + boards: zMultiField(zBoardId), + action: zRequiredField(zBoardAction), +}); + +const UpdateBoardForm = z.object({ + name: zOptionalField(zBoardName), + description: zOptionalField(zBoardDescription), +}); + +// /mod/actions/update_permissions +const UpdatePermissionsForm = z.object({ + user: zRequiredField(zUsername), + permissions: zMultiField(zPermission), +}); + +export { + LoginForm, + UpdatePasswordForm, + CreateUserForm, + UserActionsForm, + UpdateRankForm, + UpdateCapcodeForm, + ApplyRoleForm, + CreateBoardForm, + BoardActionsForm, + UpdateBoardForm, + UpdatePermissionsForm, +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100755 index 0000000..6f4f679 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "rootDir": ".", + "outDir": "./dist", + "strict": true, + "moduleResolution": "NodeNext", + // Style + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + // JSX + "jsx": "react-jsx", + "jsxImportSource": "@kitajs/html", + "plugins": [{ "name": "@kitajs/ts-html-plugin" }] + } +} diff --git a/web/styles/base.scss b/web/styles/base.scss new file mode 100755 index 0000000..0c6e679 --- /dev/null +++ b/web/styles/base.scss @@ -0,0 +1,145 @@ +body { + margin: 0; + overflow-wrap: break-word; +} + +input[type="text"], +input[type="password"], +input[type="number"], +input[type="file"], +button, +textarea, +select, +.input-wrapper { + padding: 4px; +} + +button { + cursor: pointer; +} + +header { + padding: 4px; +} + +header:after { + display: block; + clear: right; +} + +nav a { + text-decoration: none; +} + +footer, +.main { + margin: 8px; +} + +.user-options { + float: right; +} + +.container { + margin: 0 auto; + /* Gracefully degrades into just being full-width on ancient browsers */ + max-width: 960px; +} + +.logo { + margin: 0 auto; + width: 400px; + max-width: 100%; +} + +.subtitle { + font-weight: bold; +} + +.form-table { + /* 4px on the outside in total */ + border-spacing: 2px; + padding: 2px; +} + +.form-table th { + padding: 4px; + font-weight: bold; + text-align: left; +} + +.form-table td { + padding: 0; +} + +.data-table { + border-collapse: collapse; +} + +.data-table th { + text-align: left; +} + +.data-table th, +.data-table td { + padding: 4px; +} + +.infobox-head { + padding: 4px; + font-weight: bold; +} + +.infobox-body { + padding: 4px; +} + +.pagination { + display: inline-block; + padding: 8px; +} + +footer { + text-align: center; +} + +.inner-footer { + display: inline-block; + margin: 0 auto; + padding: 4px; +} + +/* Formatting */ + +.capcode { + font-weight: bold; +} + +.icon { + vertical-align: text-top; + height: 16px; +} + +/* Utility */ + +.clearfix:after { + display: block; + clear: both; + content: ""; +} + +.left { + text-align: left; +} + +.center { + text-align: center; +} + +.auto-center { + margin: 0 auto; +} + +.full-width { + width: 100%; +} diff --git a/web/styles/themes/yotsuba.scss b/web/styles/themes/yotsuba.scss new file mode 100755 index 0000000..aa31762 --- /dev/null +++ b/web/styles/themes/yotsuba.scss @@ -0,0 +1,113 @@ +$bg: #ffffee url("/public/img/theme/bg-yotsuba.png") top repeat-x; +$font: Arial, Helvetica, sans-serif; +$title-font: + Times New Roman, + Georgia, + serif; + +$text: #800000; +$link: #800000; +$link-hover: #dd0000; +$post-link: #000080; +$post-link-hover: #dd0000; +$link-list: #bb8866; +$title: #af0a0f; +$name: #117743; +$trip: #117743; +$capcode: #cc1105; + +$hr: #d9bfb7; +$table-head: #ffccaa; +$table-bg: #ffffff; +$table-border: #800000; +$input: #000000; +$input-bg: #ffffff; +$input-border: #808080; +$box-bg: #f0e0d6; +$box-border: #d9bfb7; +$hl-box-bg: #f0c0b0; +$hl-box-border: #d99f91; + +body { + background: $bg; + color: $text; + font-family: $font; +} + +a { + color: $link; +} + +a:hover { + color: $link-hover; +} + +hr { + border: none; + border-top: 1px solid $hr; +} + +header { + border-bottom: 1px solid $box-border; + background-color: $box-bg; + color: $link-list; +} + +.title { + color: $title; + font-family: $title-font; +} + +.form-table { + border-right: 1px solid $box-border; + border-bottom: 1px solid $box-border; + background-color: $box-bg; +} + +.form-table th, +.data-table th { + border: 1px solid $table-border; + background-color: $table-head; +} + +input[type="text"], +input[type="password"], +input[type="number"], +input[type="file"], +button, +textarea, +select, +.input-wrapper { + border: 1px solid $input-border; + background-color: $input-bg; + color: $input; +} + +.data-table { + background-color: $table-bg; +} + +.data-table td { + border: 1px solid $table-border; +} + +.infobox { + border: 1px solid $table-border; + background-color: $table-bg; +} + +.infobox-head { + background-color: $table-head; +} + +.pagination { + border-right: 1px solid $box-border; + border-bottom: 1px solid $box-border; + background-color: $box-bg; +} + +.inner-footer { + border-right: 1px solid $box-border; + border-bottom: 1px solid $box-border; + background-color: $box-bg; +}