From bee5d045c306c4c4cfb1611d8c25e61b4a3878b6 Mon Sep 17 00:00:00 2001 From: Thomas Miceli Date: Tue, 14 Mar 2023 16:22:52 +0100 Subject: [PATCH] Initial commit --- .gitattributes | 2 + .gitignore | 7 + LICENSE | 661 ++++++++ config.yml | 49 + go.mod | 35 + go.sum | 110 ++ internal/config/config.go | 83 + internal/git/commands.go | 293 ++++ internal/models/db.go | 31 + internal/models/gist.go | 136 ++ internal/models/sshkey.go | 56 + internal/models/user.go | 77 + internal/resources/pre-receive | 21 + internal/ssh/git-ssh.go | 103 ++ internal/ssh/run.go | 153 ++ internal/web/admin.go | 106 ++ internal/web/auth.go | 103 ++ internal/web/gist.go | 520 ++++++ internal/web/git-http.go | 253 +++ internal/web/run.go | 255 +++ internal/web/ssh.go | 79 + internal/web/util.go | 254 +++ opengist.go | 54 + package-lock.json | 2705 ++++++++++++++++++++++++++++++ package.json | 25 + postcss.config.js | 6 + public/editor.js | 75 + public/favicon.svg | 3 + public/main.js | 132 ++ public/markdown.css | 942 +++++++++++ public/style.css | 109 ++ tailwind.config.js | 37 + templates/base/admin_footer.html | 9 + templates/base/admin_header.html | 22 + templates/base/base_footer.html | 21 + templates/base/base_header.html | 142 ++ templates/base/gist_footer.html | 4 + templates/base/gist_header.html | 165 ++ templates/base/pagination.html | 31 + templates/pages/admin_gists.html | 43 + templates/pages/admin_index.html | 55 + templates/pages/admin_users.html | 35 + templates/pages/all.html | 109 ++ templates/pages/auth_form.html | 58 + templates/pages/create.html | 49 + templates/pages/edit.html | 57 + templates/pages/error.html | 14 + templates/pages/gist.html | 45 + templates/pages/likes.html | 27 + templates/pages/revisions.html | 110 ++ templates/pages/ssh_keys.html | 73 + vite.config.js | 16 + 52 files changed, 8560 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 config.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/git/commands.go create mode 100644 internal/models/db.go create mode 100644 internal/models/gist.go create mode 100644 internal/models/sshkey.go create mode 100644 internal/models/user.go create mode 100644 internal/resources/pre-receive create mode 100644 internal/ssh/git-ssh.go create mode 100644 internal/ssh/run.go create mode 100644 internal/web/admin.go create mode 100644 internal/web/auth.go create mode 100644 internal/web/gist.go create mode 100644 internal/web/git-http.go create mode 100644 internal/web/run.go create mode 100644 internal/web/ssh.go create mode 100644 internal/web/util.go create mode 100644 opengist.go create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/editor.js create mode 100644 public/favicon.svg create mode 100644 public/main.js create mode 100644 public/markdown.css create mode 100644 public/style.css create mode 100644 tailwind.config.js create mode 100644 templates/base/admin_footer.html create mode 100644 templates/base/admin_header.html create mode 100644 templates/base/base_footer.html create mode 100644 templates/base/base_header.html create mode 100644 templates/base/gist_footer.html create mode 100644 templates/base/gist_header.html create mode 100644 templates/base/pagination.html create mode 100644 templates/pages/admin_gists.html create mode 100644 templates/pages/admin_index.html create mode 100644 templates/pages/admin_users.html create mode 100644 templates/pages/all.html create mode 100644 templates/pages/auth_form.html create mode 100644 templates/pages/create.html create mode 100644 templates/pages/edit.html create mode 100644 templates/pages/error.html create mode 100644 templates/pages/gist.html create mode 100644 templates/pages/likes.html create mode 100644 templates/pages/revisions.html create mode 100644 templates/pages/ssh_keys.html create mode 100644 vite.config.js diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..de7e34d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +templates/**/* linguist-vendored +public/**/* linguist-vendored diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe9edce --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +gist.db +.idea/ +.DS_Store +/**/.DS_Store +public/assets/* +public/manifest.json \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -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/config.yml b/config.yml new file mode 100644 index 0000000..4cb4eae --- /dev/null +++ b/config.yml @@ -0,0 +1,49 @@ +# Directory where Opengist will store its data. Default: ~/.opengist/ +opengist-home: + +# Name of the SQLite database file. Default: opengist.db +db-filename: opengist.db + +# Prevents the creation of new accounts (either `true` or `false`). Default: false +disable-signup: false + +# Set the log level to one of the following: trace, debug, info, warn, error, fatal, panic. Default: warn +log-level: warn + +# HTTP server configuration +http: + + # Host to bind to. Default: 0.0.0.0 + host: 0.0.0.0 + + # Port to bind to. Default: 6157 + port: 6157 + + # Domain to use in links. Default: localhost + domain: localhost + + # Enable or disable git operations (clone, pull, push) via HTTP (either `true` or `false`). Default: true + git-enabled: true + +# SSH built-in server configuration +# Note: it is not using the SSH daemon from your machine (yet) +ssh: + + # Enable or disable SSH built-in server + # for git operations (clone, pull, push) via SSH (either `true` or `false`). Default: true + enabled: true + + # Host to bind to. Default: 0.0.0.0 + host: 0.0.0.0 + + # Port to bind to. Default: 2222 + # Note: it cannot be the same port as the SSH daemon if it's currently running + # If you want to use the port 22 for the built-in SSH server, + # you can either change the port of the SSH daemon or stop it + port: 2222 + + # Domain to use in links. Default: localhost + domain: localhost + + # Path or alias to ssh-keygen executable. Default: ssh-keygen + keygen-executable: ssh-keygen diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..25a1781 --- /dev/null +++ b/go.mod @@ -0,0 +1,35 @@ +module opengist + +go 1.18 + +require ( + github.com/go-playground/validator/v10 v10.11.0 + github.com/google/uuid v1.3.0 + github.com/gorilla/sessions v1.2.1 + github.com/labstack/echo/v4 v4.10.0 + github.com/rs/zerolog v1.29.0 + golang.org/x/crypto v0.2.0 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/sqlite v1.3.2 + gorm.io/gorm v1.23.5 +) + +require ( + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/gorilla/securecookie v1.1.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/labstack/gommon v0.4.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-sqlite3 v1.14.13 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/net v0.4.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.5.0 // indirect + golang.org/x/time v0.2.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..20f6960 --- /dev/null +++ b/go.sum @@ -0,0 +1,110 @@ +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= +github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA= +github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= +github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= +github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE= +golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE= +golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.3.2 h1:nWTy4cE52K6nnMhv23wLmur9Y3qWbZvOBz+V4PrGAxg= +gorm.io/driver/sqlite v1.3.2/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U= +gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM= +gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..4fbac7e --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,83 @@ +package config + +import ( + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "gopkg.in/yaml.v3" + "os" + "path/filepath" +) + +var OpengistVersion = "0.0.1" + +var C *config + +type config struct { + OpengistHome string `yaml:"opengist-home"` + DBFilename string `yaml:"db-filename"` + DisableSignup bool `yaml:"disable-signup"` + LogLevel string `yaml:"log-level"` + + HTTP struct { + Host string `yaml:"host"` + Port string `yaml:"port"` + Domain string `yaml:"domain"` + Git bool `yaml:"git-enabled"` + } `yaml:"http"` + + SSH struct { + Enabled bool `yaml:"enabled"` + Host string `yaml:"host"` + Port string `yaml:"port"` + Domain string `yaml:"domain"` + Keygen string `yaml:"keygen-executable"` + } `yaml:"ssh"` +} + +func InitConfig(configPath string) error { + c := &config{} + + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + c.OpengistHome = filepath.Join(homeDir, ".opengist") + c.LogLevel = "warn" + file, err := os.Open(configPath) + if err != nil { + return err + } + defer file.Close() + + d := yaml.NewDecoder(file) + if err = d.Decode(&c); err != nil { + return err + } + C = c + + return nil +} + +func InitLog() { + if err := os.MkdirAll(filepath.Join(GetHomeDir(), "log"), 0755); err != nil { + panic(err) + } + file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + panic(err) + } + multi := zerolog.MultiLevelWriter(zerolog.NewConsoleWriter(), file) + + var level zerolog.Level + level, err = zerolog.ParseLevel(C.LogLevel) + if err != nil { + level = zerolog.InfoLevel + } + + log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger() +} + +func GetHomeDir() string { + absolutePath, _ := filepath.Abs(C.OpengistHome) + return filepath.Clean(absolutePath) +} diff --git a/internal/git/commands.go b/internal/git/commands.go new file mode 100644 index 0000000..7329f9b --- /dev/null +++ b/internal/git/commands.go @@ -0,0 +1,293 @@ +package git + +import ( + "io" + "opengist/internal/config" + "os" + "os/exec" + "path" + "path/filepath" + "strings" +) + +func GetRepositoryPath(user string, gist string) (string, error) { + return filepath.Join(config.GetHomeDir(), "repos", strings.ToLower(user), gist), nil +} + +func getTmpRepositoryPath(gistId string) (string, error) { + dirname, err := getTmpRepositoriesPath() + if err != nil { + return "", err + } + return filepath.Join(dirname, gistId), nil +} + +func getTmpRepositoriesPath() (string, error) { + return filepath.Join(config.GetHomeDir(), "tmp", "repos"), nil +} + +func InitRepository(user string, gist string) error { + repositoryPath, err := GetRepositoryPath(user, gist) + + if err != nil { + return err + } + + cmd := exec.Command( + "git", + "init", + "--bare", + repositoryPath, + ) + + _, err = cmd.Output() + if err != nil { + return err + } + + f1, err := os.OpenFile(filepath.Join(repositoryPath, "git-daemon-export-ok"), os.O_RDONLY|os.O_CREATE, 0644) + defer f1.Close() + + preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", "pre-receive"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0744) + if err != nil { + return err + } + + preReceiveSrc, err := os.OpenFile(filepath.Join("internal", "resources", "pre-receive"), os.O_RDONLY, os.ModeAppend) + if err != nil { + return err + } + _, err = io.Copy(preReceiveDst, preReceiveSrc) + if err != nil { + return err + } + + defer preReceiveDst.Close() + defer preReceiveSrc.Close() + + return err +} + +func GetNumberOfCommitsOfRepository(user string, gist string) (string, error) { + repositoryPath, err := GetRepositoryPath(user, gist) + if err != nil { + return "", err + } + + cmd := exec.Command( + "git", + "rev-list", + "--all", + "--count", + ) + cmd.Dir = repositoryPath + + stdout, err := cmd.Output() + return strings.TrimSuffix(string(stdout), "\n"), err +} + +func GetFilesOfRepository(user string, gist string, commit string) ([]string, error) { + repositoryPath, err := GetRepositoryPath(user, gist) + if err != nil { + return nil, err + } + + cmd := exec.Command( + "git", + "ls-tree", + commit, + "--name-only", + ) + cmd.Dir = repositoryPath + + stdout, err := cmd.Output() + if err != nil { + return nil, err + } + + slice := strings.Split(string(stdout), "\n") + return slice[:len(slice)-1], nil +} + +func GetFileContent(user string, gist string, commit string, filename string) (string, error) { + repositoryPath, err := GetRepositoryPath(user, gist) + if err != nil { + return "", err + } + + cmd := exec.Command( + "git", + "--no-pager", + "show", + commit+":"+filename, + ) + cmd.Dir = repositoryPath + + stdout, err := cmd.Output() + return string(stdout), err +} + +func GetLog(user string, gist string, skip string) (string, error) { + repositoryPath, err := GetRepositoryPath(user, gist) + if err != nil { + return "", err + } + + cmd := exec.Command( + "git", + "--no-pager", + "log", + "-n", + "11", + "--no-prefix", + "--no-color", + "-p", + "--skip", + skip, + "--format=format:%n=commit %H:%aN:%at", + "--shortstat", + "--ignore-missing", // avoid errors if a wrong hash is given + "HEAD", + ) + cmd.Dir = repositoryPath + + stdout, err := cmd.Output() + return string(stdout), err +} + +func CloneTmp(user string, gist string, gistTmpId string) error { + repositoryPath, err := GetRepositoryPath(user, gist) + if err != nil { + return err + } + + tmpPath, err := getTmpRepositoriesPath() + if err != nil { + return err + } + + tmpRepositoryPath := path.Join(tmpPath, gistTmpId) + + err = os.RemoveAll(tmpRepositoryPath) + if err != nil { + return err + } + + cmd := exec.Command("git", "clone", repositoryPath, gistTmpId) + cmd.Dir = tmpPath + if err = cmd.Run(); err != nil { + return err + } + + cmd = exec.Command("git", "config", "user.name", user) + cmd.Dir = tmpRepositoryPath + if err = cmd.Run(); err != nil { + return err + } + + // remove every file (and not the .git directory!) + cmd = exec.Command("find", ".", "-maxdepth", "1", "-type", "f", "-delete") + cmd.Dir = tmpRepositoryPath + return cmd.Run() +} + +func SetFileContent(gistTmpId string, filename string, content string) error { + repositoryPath, err := getTmpRepositoryPath(gistTmpId) + if err != nil { + return err + } + + return os.WriteFile(filepath.Join(repositoryPath, filename), []byte(content), 0644) +} + +func AddAll(gistTmpId string) error { + tmpPath, err := getTmpRepositoryPath(gistTmpId) + if err != nil { + return err + } + + // in case of a change where only a file name has its case changed + cmd := exec.Command("git", "rm", "-r", "--cached", "--ignore-unmatch", ".") + cmd.Dir = tmpPath + err = cmd.Run() + if err != nil { + return err + } + + cmd = exec.Command("git", "add", "-A") + cmd.Dir = tmpPath + + return cmd.Run() +} + +func Commit(gistTmpId string) error { + cmd := exec.Command("git", "commit", "--allow-empty", "-m", `"Opengist commit"`) + tmpPath, err := getTmpRepositoryPath(gistTmpId) + if err != nil { + return err + } + cmd.Dir = tmpPath + + return cmd.Run() +} + +func Push(gistTmpId string) error { + tmpRepositoryPath, err := getTmpRepositoryPath(gistTmpId) + if err != nil { + return err + } + cmd := exec.Command( + "git", + "push", + ) + cmd.Dir = tmpRepositoryPath + + err = cmd.Run() + if err != nil { + return err + } + + return os.RemoveAll(tmpRepositoryPath) +} + +func DeleteRepository(user string, gist string) error { + return os.RemoveAll(filepath.Join(config.GetHomeDir(), "repos", strings.ToLower(user), gist)) +} + +func UpdateServerInfo(user string, gist string) error { + repositoryPath, err := GetRepositoryPath(user, gist) + if err != nil { + return err + } + + cmd := exec.Command("git", "update-server-info") + cmd.Dir = repositoryPath + return cmd.Run() +} + +func RPCRefs(user string, gist string, service string) ([]byte, error) { + repositoryPath, err := GetRepositoryPath(user, gist) + if err != nil { + return nil, err + } + + cmd := exec.Command("git", service, "--stateless-rpc", "--advertise-refs", ".") + cmd.Dir = repositoryPath + stdout, err := cmd.Output() + return stdout, err +} + +func GetGitVersion() (string, error) { + cmd := exec.Command("git", "--version") + stdout, err := cmd.Output() + if err != nil { + return "", err + } + + versionFields := strings.Fields(string(stdout)) + if len(versionFields) < 3 { + return string(stdout), nil + } + + return versionFields[2], nil +} diff --git a/internal/models/db.go b/internal/models/db.go new file mode 100644 index 0000000..c6d76bf --- /dev/null +++ b/internal/models/db.go @@ -0,0 +1,31 @@ +package models + +import ( + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var db *gorm.DB + +func Setup(dbpath string) error { + var err error + + if db, err = gorm.Open(sqlite.Open(dbpath+"?_fk=true"), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }); err != nil { + return err + } + + if err = db.AutoMigrate(&User{}, &SSHKey{}, &Gist{}); err != nil { + return err + } + + return nil +} + +func CountAll(table interface{}) (int64, error) { + var count int64 + err := db.Model(table).Count(&count).Error + return count, err +} diff --git a/internal/models/gist.go b/internal/models/gist.go new file mode 100644 index 0000000..a03f8f3 --- /dev/null +++ b/internal/models/gist.go @@ -0,0 +1,136 @@ +package models + +import ( + "time" +) + +type Gist struct { + ID uint `gorm:"primaryKey"` + Uuid string + Title string `validate:"max=50" form:"title"` + Preview string + PreviewFilename string + Description string `validate:"max=150" form:"description"` + Private bool `form:"private"` + UserID uint + User User `validate:"-"` + NbFiles int + NbLikes int + CreatedAt int64 + UpdatedAt int64 + + Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + + Files []File `gorm:"-" validate:"min=1,dive"` +} + +type File struct { + Filename string `validate:"excludes=\x2f,excludes=\x5c,max=50"` + OldFilename string `validate:"excludes=\x2f,excludes=\x5c,max=50"` + Content string `validate:"required"` +} + +type Commit struct { + Hash string + Author string + Timestamp string + Changed string + Files []File +} + +func GetGist(user string, gistUuid string) (*Gist, error) { + gist := new(Gist) + err := db.Preload("User"). + Where("gists.uuid = ? AND users.username like ?", gistUuid, user). + Joins("join users on gists.user_id = users.id"). + First(&gist).Error + + return gist, err +} + +func GetGistByID(gistId string) (*Gist, error) { + gist := new(Gist) + err := db.Preload("User"). + Where("gists.id = ?", gistId). + First(&gist).Error + + return gist, err +} + +func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) { + var gists []*Gist + err := db.Preload("User"). + Where("gists.private = 0 or gists.user_id = ?", currentUserId). + Limit(11). + Offset(offset * 10). + Order(sort + "_at " + order). + Find(&gists).Error + + return gists, err +} + +func GetAllGists(offset int) ([]*Gist, error) { + var all []*Gist + err := db.Preload("User"). + Limit(11). + Offset(offset * 10). + Order("id asc"). + Find(&all).Error + + return all, err +} + +func GetAllGistsFromUser(fromUser string, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) { + var gists []*Gist + err := db.Preload("User"). + Where("users.username = ? and ((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", fromUser, currentUserId). + Joins("join users on gists.user_id = users.id"). + Limit(11). + Offset(offset * 10). + Order("gists." + sort + "_at " + order). + Find(&gists).Error + + return gists, err +} + +func CreateGist(gist *Gist) error { + return db.Create(&gist).Error +} + +func UpdateGist(gist *Gist) error { + return db.Save(&gist).Error +} + +func DeleteGist(gist *Gist) error { + return db.Delete(&gist).Error +} + +func GistLastActiveNow(gistID uint) error { + return db.Model(&Gist{}). + Where("id = ?", gistID). + Update("updated_at", time.Now().Unix()).Error +} + +func AppendUserLike(gist *Gist, user *User) error { + db.Model(&gist).Omit("updated_at").Update("nb_likes", gist.NbLikes+1) + return db.Model(&gist).Omit("updated_at").Association("Likes").Append(user) +} + +func RemoveUserLike(gist *Gist, user *User) error { + db.Model(&gist).Omit("updated_at").Update("nb_likes", gist.NbLikes-1) + return db.Model(&gist).Omit("updated_at").Association("Likes").Delete(user) +} + +func GetUsersLikesForGists(gist *Gist, offset int) ([]*User, error) { + var users []*User + err := db.Model(&gist). + Where("gist_id = ?", gist.ID). + Limit(31). + Offset(offset * 30). + Association("Likes").Find(&users) + return users, err +} + +func UserCanWrite(user *User, gist *Gist) bool { + return !(user == nil) && (gist.UserID == user.ID) +} diff --git a/internal/models/sshkey.go b/internal/models/sshkey.go new file mode 100644 index 0000000..e840d9f --- /dev/null +++ b/internal/models/sshkey.go @@ -0,0 +1,56 @@ +package models + +import "time" + +type SSHKey struct { + ID uint `gorm:"primaryKey"` + Title string `form:"title" validate:"required,max=50"` + Content string `form:"content" validate:"required"` + SHA string + CreatedAt int64 + LastUsedAt int64 + UserID uint + User User `validate:"-" ` +} + +func GetSSHKeysByUserID(userId uint) ([]*SSHKey, error) { + var sshKeys []*SSHKey + err := db. + Where("user_id = ?", userId). + Order("created_at asc"). + Find(&sshKeys).Error + + return sshKeys, err +} + +func GetSSHKeyByID(sshKeyId uint) (*SSHKey, error) { + sshKey := new(SSHKey) + err := db. + Where("id = ?", sshKeyId). + First(&sshKey).Error + + return sshKey, err +} + +func GetSSHKeyByContent(sshKeyContent string) (*SSHKey, error) { + sshKey := new(SSHKey) + err := db. + Where("content like ?", sshKeyContent+"%"). + First(&sshKey).Error + + return sshKey, err +} + +func AddSSHKey(sshKey *SSHKey) error { + return db.Create(&sshKey).Error +} + +func RemoveSSHKey(sshKey *SSHKey) error { + return db.Delete(&sshKey).Error +} + +func SSHKeyLastUsedNow(sshKeyID uint) error { + return db.Model(&SSHKey{}). + Where("id = ?", sshKeyID). + Update("last_used_at", time.Now().Unix()).Error +} diff --git a/internal/models/user.go b/internal/models/user.go new file mode 100644 index 0000000..3a9a2cf --- /dev/null +++ b/internal/models/user.go @@ -0,0 +1,77 @@ +package models + +type User struct { + ID uint `gorm:"primaryKey"` + Username string `form:"username" gorm:"uniqueIndex" validate:"required,max=24,alphanum,notreserved"` + Password string `form:"password" validate:"required"` + IsAdmin bool + CreatedAt int64 + + Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"` + SSHKeys []SSHKey `gorm:"foreignKey:UserID"` + Liked []Gist `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` +} + +func DoesUserExists(userName string, count *int64) error { + return db.Table("users"). + Where("username like ?", userName). + Count(count).Error +} + +func GetAllUsers(offset int) ([]*User, error) { + var all []*User + err := db. + Limit(11). + Offset(offset * 10). + Order("id asc"). + Find(&all).Error + + return all, err +} + +func GetLoginUser(user *User) error { + return db. + Where("username like ?", user.Username). + First(&user).Error +} + +func GetLoginUserById(user *User) error { + return db. + Where("id = ?", user.ID). + First(&user).Error +} + +func CreateUser(user *User) error { + return db.Create(&user).Error +} + +func DeleteUserByID(userid string) error { + return db.Delete(&User{}, "id = ?", userid).Error +} + +func SetAdminUser(user *User) error { + return db.Model(&user).Update("is_admin", true).Error +} + +func GetUserBySSHKeyID(sshKeyId uint) (*User, error) { + user := new(User) + err := db. + Preload("SSHKeys"). + Joins("join ssh_keys on users.id = ssh_keys.user_id"). + Where("ssh_keys.id = ?", sshKeyId). + First(&user).Error + + return user, err +} + +func UserHasLikedGist(user *User, gist *Gist) (bool, error) { + association := db.Model(&gist).Where("user_id = ?", user.ID).Association("Likes") + if association.Error != nil { + return false, association.Error + } + + if association.Count() == 0 { + return false, nil + } + return true, nil +} diff --git a/internal/resources/pre-receive b/internal/resources/pre-receive new file mode 100644 index 0000000..86121c3 --- /dev/null +++ b/internal/resources/pre-receive @@ -0,0 +1,21 @@ +#!/bin/sh + +disallowed_files=() + +while read old_rev new_rev ref +do + for file in $(git diff --name-only $old_rev $new_rev) + do + if [[ $file =~ / ]]; then + disallowed_files+=($file) + fi + done +done + +if [ ${#disallowed_files[@]} -gt 0 ]; then + echo "Pushing files in folders is not allowed:" + for file in "${disallowed_files[@]}"; do + echo " $file" + done + exit 1 +fi diff --git a/internal/ssh/git-ssh.go b/internal/ssh/git-ssh.go new file mode 100644 index 0000000..f7ddd05 --- /dev/null +++ b/internal/ssh/git-ssh.go @@ -0,0 +1,103 @@ +package ssh + +import ( + "errors" + "golang.org/x/crypto/ssh" + "gorm.io/gorm" + "io" + "opengist/internal/git" + "opengist/internal/models" + "os/exec" + "strings" +) + +func runGitCommand(ch ssh.Channel, gitCmd string, keyID uint) error { + verb, args := parseCommand(gitCmd) + if !strings.HasPrefix(verb, "git-") { + verb = "" + } + verb = strings.TrimPrefix(verb, "git-") + + if verb != "upload-pack" && verb != "receive-pack" { + return errors.New("invalid command") + } + + repoFullName := strings.ToLower(strings.Trim(args, "'")) + repoFields := strings.SplitN(repoFullName, "/", 2) + if len(repoFields) != 2 { + return errors.New("invalid gist path") + } + + userName := strings.ToLower(repoFields[0]) + gistName := strings.TrimSuffix(strings.ToLower(repoFields[1]), ".git") + + gist, err := models.GetGist(userName, gistName) + if err != nil { + return errors.New("gist not found") + } + + if verb == "receive-pack" { + user, err := models.GetUserBySSHKeyID(keyID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("unauthorized") + } + errorSsh("Failed to get user by SSH key id", err) + return errors.New("internal server error") + } + + if user.ID != gist.UserID { + return errors.New("unauthorized") + } + } + + _ = models.SSHKeyLastUsedNow(keyID) + + repositoryPath, err := git.GetRepositoryPath(gist.User.Username, gist.Uuid) + if err != nil { + errorSsh("Failed to get repository path", err) + return errors.New("internal server error") + } + + cmd := exec.Command("git", verb, repositoryPath) + cmd.Dir = repositoryPath + + stdin, _ := cmd.StdinPipe() + stdout, _ := cmd.StdoutPipe() + stderr, _ := cmd.StderrPipe() + + if err = cmd.Start(); err != nil { + errorSsh("Failed to start git command", err) + return errors.New("internal server error") + } + + // avoid blocking + go func() { + _, _ = io.Copy(stdin, ch) + }() + _, _ = io.Copy(ch, stdout) + _, _ = io.Copy(ch, stderr) + + err = cmd.Wait() + if err != nil { + errorSsh("Failed to wait for git command", err) + return errors.New("internal server error") + } + + // updatedAt is updated only if serviceType is receive-pack + if verb == "receive-pack" { + _ = models.GistLastActiveNow(gist.ID) + } + + return nil +} + +func parseCommand(cmd string) (string, string) { + split := strings.SplitN(cmd, " ", 2) + + if len(split) != 2 { + return "", "" + } + + return split[0], strings.Replace(split[1], "'/", "'", 1) +} diff --git a/internal/ssh/run.go b/internal/ssh/run.go new file mode 100644 index 0000000..ef9a4d1 --- /dev/null +++ b/internal/ssh/run.go @@ -0,0 +1,153 @@ +package ssh + +import ( + "errors" + "github.com/rs/zerolog/log" + "golang.org/x/crypto/ssh" + "gorm.io/gorm" + "io" + "net" + "opengist/internal/config" + "opengist/internal/models" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "syscall" +) + +func Start() { + if !config.C.SSH.Enabled { + return + } + + sshConfig := &ssh.ServerConfig{ + PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { + pkey, err := models.GetSSHKeyByContent(strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + return &ssh.Permissions{Extensions: map[string]string{"key-id": strconv.Itoa(int(pkey.ID))}}, nil + }, + } + + key, err := setupHostKey() + if err != nil { + log.Fatal().Err(err).Msg("SSH: Could not setup host key") + } + + sshConfig.AddHostKey(key) + go listen(sshConfig) +} + +func listen(serverConfig *ssh.ServerConfig) { + log.Info().Msg("Starting SSH server on ssh://" + config.C.SSH.Host + ":" + config.C.SSH.Port) + listener, err := net.Listen("tcp", config.C.SSH.Host+":"+config.C.SSH.Port) + if err != nil { + log.Fatal().Err(err).Msg("SSH: Failed to start SSH server") + } + defer listener.Close() + + for { + nConn, err := listener.Accept() + if err != nil { + errorSsh("Failed to accept incoming connection", err) + continue + } + + go func() { + sConn, channels, reqs, err := ssh.NewServerConn(nConn, serverConfig) + if err != nil { + if !(err != io.EOF && !errors.Is(err, syscall.ECONNRESET)) { + errorSsh("Failed to handshake", err) + } + return + } + + go ssh.DiscardRequests(reqs) + keyID, _ := strconv.Atoi(sConn.Permissions.Extensions["key-id"]) + go handleConnexion(channels, uint(keyID)) + }() + } +} + +func handleConnexion(channels <-chan ssh.NewChannel, keyID uint) { + for channel := range channels { + if channel.ChannelType() != "session" { + _ = channel.Reject(ssh.UnknownChannelType, "Unknown channel type") + continue + } + + ch, reqs, err := channel.Accept() + if err != nil { + errorSsh("Could not accept channel", err) + continue + } + + go func(in <-chan *ssh.Request) { + defer func() { + _ = ch.Close() + }() + for req := range in { + switch req.Type { + case "env": + + case "shell": + _, _ = ch.Write([]byte("Successfully connected to Opengist SSH server.\r\n")) + _, _ = ch.SendRequest("exit-status", false, []byte{0, 0, 0, 0}) + return + case "exec": + payloadCmd := string(req.Payload) + i := strings.Index(payloadCmd, "git") + if i != -1 { + payloadCmd = payloadCmd[i:] + } + + if err = runGitCommand(ch, payloadCmd, keyID); err != nil { + _, _ = ch.Stderr().Write([]byte("Opengist: " + err.Error() + "\r\n")) + } + _, _ = ch.SendRequest("exit-status", false, []byte{0, 0, 0, 0}) + return + } + } + }(reqs) + } +} + +func setupHostKey() (ssh.Signer, error) { + dir := filepath.Join(config.GetHomeDir(), "ssh") + + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + + keyPath := filepath.Join(dir, "opengist-ed25519") + if _, err := os.Stat(keyPath); err != nil && !os.IsExist(err) { + cmd := exec.Command(config.C.SSH.Keygen, + "-t", "ssh-ed25519", + "-f", keyPath, + "-m", "PEM", + "-N", "") + err = cmd.Run() + if err != nil { + return nil, err + } + } + + keyData, err := os.ReadFile(keyPath) + if err != nil { + return nil, err + } + + signer, err := ssh.ParsePrivateKey(keyData) + if err != nil { + return nil, err + } + + return signer, nil +} + +func errorSsh(message string, err error) { + log.Error().Err(err).Msg("SSH: " + message) +} diff --git a/internal/web/admin.go b/internal/web/admin.go new file mode 100644 index 0000000..c17f399 --- /dev/null +++ b/internal/web/admin.go @@ -0,0 +1,106 @@ +package web + +import ( + "github.com/labstack/echo/v4" + "opengist/internal/config" + "opengist/internal/git" + "opengist/internal/models" + "runtime" +) + +func adminIndex(ctx echo.Context) error { + setData(ctx, "title", "Admin panel") + setData(ctx, "adminHeaderPage", "index") + + setData(ctx, "opengistVersion", config.OpengistVersion) + setData(ctx, "goVersion", runtime.Version()) + gitVersion, err := git.GetGitVersion() + if err != nil { + return errorRes(500, "Cannot get git version", err) + } + setData(ctx, "gitVersion", gitVersion) + + countUsers, err := models.CountAll(&models.User{}) + if err != nil { + return errorRes(500, "Cannot count users", err) + } + setData(ctx, "countUsers", countUsers) + + countGists, err := models.CountAll(&models.Gist{}) + if err != nil { + return errorRes(500, "Cannot count gists", err) + } + setData(ctx, "countGists", countGists) + + countKeys, err := models.CountAll(&models.SSHKey{}) + if err != nil { + return errorRes(500, "Cannot count SSH keys", err) + } + setData(ctx, "countKeys", countKeys) + + return html(ctx, "admin_index.html") +} + +func adminUsers(ctx echo.Context) error { + setData(ctx, "title", "Users") + setData(ctx, "adminHeaderPage", "users") + pageInt := getPage(ctx) + + var data []*models.User + var err error + if data, err = models.GetAllUsers(pageInt - 1); err != nil { + return errorRes(500, "Cannot get users", err) + } + + if err = paginate(ctx, data, pageInt, 10, "data", "admin/users"); err != nil { + return errorRes(404, "Page not found", nil) + } + + return html(ctx, "admin_users.html") +} + +func adminGists(ctx echo.Context) error { + setData(ctx, "title", "Users") + setData(ctx, "adminHeaderPage", "gists") + pageInt := getPage(ctx) + + var data []*models.Gist + var err error + if data, err = models.GetAllGists(pageInt - 1); err != nil { + return errorRes(500, "Cannot get gists", err) + } + + if err = paginate(ctx, data, pageInt, 10, "data", "admin/gists"); err != nil { + return errorRes(404, "Page not found", nil) + } + + return html(ctx, "admin_gists.html") +} + +func adminUserDelete(ctx echo.Context) error { + if err := models.DeleteUserByID(ctx.Param("user")); err != nil { + return errorRes(500, "Cannot delete this user", err) + } + + addFlash(ctx, "User has been deleted", "success") + return redirect(ctx, "/admin/users") +} + +func adminGistDelete(ctx echo.Context) error { + gist, err := models.GetGistByID(ctx.Param("gist")) + if err != nil { + return errorRes(500, "Cannot retrieve gist", err) + } + + if err = git.DeleteRepository(gist.User.Username, gist.Uuid); err != nil { + return errorRes(500, "Cannot delete the repository", err) + } + + if err = models.DeleteGist(gist); err != nil { + return errorRes(500, "Cannot delete this gist", err) + } + + addFlash(ctx, "Gist has been deleted", "success") + return redirect(ctx, "/admin/gists") + +} diff --git a/internal/web/auth.go b/internal/web/auth.go new file mode 100644 index 0000000..ee85b33 --- /dev/null +++ b/internal/web/auth.go @@ -0,0 +1,103 @@ +package web + +import ( + "github.com/labstack/echo/v4" + "opengist/internal/config" + "opengist/internal/models" +) + +func register(ctx echo.Context) error { + setData(ctx, "title", "New account") + setData(ctx, "htmlTitle", "New account") + return html(ctx, "auth_form.html") +} + +func processRegister(ctx echo.Context) error { + if config.C.DisableSignup { + return errorRes(403, "Signing up is disabled", nil) + } + + setData(ctx, "title", "New account") + setData(ctx, "htmlTitle", "New account") + + sess := getSession(ctx) + + var user = new(models.User) + if err := ctx.Bind(user); err != nil { + return errorRes(400, "Cannot bind data", err) + } + + if err := ctx.Validate(user); err != nil { + addFlash(ctx, validationMessages(&err), "error") + return html(ctx, "auth_form.html") + } + + password, err := argon2id.hash(user.Password) + if err != nil { + return errorRes(500, "Cannot hash password", err) + } + user.Password = password + + var count int64 + if err = models.DoesUserExists(user.Username, &count); err != nil || count >= 1 { + addFlash(ctx, "Username already exists", "error") + return html(ctx, "auth_form.html") + } + + if err = models.CreateUser(user); err != nil { + return errorRes(500, "Cannot create user", err) + } + + if user.ID == 1 { + user.IsAdmin = true + if err = models.SetAdminUser(user); err != nil { + return errorRes(500, "Cannot set user admin", err) + } + } + + sess.Values["user"] = user.ID + saveSession(sess, ctx) + + return redirect(ctx, "/") +} + +func login(ctx echo.Context) error { + setData(ctx, "title", "Login") + setData(ctx, "htmlTitle", "Login") + return html(ctx, "auth_form.html") +} + +func processLogin(ctx echo.Context) error { + sess := getSession(ctx) + + user := &models.User{} + if err := ctx.Bind(user); err != nil { + return errorRes(400, "Cannot bind data", err) + } + password := user.Password + + if err := models.GetLoginUser(user); err != nil { + addFlash(ctx, "Invalid credentials", "error") + return redirect(ctx, "/login") + } + + if ok, err := argon2id.verify(password, user.Password); !ok { + if err != nil { + return errorRes(500, "Cannot check for password", err) + } + addFlash(ctx, "Invalid credentials", "error") + return redirect(ctx, "/login") + } + + sess.Values["user"] = user.ID + saveSession(sess, ctx) + deleteCsrfCookie(ctx) + + return redirect(ctx, "/") +} + +func logout(ctx echo.Context) error { + deleteSession(ctx) + deleteCsrfCookie(ctx) + return redirect(ctx, "/all") +} diff --git a/internal/web/gist.go b/internal/web/gist.go new file mode 100644 index 0000000..77d06f6 --- /dev/null +++ b/internal/web/gist.go @@ -0,0 +1,520 @@ +package web + +import ( + "archive/zip" + "bytes" + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "html/template" + "net/url" + "opengist/internal/config" + "opengist/internal/git" + "opengist/internal/models" + "strconv" + "strings" +) + +func gistInit(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + userName := ctx.Param("user") + gistName := ctx.Param("gistname") + + if strings.HasSuffix(gistName, ".git") { + gistName = strings.TrimSuffix(gistName, ".git") + } + + gist, err := models.GetGist(userName, gistName) + if err != nil { + return notFound("Gist not found") + } + setData(ctx, "gist", gist) + + if config.C.SSH.Port == "22" { + setData(ctx, "ssh_clone_url", config.C.SSH.Domain+":"+userName+"/"+gistName+".git") + } else { + setData(ctx, "ssh_clone_url", "ssh://"+config.C.SSH.Domain+":"+config.C.SSH.Port+"/"+userName+"/"+gistName+".git") + } + + setData(ctx, "httpCloneUrl", "http://"+ctx.Request().Host+"/"+userName+"/"+gistName+".git") + setData(ctx, "httpCopyUrl", "http://"+ctx.Request().Host+"/"+userName+"/"+gistName) + + setData(ctx, "currentUrl", template.URL(ctx.Request().URL.Path)) + + nbCommits, err := git.GetNumberOfCommitsOfRepository(userName, gistName) + if err != nil { + return errorRes(500, "Error fetching number of commits", err) + } + setData(ctx, "nbCommits", nbCommits) + + if currUser := getUserLogged(ctx); currUser != nil { + hasLiked, err := models.UserHasLikedGist(currUser, gist) + if err != nil { + return errorRes(500, "Cannot get user like status", err) + } + setData(ctx, "hasLiked", hasLiked) + } + + return next(ctx) + } +} + +func allGists(ctx echo.Context) error { + var err error + fromUser := ctx.Param("user") + userLogged := getUserLogged(ctx) + + pageInt := getPage(ctx) + + sort := "created" + order := "desc" + orderText := "Recently" + + if ctx.QueryParam("sort") == "updated" { + sort = "updated" + } + + if ctx.QueryParam("order") == "asc" { + order = "asc" + orderText = "Least recently" + } + + setData(ctx, "sort", sort) + setData(ctx, "order", orderText) + + var gists []*models.Gist + var currentUserId uint + if userLogged != nil { + currentUserId = userLogged.ID + } else { + currentUserId = 0 + } + if fromUser == "" { + setData(ctx, "htmlTitle", "All gists") + fromUser = "all" + gists, err = models.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order) + } else { + setData(ctx, "htmlTitle", "All gists from "+fromUser) + setData(ctx, "fromUser", fromUser) + + var count int64 + if err = models.DoesUserExists(fromUser, &count); err != nil { + return errorRes(500, "Error fetching user", err) + } + + if count == 0 { + return notFound("User not found") + } + + gists, err = models.GetAllGistsFromUser(fromUser, currentUserId, pageInt-1, sort, order) + } + if err != nil { + return errorRes(500, "Error fetching gists", err) + } + + if err = paginate(ctx, gists, pageInt, 10, "gists", fromUser, "&sort="+sort+"&order="+order); err != nil { + return errorRes(404, "Page not found", nil) + } + + return html(ctx, "all.html") +} + +func gist(ctx echo.Context) error { + gist := getData(ctx, "gist").(*models.Gist) + userName := gist.User.Username + gistName := gist.Uuid + revision := ctx.Param("revision") + + if revision == "" { + revision = "HEAD" + } + + nbCommits := getData(ctx, "nbCommits") + files := make(map[string]string) + if nbCommits != "0" { + filesStr, err := git.GetFilesOfRepository(userName, gistName, revision) + if err != nil { + return errorRes(500, "Error fetching files from repository", err) + } + for _, file := range filesStr { + files[file], err = git.GetFileContent(userName, gistName, revision, file) + if err != nil { + return errorRes(500, "Error fetching file content from file "+file, err) + } + } + } + + setData(ctx, "page", "code") + setData(ctx, "commit", revision) + setData(ctx, "files", files) + setData(ctx, "revision", revision) + setData(ctx, "htmlTitle", gist.Title) + + return html(ctx, "gist.html") +} + +func revisions(ctx echo.Context) error { + gist := getData(ctx, "gist").(*models.Gist) + userName := gist.User.Username + gistName := gist.Uuid + + pageInt := getPage(ctx) + + nbCommits := getData(ctx, "nbCommits") + commits := make([]*models.Commit, 0) + if nbCommits != "0" { + gitlogStr, err := git.GetLog(userName, gistName, strconv.Itoa((pageInt-1)*10)) + if err != nil { + return errorRes(500, "Error fetching commits log", err) + } + + gitlog := strings.Split(gitlogStr, "\n=commit ") + for _, commitStr := range gitlog[1:] { + logContent := strings.SplitN(commitStr, "\n", 3) + + header := strings.Split(logContent[0], ":") + commitStruct := models.Commit{ + Hash: header[0], + Author: header[1], + Timestamp: header[2], + Files: make([]models.File, 0), + } + + if len(logContent) > 2 { + changed := strings.ReplaceAll(logContent[1], "(+)", "") + changed = strings.ReplaceAll(changed, "(-)", "") + commitStruct.Changed = changed + } + + files := strings.Split(logContent[len(logContent)-1], "diff --git ") + if len(files) > 1 { + for _, fileStr := range files { + content := strings.SplitN(fileStr, "\n@@", 2) + if len(content) > 1 { + header := strings.Split(content[0], "\n") + commitStruct.Files = append(commitStruct.Files, models.File{Content: "@@" + content[1], Filename: header[len(header)-1][4:], OldFilename: header[len(header)-2][4:]}) + } else { + // in case there is no content but a file renamed + header := strings.Split(content[0], "\n") + if len(header) > 3 { + commitStruct.Files = append(commitStruct.Files, models.File{Content: "", Filename: header[3][10:], OldFilename: header[2][12:]}) + } + } + } + } + commits = append(commits, &commitStruct) + } + } + + if err := paginate(ctx, commits, pageInt, 10, "commits", userName+"/"+gistName+"/revisions"); err != nil { + return errorRes(404, "Page not found", nil) + } + + setData(ctx, "page", "revisions") + setData(ctx, "revision", "HEAD") + setData(ctx, "htmlTitle", "Revision of "+gist.Title) + + return html(ctx, "revisions.html") +} + +func create(ctx echo.Context) error { + setData(ctx, "htmlTitle", "Create a new gist") + return html(ctx, "create.html") +} + +func processCreate(ctx echo.Context) error { + isCreate := false + if ctx.Request().URL.Path == "/" { + isCreate = true + } + + err := ctx.Request().ParseForm() + if err != nil { + return errorRes(400, "Bad request", err) + } + + var gist *models.Gist + + if isCreate { + gist = new(models.Gist) + setData(ctx, "htmlTitle", "Create a new gist") + } else { + gist = getData(ctx, "gist").(*models.Gist) + setData(ctx, "htmlTitle", "Edit "+gist.Title) + } + + if err := ctx.Bind(gist); err != nil { + return errorRes(400, "Cannot bind data", err) + } + + gist.Files = make([]models.File, 0) + for i := 0; i < len(ctx.Request().PostForm["content"]); i++ { + name := ctx.Request().PostForm["name"][i] + content := ctx.Request().PostForm["content"][i] + + if name == "" { + name = "gistfile" + strconv.Itoa(i+1) + ".txt" + } + + escapedValue, err := url.QueryUnescape(content) + if err != nil { + return errorRes(400, "Invalid character unescaped", err) + } + + gist.Files = append(gist.Files, models.File{ + Filename: name, + Content: escapedValue, + }) + } + user := getUserLogged(ctx) + gist.NbFiles = len(gist.Files) + + if isCreate { + uuidGist, err := uuid.NewRandom() + if err != nil { + return errorRes(500, "Error creating an UUID", err) + } + gist.Uuid = strings.Replace(uuidGist.String(), "-", "", -1) + + gist.UserID = user.ID + } + + if gist.Title == "" { + if ctx.Request().PostForm["name"][0] == "" { + gist.Title = "gist:" + gist.Uuid + } else { + gist.Title = ctx.Request().PostForm["name"][0] + } + } + + err = ctx.Validate(gist) + if err != nil { + addFlash(ctx, validationMessages(&err), "error") + if isCreate { + return html(ctx, "create.html") + } else { + files := make(map[string]string) + filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, "HEAD") + if err != nil { + return errorRes(500, "Error fetching files from repository", err) + } + for _, file := range filesStr { + files[file], err = git.GetFileContent(gist.User.Username, gist.Uuid, "HEAD", file) + if err != nil { + return errorRes(500, "Error fetching file content from file "+file, err) + } + } + + setData(ctx, "files", files) + return html(ctx, "edit.html") + } + } + + if len(gist.Files) > 0 { + split := strings.Split(gist.Files[0].Content, "\n") + if len(split) > 10 { + gist.Preview = strings.Join(split[:10], "\n") + } else { + gist.Preview = gist.Files[0].Content + } + + gist.PreviewFilename = gist.Files[0].Filename + } + + if err = git.InitRepository(user.Username, gist.Uuid); err != nil { + return errorRes(500, "Error creating the repository", err) + } + + if err = git.CloneTmp(user.Username, gist.Uuid, gist.Uuid); err != nil { + return errorRes(500, "Error cloning the repository", err) + } + + for _, file := range gist.Files { + if err = git.SetFileContent(gist.Uuid, file.Filename, file.Content); err != nil { + return errorRes(500, "Error setting file content for file "+file.Filename, err) + } + } + + if err = git.AddAll(gist.Uuid); err != nil { + return errorRes(500, "Error adding files to the repository", err) + } + + if err = git.Commit(gist.Uuid); err != nil { + return errorRes(500, "Error committing files to the local repository", err) + } + + if err = git.Push(gist.Uuid); err != nil { + return errorRes(500, "Error pushing the local repository", err) + } + + if isCreate { + if err = models.CreateGist(gist); err != nil { + return errorRes(500, "Error creating the gist", err) + } + } else { + if err = models.UpdateGist(gist); err != nil { + return errorRes(500, "Error updating the gist", err) + } + } + + return redirect(ctx, "/"+user.Username+"/"+gist.Uuid) +} + +func toggleVisibility(ctx echo.Context) error { + var gist = getData(ctx, "gist").(*models.Gist) + + gist.Private = !gist.Private + if err := models.UpdateGist(gist); err != nil { + return errorRes(500, "Error updating this gist", err) + } + + addFlash(ctx, "Gist visibility has been changed", "success") + return redirect(ctx, "/"+gist.User.Username+"/"+gist.Uuid) +} + +func deleteGist(ctx echo.Context) error { + var gist = getData(ctx, "gist").(*models.Gist) + + err := git.DeleteRepository(gist.User.Username, gist.Uuid) + if err != nil { + return errorRes(500, "Error deleting the repository", err) + } + + if err := models.DeleteGist(gist); err != nil { + return errorRes(500, "Error deleting this gist", err) + } + + addFlash(ctx, "Gist has been deleted", "success") + return redirect(ctx, "/") +} + +func like(ctx echo.Context) error { + var gist = getData(ctx, "gist").(*models.Gist) + currentUser := getUserLogged(ctx) + + hasLiked, err := models.UserHasLikedGist(currentUser, gist) + if err != nil { + return errorRes(500, "Error checking if user has liked a gist", err) + } + + if hasLiked { + err = models.RemoveUserLike(gist, getUserLogged(ctx)) + } else { + err = models.AppendUserLike(gist, getUserLogged(ctx)) + } + + if err != nil { + return errorRes(500, "Error liking/dislking this gist", err) + } + + redirectTo := "/" + gist.User.Username + "/" + gist.Uuid + if r := ctx.QueryParam("redirecturl"); r != "" { + redirectTo = r + } + return redirect(ctx, redirectTo) +} + +func rawFile(ctx echo.Context) error { + gist := getData(ctx, "gist").(*models.Gist) + fileContent, err := git.GetFileContent( + gist.User.Username, + gist.Uuid, + ctx.Param("revision"), + ctx.Param("file")) + if err != nil { + return errorRes(500, "Error getting file content", err) + } + + filebytes := []byte(fileContent) + + if len(filebytes) == 0 { + return notFound("File not found") + } + + return plainText(ctx, 200, string(filebytes)) +} + +func edit(ctx echo.Context) error { + var gist = getData(ctx, "gist").(*models.Gist) + + files := make(map[string]string) + filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, "HEAD") + if err != nil { + return errorRes(500, "Error fetching files from repository", err) + } + for _, file := range filesStr { + files[file], err = git.GetFileContent(gist.User.Username, gist.Uuid, "HEAD", file) + if err != nil { + return errorRes(500, "Error fetching file content from file "+file, err) + } + } + + setData(ctx, "files", files) + setData(ctx, "htmlTitle", "Edit "+gist.Title) + + return html(ctx, "edit.html") +} + +func downloadZip(ctx echo.Context) error { + var gist = getData(ctx, "gist").(*models.Gist) + var revision = ctx.Param("revision") + + files := make(map[string]string) + filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision) + if err != nil { + return errorRes(500, "Error fetching files from repository", err) + } + + for _, file := range filesStr { + files[file], err = git.GetFileContent(gist.User.Username, gist.Uuid, revision, file) + if err != nil { + return errorRes(500, "Error fetching file content from file "+file, err) + } + } + + zipFile := new(bytes.Buffer) + + zipWriter := zip.NewWriter(zipFile) + + for fileName, fileContent := range files { + f, err := zipWriter.Create(fileName) + if err != nil { + return errorRes(500, "Error adding a file the to the zip archive", err) + } + _, err = f.Write([]byte(fileContent)) + if err != nil { + return errorRes(500, "Error adding file content the to the zip archive", err) + } + } + err = zipWriter.Close() + if err != nil { + return errorRes(500, "Error closing the zip archive", err) + } + + ctx.Response().Header().Set("Content-Type", "application/zip") + ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+gist.Uuid+".zip") + ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(zipFile.Bytes()))) + _, err = ctx.Response().Write(zipFile.Bytes()) + if err != nil { + return errorRes(500, "Error writing the zip archive", err) + } + return nil +} + +func likes(ctx echo.Context) error { + var gist = getData(ctx, "gist").(*models.Gist) + + pageInt := getPage(ctx) + + likers, err := models.GetUsersLikesForGists(gist, pageInt-1) + if err != nil { + return errorRes(500, "Error getting users who liked this gist", err) + } + + if err = paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Uuid+"/likes"); err != nil { + return errorRes(404, "Page not found", nil) + } + + setData(ctx, "htmlTitle", "Likes for "+gist.Title) + setData(ctx, "revision", "HEAD") + return html(ctx, "likes.html") +} diff --git a/internal/web/git-http.go b/internal/web/git-http.go new file mode 100644 index 0000000..e5b5f39 --- /dev/null +++ b/internal/web/git-http.go @@ -0,0 +1,253 @@ +package web + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "github.com/labstack/echo/v4" + "net/http" + "opengist/internal/git" + "opengist/internal/models" + "os" + "os/exec" + "path" + "regexp" + "strconv" + "strings" + "time" +) + +var routes = []struct { + gitUrl string + method string + handler func(ctx echo.Context) error +}{ + {"(.*?)/git-upload-pack$", "POST", uploadPack}, + {"(.*?)/git-receive-pack$", "POST", receivePack}, + {"(.*?)/info/refs$", "GET", infoRefs}, + {"(.*?)/HEAD$", "GET", textFile}, + {"(.*?)/objects/info/alternates$", "GET", textFile}, + {"(.*?)/objects/info/http-alternates$", "GET", textFile}, + {"(.*?)/objects/info/packs$", "GET", infoPacks}, + {"(.*?)/objects/info/[^/]*$", "GET", textFile}, + {"(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$", "GET", looseObject}, + {"(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$", "GET", packFile}, + {"(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$", "GET", idxFile}, +} + +func gitHttp(ctx echo.Context) error { + for _, route := range routes { + matched, _ := regexp.MatchString(route.gitUrl, ctx.Request().URL.Path) + if ctx.Request().Method == route.method && matched { + if !strings.HasPrefix(ctx.Request().Header.Get("User-Agent"), "git/") { + continue + } + + gist := getData(ctx, "gist").(*models.Gist) + + noAuth := ctx.QueryParam("service") == "git-upload-pack" || + strings.HasSuffix(ctx.Request().URL.Path, "git-upload-pack") || + ctx.Request().Method == "GET" + + repositoryPath, err := git.GetRepositoryPath(gist.User.Username, gist.Uuid) + if err != nil { + return errorRes(500, "Cannot get repository path", err) + } + + if _, err = os.Stat(repositoryPath); os.IsNotExist(err) { + if err != nil { + return errorRes(500, "Repository does not exist", err) + } + } + + ctx.Set("repositoryPath", repositoryPath) + + // Requires Basic Auth if we push the repository + if noAuth { + return route.handler(ctx) + } + + authHeader := ctx.Request().Header.Get("Authorization") + if authHeader == "" { + return basicAuth(ctx) + } + + authFields := strings.Fields(authHeader) + if len(authFields) != 2 || authFields[0] != "Basic" { + return basicAuth(ctx) + } + + authUsername, authPassword, err := basicAuthDecode(authFields[1]) + if err != nil { + return basicAuth(ctx) + } + + if ok, err := argon2id.verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername { + if err != nil { + return errorRes(500, "Cannot verify password", err) + } + return errorRes(403, "Unauthorized", nil) + } + + return route.handler(ctx) + } + } + return notFound("Gist not found") +} + +func uploadPack(ctx echo.Context) error { + return pack(ctx, "upload-pack") +} + +func receivePack(ctx echo.Context) error { + return pack(ctx, "receive-pack") +} + +func pack(ctx echo.Context, serviceType string) error { + noCacheHeaders(ctx) + defer ctx.Request().Body.Close() + + if ctx.Request().Header.Get("Content-Type") != "application/x-git-"+serviceType+"-request" { + return errorRes(401, "Git client unsupported", nil) + } + ctx.Response().Header().Set("Content-Type", "application/x-git-"+serviceType+"-result") + + var err error + reqBody := ctx.Request().Body + + if ctx.Request().Header.Get("Content-Encoding") == "gzip" { + reqBody, err = gzip.NewReader(reqBody) + if err != nil { + return errorRes(500, "Cannot create gzip reader", err) + } + } + + repositoryPath := ctx.Get("repositoryPath").(string) + + var stderr bytes.Buffer + cmd := exec.Command("git", serviceType, "--stateless-rpc", repositoryPath) + cmd.Dir = repositoryPath + cmd.Stdin = reqBody + cmd.Stdout = ctx.Response().Writer + cmd.Stderr = &stderr + if err = cmd.Run(); err != nil { + return errorRes(500, "Cannot run git "+serviceType+" ; "+stderr.String(), err) + } + + // updatedAt is updated only if serviceType is receive-pack + if serviceType == "receive-pack" { + _ = models.GistLastActiveNow(getData(ctx, "gist").(*models.Gist).ID) + } + return nil +} + +func infoRefs(ctx echo.Context) error { + noCacheHeaders(ctx) + var service string + + gist := getData(ctx, "gist").(*models.Gist) + + serviceType := ctx.QueryParam("service") + if !strings.HasPrefix(serviceType, "git-") { + service = "" + } + service = strings.TrimPrefix(serviceType, "git-") + + if service != "upload-pack" && service != "receive-pack" { + if err := git.UpdateServerInfo(gist.User.Username, gist.Uuid); err != nil { + return errorRes(500, "Cannot update server info", err) + } + return sendFile(ctx, "text/plain; charset=utf-8") + } + + refs, err := git.RPCRefs(gist.User.Username, gist.Uuid, service) + if err != nil { + return errorRes(500, "Cannot run git "+service, err) + } + + ctx.Response().Header().Set("Content-Type", "application/x-git-"+service+"-advertisement") + ctx.Response().WriteHeader(200) + _, _ = ctx.Response().Write(packetWrite("# service=git-" + service + "\n")) + _, _ = ctx.Response().Write([]byte("0000")) + _, _ = ctx.Response().Write(refs) + + return nil +} + +func textFile(ctx echo.Context) error { + noCacheHeaders(ctx) + return sendFile(ctx, "text/plain") +} + +func infoPacks(ctx echo.Context) error { + cacheHeadersForever(ctx) + return sendFile(ctx, "text/plain; charset=utf-8") +} + +func looseObject(ctx echo.Context) error { + cacheHeadersForever(ctx) + return sendFile(ctx, "application/x-git-loose-object") +} + +func packFile(ctx echo.Context) error { + cacheHeadersForever(ctx) + return sendFile(ctx, "application/x-git-packed-objects") +} + +func idxFile(ctx echo.Context) error { + cacheHeadersForever(ctx) + return sendFile(ctx, "application/x-git-packed-objects-toc") +} + +func noCacheHeaders(ctx echo.Context) { + ctx.Response().Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 UTC") + ctx.Response().Header().Set("Pragma", "no-cache") + ctx.Response().Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") +} + +func cacheHeadersForever(ctx echo.Context) { + now := time.Now().Unix() + expires := now + 31536000 + ctx.Response().Header().Set("Date", fmt.Sprintf("%d", now)) + ctx.Response().Header().Set("Expires", fmt.Sprintf("%d", expires)) + ctx.Response().Header().Set("Cache-Control", "public, max-age=31536000") +} + +func basicAuth(ctx echo.Context) error { + ctx.Response().Header().Set("WWW-Authenticate", `Basic realm="."`) + return plainText(ctx, 401, "Requires authentication") +} + +func basicAuthDecode(encoded string) (string, string, error) { + s, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return "", "", err + } + + auth := strings.SplitN(string(s), ":", 2) + return auth[0], auth[1], nil +} + +func sendFile(ctx echo.Context, contentType string) error { + gitFile := "/" + strings.Join(strings.Split(ctx.Request().URL.Path, "/")[3:], "/") + gitFile = path.Join(ctx.Get("repositoryPath").(string), gitFile) + fi, err := os.Stat(gitFile) + if os.IsNotExist(err) { + return errorRes(404, "File not found", nil) + } + ctx.Response().Header().Set("Content-Type", contentType) + ctx.Response().Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size())) + ctx.Response().Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat)) + return ctx.File(gitFile) +} + +func packetWrite(str string) []byte { + s := strconv.FormatInt(int64(len(str)+4), 16) + + if len(s)%4 != 0 { + s = strings.Repeat("0", 4-len(s)%4) + s + } + + return []byte(s + str) +} diff --git a/internal/web/run.go b/internal/web/run.go new file mode 100644 index 0000000..3a2835a --- /dev/null +++ b/internal/web/run.go @@ -0,0 +1,255 @@ +package web + +import ( + "context" + "fmt" + "github.com/gorilla/sessions" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/rs/zerolog/log" + "html/template" + "io" + "net/http" + "opengist/internal/config" + "opengist/internal/models" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" +) + +var store *sessions.CookieStore +var re = regexp.MustCompile("[^a-z0-9]+") + +type Template struct { + templates *template.Template +} + +func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Context) error { + return t.templates.ExecuteTemplate(w, name, data) +} + +func Start() { + store = sessions.NewCookieStore([]byte("opengist")) + + e := echo.New() + e.HideBanner = true + e.HidePort = true + + e.Use(dataInit) + e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ + Getter: middleware.MethodFromForm("_method"), + })) + e.Pre(middleware.RemoveTrailingSlash()) + e.Use(middleware.CORS()) + e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogURI: true, LogStatus: true, LogMethod: true, + LogValuesFunc: func(ctx echo.Context, v middleware.RequestLoggerValues) error { + log.Info().Str("URI", v.URI).Int("status", v.Status).Str("method", v.Method). + Msg("HTTP") + return nil + }, + })) + e.Use(middleware.Recover()) + e.Use(middleware.Secure()) + + e.Renderer = &Template{ + templates: template.Must(template.New("t").Funcs( + template.FuncMap{ + "split": strings.Split, + "indexByte": strings.IndexByte, + "toInt": func(i string) int64 { + val, _ := strconv.ParseInt(i, 10, 64) + return val + }, + "inc": func(i int64) int64 { + return i + 1 + }, + "splitGit": func(i string) []string { + return strings.FieldsFunc(i, func(r rune) bool { + return r == ',' || r == ' ' + }) + }, + "lines": func(i string) []string { + return strings.Split(i, "\n") + }, + "isMarkdown": func(i string) bool { + return ".md" == strings.ToLower(filepath.Ext(i)) + }, + "httpStatusText": http.StatusText, + "loadedTime": func(startTime time.Time) string { + return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" + }, + "slug": func(s string) string { + return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-") + }, + }).ParseGlob("templates/*/*.html")), + } + + e.HTTPErrorHandler = func(er error, ctx echo.Context) { + if err, ok := er.(*echo.HTTPError); ok { + if err.Code >= 500 { + log.Error().Int("code", err.Code).Err(err.Internal).Msg("HTTP: " + err.Message.(string)) + } + + setData(ctx, "error", err) + if errHtml := htmlWithCode(ctx, err.Code, "error.html"); errHtml != nil { + log.Fatal().Err(errHtml).Send() + } + } else { + log.Fatal().Err(er).Send() + } + } + + e.Use(basicInit) + + e.Validator = NewValidator() + + e.Static("/assets", "./public/assets") + + // Web based routes + g1 := e.Group("") + { + g1.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "form:_csrf", + CookiePath: "/", + CookieHTTPOnly: true, + CookieSameSite: http.SameSiteStrictMode, + })) + g1.Use(csrfInit) + + g1.GET("/", create, logged) + g1.POST("/", processCreate, logged) + + g1.GET("/register", register) + g1.POST("/register", processRegister) + g1.GET("/login", login) + g1.POST("/login", processLogin) + g1.GET("/logout", logout) + + g1.GET("/ssh-keys", sshKeys, logged) + g1.POST("/ssh-keys", sshKeysProcess, logged) + g1.DELETE("/ssh-keys/:id", sshKeysDelete, logged) + + g2 := g1.Group("/admin") + { + g2.Use(adminPermission) + g2.GET("", adminIndex) + g2.GET("/users", adminUsers) + g2.POST("/users/:user/delete", adminUserDelete) + g2.GET("/gists", adminGists) + g2.POST("/gists/:gist/delete", adminGistDelete) + } + + g1.GET("/all", allGists) + g1.GET("/:user", allGists) + + g3 := g1.Group("/:user/:gistname") + { + g3.Use(gistInit) + g3.GET("", gist) + g3.GET("/rev/:revision", gist) + g3.GET("/revisions", revisions) + g3.GET("/archive/:revision", downloadZip) + g3.POST("/visibility", toggleVisibility, logged, writePermission) + g3.POST("/delete", deleteGist, logged, writePermission) + g3.GET("/raw/:revision/:file", rawFile) + g3.GET("/edit", edit, logged, writePermission) + g3.POST("/edit", processCreate, logged, writePermission) + g3.POST("/like", like, logged) + g3.GET("/likes", likes) + } + } + + debugStr := "" + // Git HTTP routes + if config.C.HTTP.Git { + e.Any("/:user/:gistname/*", gitHttp, gistInit) + debugStr = " (with Git HTTP support)" + } + + e.Any("/*", noRouteFound) + + addr := config.C.HTTP.Host + ":" + config.C.HTTP.Port + log.Info().Msg("Starting HTTP server on http://" + addr + debugStr) + + if err := e.Start(addr); err != nil { + log.Fatal().Err(err).Msg("Failed to start HTTP server") + } +} + +func dataInit(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + ctxValue := context.WithValue(ctx.Request().Context(), "data", echo.Map{}) + ctx.SetRequest(ctx.Request().WithContext(ctxValue)) + setData(ctx, "loadStartTime", time.Now()) + return next(ctx) + } +} + +func basicInit(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + setData(ctx, "signupDisabled", config.C.DisableSignup) + + sess := getSession(ctx) + if sess.Values["user"] != nil { + user := &models.User{ID: sess.Values["user"].(uint)} + if err := models.GetLoginUserById(user); err != nil { + sess.Values["user"] = nil + saveSession(sess, ctx) + setData(ctx, "userLogged", nil) + return redirect(ctx, "/all") + } + if user != nil { + setData(ctx, "userLogged", user) + } + return next(ctx) + } + + setData(ctx, "userLogged", nil) + return next(ctx) + } +} + +func csrfInit(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + setCsrfHtmlForm(ctx) + return next(ctx) + } +} + +func writePermission(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + gist := getData(ctx, "gist") + user := getUserLogged(ctx) + if !models.UserCanWrite(user, gist.(*models.Gist)) { + return redirect(ctx, "/"+gist.(*models.Gist).User.Username+"/"+gist.(*models.Gist).Uuid) + } + return next(ctx) + } +} + +func adminPermission(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + user := getUserLogged(ctx) + if user == nil || !user.IsAdmin { + return notFound("User not found") + } + return next(ctx) + } +} + +func logged(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + user := getUserLogged(ctx) + if user != nil { + return next(ctx) + } + return redirect(ctx, "/all") + } +} + +func noRouteFound(echo.Context) error { + return notFound("Page not found") +} diff --git a/internal/web/ssh.go b/internal/web/ssh.go new file mode 100644 index 0000000..47dfb30 --- /dev/null +++ b/internal/web/ssh.go @@ -0,0 +1,79 @@ +package web + +import ( + "crypto/sha256" + "encoding/base64" + "github.com/labstack/echo/v4" + "golang.org/x/crypto/ssh" + "opengist/internal/models" + "strconv" +) + +func sshKeys(ctx echo.Context) error { + user := getUserLogged(ctx) + + keys, err := models.GetSSHKeysByUserID(user.ID) + if err != nil { + return errorRes(500, "Cannot get SSH keys", err) + } + + setData(ctx, "sshKeys", keys) + setData(ctx, "htmlTitle", "Manage SSH keys") + return html(ctx, "ssh_keys.html") +} + +func sshKeysProcess(ctx echo.Context) error { + setData(ctx, "htmlTitle", "Manage SSH keys") + + user := getUserLogged(ctx) + + var key = new(models.SSHKey) + if err := ctx.Bind(key); err != nil { + return errorRes(400, "Cannot bind data", err) + } + + if err := ctx.Validate(key); err != nil { + addFlash(ctx, validationMessages(&err), "error") + return redirect(ctx, "/ssh-keys") + } + + key.UserID = user.ID + + pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content)) + if err != nil { + addFlash(ctx, "Invalid SSH key", "error") + return redirect(ctx, "/ssh-keys") + } + + sha := sha256.Sum256(pubKey.Marshal()) + key.SHA = base64.StdEncoding.EncodeToString(sha[:]) + + if err := models.AddSSHKey(key); err != nil { + return errorRes(500, "Cannot add SSH key", err) + } + + addFlash(ctx, "SSH key added", "success") + return redirect(ctx, "/ssh-keys") +} + +func sshKeysDelete(ctx echo.Context) error { + user := getUserLogged(ctx) + keyId, err := strconv.Atoi(ctx.Param("id")) + + if err != nil { + return redirect(ctx, "/ssh-keys") + } + + key, err := models.GetSSHKeyByID(uint(keyId)) + + if err != nil || key.UserID != user.ID { + return redirect(ctx, "/ssh-keys") + } + + if err := models.RemoveSSHKey(key); err != nil { + return errorRes(500, "Cannot delete SSH key", err) + } + + addFlash(ctx, "SSH key deleted", "success") + return redirect(ctx, "/ssh-keys") +} diff --git a/internal/web/util.go b/internal/web/util.go new file mode 100644 index 0000000..6228691 --- /dev/null +++ b/internal/web/util.go @@ -0,0 +1,254 @@ +package web + +import ( + "context" + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "errors" + "fmt" + "github.com/go-playground/validator/v10" + "github.com/gorilla/sessions" + "github.com/labstack/echo/v4" + "golang.org/x/crypto/argon2" + "html/template" + "net/http" + "opengist/internal/models" + "strconv" + "strings" +) + +func setData(ctx echo.Context, key string, value any) { + data := ctx.Request().Context().Value("data").(echo.Map) + data[key] = value + ctxValue := context.WithValue(ctx.Request().Context(), "data", data) + ctx.SetRequest(ctx.Request().WithContext(ctxValue)) +} + +func getData(ctx echo.Context, key string) any { + data := ctx.Request().Context().Value("data").(echo.Map) + return data[key] +} + +func html(ctx echo.Context, template string) error { + return htmlWithCode(ctx, 200, template) +} + +func htmlWithCode(ctx echo.Context, code int, template string) error { + setErrorFlashes(ctx) + return ctx.Render(code, template, ctx.Request().Context().Value("data")) +} + +func redirect(ctx echo.Context, location string) error { + return ctx.Redirect(302, location) +} + +func plainText(ctx echo.Context, code int, message string) error { + return ctx.String(code, message) +} + +func notFound(message string) error { + return errorRes(404, message, nil) +} + +func errorRes(code int, message string, err error) error { + return &echo.HTTPError{Code: code, Message: message, Internal: err} +} + +func getUserLogged(ctx echo.Context) *models.User { + user := getData(ctx, "userLogged") + if user != nil { + return user.(*models.User) + } + return nil +} + +func setErrorFlashes(ctx echo.Context) { + sess, _ := store.Get(ctx.Request(), "flash") + + setData(ctx, "flashErrors", sess.Flashes("error")) + setData(ctx, "flashSuccess", sess.Flashes("success")) + + _ = sess.Save(ctx.Request(), ctx.Response()) +} + +func addFlash(ctx echo.Context, flashMessage string, flashType string) { + sess, _ := store.Get(ctx.Request(), "flash") + sess.AddFlash(flashMessage, flashType) + _ = sess.Save(ctx.Request(), ctx.Response()) +} + +func getSession(ctx echo.Context) *sessions.Session { + sess, _ := store.Get(ctx.Request(), "session") + return sess +} + +func saveSession(sess *sessions.Session, ctx echo.Context) { + _ = sess.Save(ctx.Request(), ctx.Response()) +} + +func deleteSession(ctx echo.Context) { + sess := getSession(ctx) + sess.Options.MaxAge = -1 + sess.Values["user"] = nil + saveSession(sess, ctx) +} + +func setCsrfHtmlForm(ctx echo.Context) { + if csrfToken, ok := ctx.Get("csrf").(string); ok { + setData(ctx, "csrfHtml", template.HTML(``)) + } +} + +func deleteCsrfCookie(ctx echo.Context) { + ctx.SetCookie(&http.Cookie{Name: "_csrf", Path: "/", MaxAge: -1}) +} + +type OpengistValidator struct { + v *validator.Validate +} + +func NewValidator() *OpengistValidator { + v := validator.New() + _ = v.RegisterValidation("notreserved", validateReservedKeywords) + return &OpengistValidator{v} +} + +func (cv *OpengistValidator) Validate(i interface{}) error { + if err := cv.v.Struct(i); err != nil { + return err + } + return nil +} + +func validationMessages(err *error) string { + errs := (*err).(validator.ValidationErrors) + messages := make([]string, len(errs)) + for i, e := range errs { + switch e.Tag() { + case "max": + messages[i] = e.Field() + " is too long" + case "required": + messages[i] = e.Field() + " should not be empty" + case "excludes": + messages[i] = e.Field() + " should not include a sub directory" + case "alphanum": + messages[i] = e.Field() + " should only contain alphanumeric characters" + case "min": + messages[i] = "Not enough " + e.Field() + case "notreserved": + messages[i] = "Invalid " + e.Field() + } + } + + return strings.Join(messages, " ; ") +} + +func validateReservedKeywords(fl validator.FieldLevel) bool { + name := fl.Field().String() + + restrictedNames := map[string]struct{}{} + for _, restrictedName := range []string{"register", "login", "logout", "ssh-keys", "admin", "all"} { + restrictedNames[restrictedName] = struct{}{} + } + + // if the name is not in the restricted names, it is valid + _, ok := restrictedNames[name] + return !ok +} + +func getPage(ctx echo.Context) int { + page := ctx.QueryParam("page") + if page == "" { + page = "1" + } + pageInt, err := strconv.Atoi(page) + if err != nil { + pageInt = 1 + } + setData(ctx, "currPage", pageInt) + + return pageInt +} + +func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, templateDataName string, urlPage string, urlParams ...string) error { + lenData := len(data) + if lenData == 0 && pageInt != 1 { + return errors.New("page not found") + } + + if lenData > perPage { + if lenData > 1 { + data = data[:lenData-1] + } + setData(ctx, "nextPage", pageInt+1) + } + if pageInt > 1 { + setData(ctx, "prevPage", pageInt-1) + } + + if len(urlParams) > 0 { + setData(ctx, "urlParams", template.URL(urlParams[0])) + } + + setData(ctx, "urlPage", urlPage) + setData(ctx, templateDataName, data) + return nil +} + +type Argon2ID struct { + format string + version int + time uint32 + memory uint32 + keyLen uint32 + saltLen uint32 + threads uint8 +} + +var argon2id = Argon2ID{ + format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + version: argon2.Version, + time: 1, + memory: 64 * 1024, + keyLen: 32, + saltLen: 16, + threads: 4, +} + +func (a Argon2ID) hash(plain string) (string, error) { + salt := make([]byte, a.saltLen) + if _, err := rand.Read(salt); err != nil { + return "", err + } + + hash := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, a.keyLen) + + return fmt.Sprintf(a.format, a.version, a.memory, a.time, a.threads, + base64.RawStdEncoding.EncodeToString(salt), + base64.RawStdEncoding.EncodeToString(hash), + ), nil +} + +func (a Argon2ID) verify(plain, hash string) (bool, error) { + hashParts := strings.Split(hash, "$") + + _, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads) + if err != nil { + return false, err + } + + salt, err := base64.RawStdEncoding.DecodeString(hashParts[4]) + if err != nil { + return false, err + } + + decodedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5]) + if err != nil { + return false, err + } + + hashToCompare := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, uint32(len(decodedHash))) + + return subtle.ConstantTimeCompare(decodedHash, hashToCompare) == 1, nil +} diff --git a/opengist.go b/opengist.go new file mode 100644 index 0000000..8b8a157 --- /dev/null +++ b/opengist.go @@ -0,0 +1,54 @@ +package main + +import ( + "flag" + "github.com/rs/zerolog/log" + "opengist/internal/config" + "opengist/internal/models" + "opengist/internal/ssh" + "opengist/internal/web" + "os" + "path/filepath" +) + +func initialize() { + configPath := flag.String("config", "config.yml", "Path to a config file in YML format") + flag.Parse() + absolutePath, _ := filepath.Abs(*configPath) + absolutePath = filepath.Clean(absolutePath) + if err := config.InitConfig(absolutePath); err != nil { + panic(err) + } + if err := os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755); err != nil { + panic(err) + } + + config.InitLog() + + log.Info().Msg("Opengist v" + config.OpengistVersion) + log.Info().Msg("Using config file: " + absolutePath) + + homePath := config.GetHomeDir() + log.Info().Msg("Data directory: " + homePath) + + if err := os.MkdirAll(filepath.Join(homePath, "repos"), 0755); err != nil { + log.Fatal().Err(err).Send() + } + if err := os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755); err != nil { + log.Fatal().Err(err).Send() + } + + log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename)) + if err := models.Setup(filepath.Join(homePath, config.C.DBFilename)); err != nil { + log.Fatal().Err(err).Msg("Failed to initialize database") + } +} + +func main() { + initialize() + + go web.Start() + go ssh.Start() + + select {} +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..68d9c73 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2705 @@ +{ + "name": "opengist", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "opengist", + "version": "1.0.0", + "devDependencies": { + "@codemirror/commands": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/view": "^6.0.1", + "@tailwindcss/forms": "^0.5.1", + "@tailwindcss/typography": "^0.5.2", + "autoprefixer": "^10.4.7", + "codemirror": "^6.0.0", + "highlight.js": "^11.5.1", + "markdown-it": "^13.0.1", + "moment": "^2.29.3", + "postcss": "^8.4.13", + "tailwindcss": "^3.0.24", + "vite": "^2.9.2" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.0.2.tgz", + "integrity": "sha512-9PDjnllmXan/7Uax87KGORbxerDJ/cu10SB+n4Jz0zXMEvIh3+TGgZxhIvDOtaQ4jDBQEM7kHYW4vLdQB0DGZQ==", + "dev": true, + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.0.0.tgz", + "integrity": "sha512-nVJDPiCQXWXj5AZxqNVXyIM3nOYauF4Dko9NGPSwgVdK+lXWJQhI5LGhS/AvdG5b7u7/pTQBkrQmzkLWRBF62A==", + "dev": true, + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.0.0.tgz", + "integrity": "sha512-IQLfR+pgydwLxP3AQYOhjGfLWlAZz5SEH+M1tnUXVvp2+KZhn3iDTQX/7HZgoJ3w0oySKOxPjdq6jmkTCW6/sg==", + "dev": true, + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.1.0.tgz", + "integrity": "sha512-CeqY80nvUFrJcXcBW115aNi06D0PS8NSW6nuJRSwbrYFkE0SfJnPfyLGrcM90AV95lqg5+4xUi99BCmzNaPGJg==", + "dev": true, + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.0.0.tgz", + "integrity": "sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==", + "dev": true, + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.0.0.tgz", + "integrity": "sha512-rL0rd3AhI0TAsaJPUaEwC63KHLO7KL0Z/dYozXj6E7L3wNHRyx7RfE0/j5HsIf912EE5n2PCb4Vg0rGYmDv4UQ==", + "dev": true, + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.0.1.tgz", + "integrity": "sha512-6vYgaXc4KjSY0BUfSVDJooGcoswg/RJZpq/ZGjsUYmY0KN1lmB8u03nv+jiG1ncUV5qoggyxFT5AGD5Ak+5Zrw==", + "dev": true + }, + "node_modules/@codemirror/view": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.0.2.tgz", + "integrity": "sha512-mnVT/q1JvKPjpmjXJNeCi/xHyaJ3abGJsumIVpdQ1nE1MXAyHf7GHWt8QpWMUvDiqF0j+inkhVR2OviTdFFX7Q==", + "dev": true, + "dependencies": { + "@codemirror/state": "^6.0.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@lezer/common": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", + "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==", + "dev": true + }, + "node_modules/@lezer/highlight": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz", + "integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==", + "dev": true, + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.0.0.tgz", + "integrity": "sha512-RawBSrMD9yrVdrXWKn7hqo5BqgBaFelUx80i6p2/V0f+0THjncSSrRC6v3QWVv++RpqWT59L8ujKZjlExJq9xw==", + "dev": true, + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.0.0.tgz", + "integrity": "sha512-k6DEqBh4HxqO/cVGedb6Ern6LS7K6IOzfydJ5WaqCR26v6UR9sIFyb6PS+5rPUs/mXgnBR/QQCW7RkyjSCMoQA==", + "dev": true, + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.2.tgz", + "integrity": "sha512-pSrFeJB6Bg1Mrg9CdQW3+hqZXAKsBrSG9MAfFLKy1pVA4Mb4W7C0k7mEhlmS2Dfo/otxrQOET7NJiJ9RrS563w==", + "dev": true, + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.2.tgz", + "integrity": "sha512-coq8DBABRPFcVhVIk6IbKyyHUt7YTEC/C992tatFB+yEx5WGBQrCgsSFjxHUr8AWXphWckadVJbominEduYBqw==", + "dev": true, + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || insiders" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.7", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz", + "integrity": "sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.20.3", + "caniuse-lite": "^1.0.30001335", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.0.tgz", + "integrity": "sha512-UQxE0DIhRB5z/zDz9iA03BOfxaN2+GQdBYH/2WrSIWEUrnpzTPJbhqt+umq6r3acaPRTW1FNTkrcp0PXgtFkvA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001358", + "electron-to-chromium": "^1.4.164", + "node-releases": "^2.0.5", + "update-browserslist-db": "^1.0.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001359", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001359.tgz", + "integrity": "sha512-Xln/BAsPzEuiVLgJ2/45IaqD9jShtk3Y33anKb4+yLwQzws3+v6odKfpgES/cDEaZMLzSChpIGdbOYtH9MyuHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/codemirror": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.0.tgz", + "integrity": "sha512-c4XR9QtDn+NhKLM2FBsnRn9SFdRH7G6594DYC/fyKKIsTOcdLF0WNWRd+f6kNyd5j1vgYPucbIeq2XkywYCwhA==", + "dev": true, + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.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==", + "dev": true + }, + "node_modules/crelt": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", + "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==", + "dev": true + }, + "node_modules/detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dev": true, + "dependencies": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.168", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.168.tgz", + "integrity": "sha512-yz247hclRBaP8ABB1hf9kL7AMfa+yC2hB9F3XF8Y87VWMnYgq4QYvV6acRACcDkTDxfGQ4GYK/aZPQiuFMGbaA==", + "dev": true + }, + "node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.47.tgz", + "integrity": "sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "esbuild-android-64": "0.14.47", + "esbuild-android-arm64": "0.14.47", + "esbuild-darwin-64": "0.14.47", + "esbuild-darwin-arm64": "0.14.47", + "esbuild-freebsd-64": "0.14.47", + "esbuild-freebsd-arm64": "0.14.47", + "esbuild-linux-32": "0.14.47", + "esbuild-linux-64": "0.14.47", + "esbuild-linux-arm": "0.14.47", + "esbuild-linux-arm64": "0.14.47", + "esbuild-linux-mips64le": "0.14.47", + "esbuild-linux-ppc64le": "0.14.47", + "esbuild-linux-riscv64": "0.14.47", + "esbuild-linux-s390x": "0.14.47", + "esbuild-netbsd-64": "0.14.47", + "esbuild-openbsd-64": "0.14.47", + "esbuild-sunos-64": "0.14.47", + "esbuild-windows-32": "0.14.47", + "esbuild-windows-64": "0.14.47", + "esbuild-windows-arm64": "0.14.47" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.47.tgz", + "integrity": "sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.47.tgz", + "integrity": "sha512-OkwOjj7ts4lBp/TL6hdd8HftIzOy/pdtbrNA4+0oVWgGG64HrdVzAF5gxtJufAPOsEjkyh1oIYvKAUinKKQRSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.47.tgz", + "integrity": "sha512-R6oaW0y5/u6Eccti/TS6c/2c1xYTb1izwK3gajJwi4vIfNs1s8B1dQzI1UiC9T61YovOQVuePDcfqHLT3mUZJA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.47.tgz", + "integrity": "sha512-seCmearlQyvdvM/noz1L9+qblC5vcBrhUaOoLEDDoLInF/VQ9IkobGiLlyTPYP5dW1YD4LXhtBgOyevoIHGGnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.47.tgz", + "integrity": "sha512-ZH8K2Q8/Ux5kXXvQMDsJcxvkIwut69KVrYQhza/ptkW50DC089bCVrJZZ3sKzIoOx+YPTrmsZvqeZERjyYrlvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.47.tgz", + "integrity": "sha512-ZJMQAJQsIOhn3XTm7MPQfCzEu5b9STNC+s90zMWe2afy9EwnHV7Ov7ohEMv2lyWlc2pjqLW8QJnz2r0KZmeAEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.47.tgz", + "integrity": "sha512-FxZOCKoEDPRYvq300lsWCTv1kcHgiiZfNrPtEhFAiqD7QZaXrad8LxyJ8fXGcWzIFzRiYZVtB3ttvITBvAFhKw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.47.tgz", + "integrity": "sha512-nFNOk9vWVfvWYF9YNYksZptgQAdstnDCMtR6m42l5Wfugbzu11VpMCY9XrD4yFxvPo9zmzcoUL/88y0lfJZJJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.47.tgz", + "integrity": "sha512-ZGE1Bqg/gPRXrBpgpvH81tQHpiaGxa8c9Rx/XOylkIl2ypLuOcawXEAo8ls+5DFCcRGt/o3sV+PzpAFZobOsmA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.47.tgz", + "integrity": "sha512-ywfme6HVrhWcevzmsufjd4iT3PxTfCX9HOdxA7Hd+/ZM23Y9nXeb+vG6AyA6jgq/JovkcqRHcL9XwRNpWG6XRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.47.tgz", + "integrity": "sha512-mg3D8YndZ1LvUiEdDYR3OsmeyAew4MA/dvaEJxvyygahWmpv1SlEEnhEZlhPokjsUMfRagzsEF/d/2XF+kTQGg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.47.tgz", + "integrity": "sha512-WER+f3+szmnZiWoK6AsrTKGoJoErG2LlauSmk73LEZFQ/iWC+KhhDsOkn1xBUpzXWsxN9THmQFltLoaFEH8F8w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.47.tgz", + "integrity": "sha512-1fI6bP3A3rvI9BsaaXbMoaOjLE3lVkJtLxsgLHqlBhLlBVY7UqffWBvkrX/9zfPhhVMd9ZRFiaqXnB1T7BsL2g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.47.tgz", + "integrity": "sha512-eZrWzy0xFAhki1CWRGnhsHVz7IlSKX6yT2tj2Eg8lhAwlRE5E96Hsb0M1mPSE1dHGpt1QVwwVivXIAacF/G6mw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.47.tgz", + "integrity": "sha512-Qjdjr+KQQVH5Q2Q1r6HBYswFTToPpss3gqCiSw2Fpq/ua8+eXSQyAMG+UvULPqXceOwpnPo4smyZyHdlkcPppQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.47.tgz", + "integrity": "sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.47.tgz", + "integrity": "sha512-uOeSgLUwukLioAJOiGYm3kNl+1wJjgJA8R671GYgcPgCx7QR73zfvYqXFFcIO93/nBdIbt5hd8RItqbbf3HtAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.47.tgz", + "integrity": "sha512-H0fWsLTp2WBfKLBgwYT4OTfFly4Im/8B5f3ojDv1Kx//kiubVY0IQunP2Koc/fr/0wI7hj3IiBDbSrmKlrNgLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.47.tgz", + "integrity": "sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.47.tgz", + "integrity": "sha512-HFSW2lnp62fl86/qPQlqw6asIwCnEsEoNIL1h2uVMgakddf+vUuMcCbtUY1i8sst7KkgHrVKCJQB33YhhOweCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/highlight.js": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.5.1.tgz", + "integrity": "sha512-LKzHqnxr4CrD2YsNoIf/o5nJ09j4yi/GcH5BnYz9UnVpZdS4ucMgvP61TDty5xJcFGRjnH4DpujkS9bHT3hq0Q==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": 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==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lilconfig": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", + "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, + "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==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz", + "integrity": "sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.6" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "2.75.7", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.7.tgz", + "integrity": "sha512-VSE1iy0eaAYNCxEXaleThdFXqZJ42qDBatAwrfnPlENEZ8erQ+0LYX4JXOLPceWfZpV1VtZwZ3dFCuOZiSyFtQ==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-mod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", + "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==", + "dev": true + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.4.tgz", + "integrity": "sha512-NrxbFV4tYsga/hpWbRyUfIaBrNMXDxx5BsHgBS4v5tlyjf+sDsgBg5m9OxjrXIqAS/uR9kicxLKP+bEHI7BSeQ==", + "dev": true, + "dependencies": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.14", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "5.0.6", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "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==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz", + "integrity": "sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "2.9.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.12.tgz", + "integrity": "sha512-suxC36dQo9Rq1qMB2qiRorNJtJAdxguu5TMvBHOc/F370KvqAe9t48vYp+/TbPKRNrMh/J55tOUmkuIqstZaew==", + "dev": true, + "dependencies": { + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": "^2.59.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + } + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz", + "integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + } + }, + "dependencies": { + "@codemirror/autocomplete": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.0.2.tgz", + "integrity": "sha512-9PDjnllmXan/7Uax87KGORbxerDJ/cu10SB+n4Jz0zXMEvIh3+TGgZxhIvDOtaQ4jDBQEM7kHYW4vLdQB0DGZQ==", + "dev": true, + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/commands": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.0.0.tgz", + "integrity": "sha512-nVJDPiCQXWXj5AZxqNVXyIM3nOYauF4Dko9NGPSwgVdK+lXWJQhI5LGhS/AvdG5b7u7/pTQBkrQmzkLWRBF62A==", + "dev": true, + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/lang-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.0.0.tgz", + "integrity": "sha512-IQLfR+pgydwLxP3AQYOhjGfLWlAZz5SEH+M1tnUXVvp2+KZhn3iDTQX/7HZgoJ3w0oySKOxPjdq6jmkTCW6/sg==", + "dev": true, + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "@codemirror/language": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.1.0.tgz", + "integrity": "sha512-CeqY80nvUFrJcXcBW115aNi06D0PS8NSW6nuJRSwbrYFkE0SfJnPfyLGrcM90AV95lqg5+4xUi99BCmzNaPGJg==", + "dev": true, + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/lint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.0.0.tgz", + "integrity": "sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==", + "dev": true, + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/search": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.0.0.tgz", + "integrity": "sha512-rL0rd3AhI0TAsaJPUaEwC63KHLO7KL0Z/dYozXj6E7L3wNHRyx7RfE0/j5HsIf912EE5n2PCb4Vg0rGYmDv4UQ==", + "dev": true, + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/state": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.0.1.tgz", + "integrity": "sha512-6vYgaXc4KjSY0BUfSVDJooGcoswg/RJZpq/ZGjsUYmY0KN1lmB8u03nv+jiG1ncUV5qoggyxFT5AGD5Ak+5Zrw==", + "dev": true + }, + "@codemirror/view": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.0.2.tgz", + "integrity": "sha512-mnVT/q1JvKPjpmjXJNeCi/xHyaJ3abGJsumIVpdQ1nE1MXAyHf7GHWt8QpWMUvDiqF0j+inkhVR2OviTdFFX7Q==", + "dev": true, + "requires": { + "@codemirror/state": "^6.0.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "@lezer/common": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", + "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==", + "dev": true + }, + "@lezer/highlight": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz", + "integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==", + "dev": true, + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/javascript": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.0.0.tgz", + "integrity": "sha512-RawBSrMD9yrVdrXWKn7hqo5BqgBaFelUx80i6p2/V0f+0THjncSSrRC6v3QWVv++RpqWT59L8ujKZjlExJq9xw==", + "dev": true, + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/lr": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.0.0.tgz", + "integrity": "sha512-k6DEqBh4HxqO/cVGedb6Ern6LS7K6IOzfydJ5WaqCR26v6UR9sIFyb6PS+5rPUs/mXgnBR/QQCW7RkyjSCMoQA==", + "dev": true, + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@tailwindcss/forms": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.2.tgz", + "integrity": "sha512-pSrFeJB6Bg1Mrg9CdQW3+hqZXAKsBrSG9MAfFLKy1pVA4Mb4W7C0k7mEhlmS2Dfo/otxrQOET7NJiJ9RrS563w==", + "dev": true, + "requires": { + "mini-svg-data-uri": "^1.2.3" + } + }, + "@tailwindcss/typography": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.2.tgz", + "integrity": "sha512-coq8DBABRPFcVhVIk6IbKyyHUt7YTEC/C992tatFB+yEx5WGBQrCgsSFjxHUr8AWXphWckadVJbominEduYBqw==", + "dev": true, + "requires": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "autoprefixer": { + "version": "10.4.7", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz", + "integrity": "sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA==", + "dev": true, + "requires": { + "browserslist": "^4.20.3", + "caniuse-lite": "^1.0.30001335", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.0.tgz", + "integrity": "sha512-UQxE0DIhRB5z/zDz9iA03BOfxaN2+GQdBYH/2WrSIWEUrnpzTPJbhqt+umq6r3acaPRTW1FNTkrcp0PXgtFkvA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001358", + "electron-to-chromium": "^1.4.164", + "node-releases": "^2.0.5", + "update-browserslist-db": "^1.0.0" + } + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001359", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001359.tgz", + "integrity": "sha512-Xln/BAsPzEuiVLgJ2/45IaqD9jShtk3Y33anKb4+yLwQzws3+v6odKfpgES/cDEaZMLzSChpIGdbOYtH9MyuHw==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "codemirror": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.0.tgz", + "integrity": "sha512-c4XR9QtDn+NhKLM2FBsnRn9SFdRH7G6594DYC/fyKKIsTOcdLF0WNWRd+f6kNyd5j1vgYPucbIeq2XkywYCwhA==", + "dev": true, + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "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==", + "dev": true + }, + "crelt": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", + "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==", + "dev": true + }, + "detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dev": true, + "requires": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + } + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.168", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.168.tgz", + "integrity": "sha512-yz247hclRBaP8ABB1hf9kL7AMfa+yC2hB9F3XF8Y87VWMnYgq4QYvV6acRACcDkTDxfGQ4GYK/aZPQiuFMGbaA==", + "dev": true + }, + "entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true + }, + "esbuild": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.47.tgz", + "integrity": "sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA==", + "dev": true, + "requires": { + "esbuild-android-64": "0.14.47", + "esbuild-android-arm64": "0.14.47", + "esbuild-darwin-64": "0.14.47", + "esbuild-darwin-arm64": "0.14.47", + "esbuild-freebsd-64": "0.14.47", + "esbuild-freebsd-arm64": "0.14.47", + "esbuild-linux-32": "0.14.47", + "esbuild-linux-64": "0.14.47", + "esbuild-linux-arm": "0.14.47", + "esbuild-linux-arm64": "0.14.47", + "esbuild-linux-mips64le": "0.14.47", + "esbuild-linux-ppc64le": "0.14.47", + "esbuild-linux-riscv64": "0.14.47", + "esbuild-linux-s390x": "0.14.47", + "esbuild-netbsd-64": "0.14.47", + "esbuild-openbsd-64": "0.14.47", + "esbuild-sunos-64": "0.14.47", + "esbuild-windows-32": "0.14.47", + "esbuild-windows-64": "0.14.47", + "esbuild-windows-arm64": "0.14.47" + } + }, + "esbuild-android-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.47.tgz", + "integrity": "sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.47.tgz", + "integrity": "sha512-OkwOjj7ts4lBp/TL6hdd8HftIzOy/pdtbrNA4+0oVWgGG64HrdVzAF5gxtJufAPOsEjkyh1oIYvKAUinKKQRSQ==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.47.tgz", + "integrity": "sha512-R6oaW0y5/u6Eccti/TS6c/2c1xYTb1izwK3gajJwi4vIfNs1s8B1dQzI1UiC9T61YovOQVuePDcfqHLT3mUZJA==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.47.tgz", + "integrity": "sha512-seCmearlQyvdvM/noz1L9+qblC5vcBrhUaOoLEDDoLInF/VQ9IkobGiLlyTPYP5dW1YD4LXhtBgOyevoIHGGnw==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.47.tgz", + "integrity": "sha512-ZH8K2Q8/Ux5kXXvQMDsJcxvkIwut69KVrYQhza/ptkW50DC089bCVrJZZ3sKzIoOx+YPTrmsZvqeZERjyYrlvQ==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.47.tgz", + "integrity": "sha512-ZJMQAJQsIOhn3XTm7MPQfCzEu5b9STNC+s90zMWe2afy9EwnHV7Ov7ohEMv2lyWlc2pjqLW8QJnz2r0KZmeAEQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.47.tgz", + "integrity": "sha512-FxZOCKoEDPRYvq300lsWCTv1kcHgiiZfNrPtEhFAiqD7QZaXrad8LxyJ8fXGcWzIFzRiYZVtB3ttvITBvAFhKw==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.47.tgz", + "integrity": "sha512-nFNOk9vWVfvWYF9YNYksZptgQAdstnDCMtR6m42l5Wfugbzu11VpMCY9XrD4yFxvPo9zmzcoUL/88y0lfJZJJw==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.47.tgz", + "integrity": "sha512-ZGE1Bqg/gPRXrBpgpvH81tQHpiaGxa8c9Rx/XOylkIl2ypLuOcawXEAo8ls+5DFCcRGt/o3sV+PzpAFZobOsmA==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.47.tgz", + "integrity": "sha512-ywfme6HVrhWcevzmsufjd4iT3PxTfCX9HOdxA7Hd+/ZM23Y9nXeb+vG6AyA6jgq/JovkcqRHcL9XwRNpWG6XRw==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.47.tgz", + "integrity": "sha512-mg3D8YndZ1LvUiEdDYR3OsmeyAew4MA/dvaEJxvyygahWmpv1SlEEnhEZlhPokjsUMfRagzsEF/d/2XF+kTQGg==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.47.tgz", + "integrity": "sha512-WER+f3+szmnZiWoK6AsrTKGoJoErG2LlauSmk73LEZFQ/iWC+KhhDsOkn1xBUpzXWsxN9THmQFltLoaFEH8F8w==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.47.tgz", + "integrity": "sha512-1fI6bP3A3rvI9BsaaXbMoaOjLE3lVkJtLxsgLHqlBhLlBVY7UqffWBvkrX/9zfPhhVMd9ZRFiaqXnB1T7BsL2g==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.47.tgz", + "integrity": "sha512-eZrWzy0xFAhki1CWRGnhsHVz7IlSKX6yT2tj2Eg8lhAwlRE5E96Hsb0M1mPSE1dHGpt1QVwwVivXIAacF/G6mw==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.47.tgz", + "integrity": "sha512-Qjdjr+KQQVH5Q2Q1r6HBYswFTToPpss3gqCiSw2Fpq/ua8+eXSQyAMG+UvULPqXceOwpnPo4smyZyHdlkcPppQ==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.47.tgz", + "integrity": "sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.47.tgz", + "integrity": "sha512-uOeSgLUwukLioAJOiGYm3kNl+1wJjgJA8R671GYgcPgCx7QR73zfvYqXFFcIO93/nBdIbt5hd8RItqbbf3HtAQ==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.47.tgz", + "integrity": "sha512-H0fWsLTp2WBfKLBgwYT4OTfFly4Im/8B5f3ojDv1Kx//kiubVY0IQunP2Koc/fr/0wI7hj3IiBDbSrmKlrNgLQ==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.47.tgz", + "integrity": "sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.47.tgz", + "integrity": "sha512-HFSW2lnp62fl86/qPQlqw6asIwCnEsEoNIL1h2uVMgakddf+vUuMcCbtUY1i8sst7KkgHrVKCJQB33YhhOweCQ==", + "dev": true, + "optional": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "highlight.js": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.5.1.tgz", + "integrity": "sha512-LKzHqnxr4CrD2YsNoIf/o5nJ09j4yi/GcH5BnYz9UnVpZdS4ucMgvP61TDty5xJcFGRjnH4DpujkS9bHT3hq0Q==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "lilconfig": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", + "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", + "dev": true + }, + "linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, + "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==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true + }, + "node-releases": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz", + "integrity": "sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "dev": true, + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1" + } + }, + "postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "requires": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + } + }, + "postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.6" + } + }, + "postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "requires": { + "pify": "^2.3.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rollup": { + "version": "2.75.7", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.7.tgz", + "integrity": "sha512-VSE1iy0eaAYNCxEXaleThdFXqZJ42qDBatAwrfnPlENEZ8erQ+0LYX4JXOLPceWfZpV1VtZwZ3dFCuOZiSyFtQ==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "style-mod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", + "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==", + "dev": true + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "tailwindcss": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.4.tgz", + "integrity": "sha512-NrxbFV4tYsga/hpWbRyUfIaBrNMXDxx5BsHgBS4v5tlyjf+sDsgBg5m9OxjrXIqAS/uR9kicxLKP+bEHI7BSeQ==", + "dev": true, + "requires": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.14", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "5.0.6", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.0" + } + }, + "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==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz", + "integrity": "sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "vite": { + "version": "2.9.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.12.tgz", + "integrity": "sha512-suxC36dQo9Rq1qMB2qiRorNJtJAdxguu5TMvBHOc/F370KvqAe9t48vYp+/TbPKRNrMh/J55tOUmkuIqstZaew==", + "dev": true, + "requires": { + "esbuild": "^0.14.27", + "fsevents": "~2.3.2", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": "^2.59.0" + } + }, + "w3c-keyname": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz", + "integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ba9fef9 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "opengist", + "private": true, + "version": "1.0.0", + "scripts": { + "dev": "node_modules/.bin/vite", + "build": "node_modules/.bin/vite build", + "preview": "node_modules/.bin/vite preview" + }, + "devDependencies": { + "@codemirror/commands": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/view": "^6.0.1", + "@tailwindcss/forms": "^0.5.1", + "@tailwindcss/typography": "^0.5.2", + "autoprefixer": "^10.4.7", + "codemirror": "^6.0.0", + "highlight.js": "^11.5.1", + "markdown-it": "^13.0.1", + "moment": "^2.29.3", + "postcss": "^8.4.13", + "tailwindcss": "^3.0.24", + "vite": "^2.9.2" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..07d5d79 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: [ + require('tailwindcss'), + require('autoprefixer'), + ], +} diff --git a/public/editor.js b/public/editor.js new file mode 100644 index 0000000..aab8ad4 --- /dev/null +++ b/public/editor.js @@ -0,0 +1,75 @@ +import {EditorView, keymap, gutter, lineNumbers} from "@codemirror/view" +import {indentWithTab} from "@codemirror/commands" + +EditorView.theme({}, {dark: true}) + +let editorsjs = [] +let editorsParentdom = document.getElementById('editors') +let allEditorsdom = document.querySelectorAll('#editors > .editor') +let firstEditordom = allEditorsdom[0] + +const newEditor = (dom, value = '') => { + return new EditorView({ + doc: value, + extensions: [ + lineNumbers(), gutter({class: "cm-mygutter"}), + keymap.of([indentWithTab]), + ], + parent: dom + }) +} + + +document.onsubmit = () => { + console.log('onsubmit'); + window.onbeforeunload = null; +} + +let arr = [...allEditorsdom] +arr.forEach(el => { + // in case we edit the gist contents + let currEditor = newEditor(el, el.querySelector('.form-filecontent').value) + editorsjs.push(currEditor) + + currEditor.dom.addEventListener("input", function inputConfirmLeave() { + if (!currEditor.inView) return; // skip events outside the viewport + + currEditor.dom.removeEventListener("input", inputConfirmLeave); + window.onbeforeunload = () => { + return 'Are you sure you want to quit?'; + } + }); + + // remove editor on delete + let deleteBtns = el.querySelector('button.delete-file') + if (deleteBtns !== null) { + + deleteBtns.onclick = () => { + + editorsjs.splice(editorsjs.indexOf(currEditor), 1); + el.remove() + } + } +}) + +document.getElementById('add-file').onclick = () => { + let newEditorDom = firstEditordom.cloneNode(true) + + // reset the filename of the new cloned element + newEditorDom.querySelector('input[name="name"]').value = "" + + // removing the previous codemirror editor + let newEditorDomCM = newEditorDom.querySelector('.cm-editor') + newEditorDomCM.remove() + + // creating the new codemirror editor and append it in the editor div + editorsjs.push(newEditor(newEditorDom)) + editorsParentdom.append(newEditorDom) +} + +document.querySelector('form#create').onsubmit = () => { + let j = 0 + document.querySelectorAll('.form-filecontent').forEach((e) => { + e.value = encodeURIComponent(editorsjs[j++].state.doc.toString()) + }) +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..26a05c2 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/public/main.js b/public/main.js new file mode 100644 index 0000000..b8d4749 --- /dev/null +++ b/public/main.js @@ -0,0 +1,132 @@ +import './style.css' +import './markdown.css' +import 'highlight.js/styles/tokyo-night-dark.css' +import moment from 'moment' +import md from 'markdown-it' +import hljs from 'highlight.js' + +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('.moment-timestamp').forEach((e) => { + e.title = moment.unix(e.innerHTML).format('LLLL') + e.innerHTML = moment.unix(e.innerHTML).fromNow() + }) + + document.querySelectorAll('.moment-timestamp-date').forEach((e) => { + e.innerHTML = moment.unix(e.innerHTML).format('DD/MM/YYYY HH:mm') + }) + + let rev = document.querySelector('.revision-text') + if (rev) { + let fullRev = rev.innerHTML + let smallRev = fullRev.substring(0, 8) + rev.innerHTML = smallRev + + rev.onmouseover = () => { + rev.innerHTML = fullRev + } + rev.onmouseout = () => { + rev.innerHTML = smallRev + } + } + + document.querySelectorAll('.markdown').forEach((e) => { + e.innerHTML = md().render(e.innerHTML); + }) + + document.querySelectorAll('.table-code').forEach((el) => { + let ext = el.dataset.filename.split('.').pop() + if (ext!== 'txt') { + + el.querySelectorAll('td.line-code').forEach((ell) => { + ell.classList.add('language-'+ext) + hljs.highlightElement(ell); + }); + } + + // more efficient + el.addEventListener('click', event => { + if (event.target.matches('.line-num')) { + Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected')); + + event.target.nextSibling.classList.add('selected') + + let filename = el.dataset.filenameSlug + let line = event.target.textContent + let url = location.protocol + '//' + location.host + location.pathname + let hash = '#file-'+ filename + '-' +line + window.history.pushState(null, null, url+hash); + location.hash = hash; + } + }); + }); + + + let colorhash = () => { + Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected')); + let lineEl = document.querySelector(location.hash) + if (lineEl) { + lineEl.nextSibling.classList.add('selected') + } + } + + if (location.hash) { + colorhash() + } + window.onhashchange = colorhash + + document.getElementById('main-menu-button').onclick = () => { + document.getElementById('mobile-menu').classList.toggle('hidden') + } + + let tabs = document.getElementById('gist-tabs') + if (tabs) { + tabs.onchange = (e) => { + // navigate to the url in data-url + window.location.href = e.target.selectedOptions[0].dataset.url + } + } + + let gistmenutoggle = document.getElementById('gist-menu-toggle'); + if (gistmenutoggle) { + let gistmenucopy = document.getElementById('gist-menu-copy') + let gistmenubuttoncopy = document.getElementById('gist-menu-button-copy') + let gistmenuinput = document.getElementById('gist-menu-input') + let gistmenutitle = document.getElementById('gist-menu-title') + gistmenutoggle.onclick = () => { + gistmenucopy.classList.toggle('hidden') + } + + for (let item of gistmenucopy.children) { + item.onclick = () => { + gistmenutitle.textContent = item.firstChild.textContent + gistmenuinput.value = item.dataset.link + gistmenucopy.classList.toggle('hidden') + } + } + + gistmenubuttoncopy.onclick = () => { + let text = gistmenuinput.value + navigator.clipboard.writeText(text).then(null, function(err) { + console.error('Could not copy text: ', err); + }) + } + } + + let sortgist = document.getElementById('sort-gists-button') + if (sortgist) { + sortgist.onclick = () => { + document.getElementById('sort-gists-dropdown').classList.toggle('hidden') + } + } + + document.querySelectorAll('.copy-gist-btn').forEach((e) => { + e.onclick = () => { + navigator.clipboard.writeText(e.parentNode.querySelector('.gist-content').textContent).then(null, function (err) { + console.error('Could not copy text: ', err); + }) + } + }) + + + +}); \ No newline at end of file diff --git a/public/markdown.css b/public/markdown.css new file mode 100644 index 0000000..65586e9 --- /dev/null +++ b/public/markdown.css @@ -0,0 +1,942 @@ +/* https://github.com/sindresorhus/github-markdown-css/blob/main/github-markdown-dark.css */ + +.markdown-body { + color-scheme: dark; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + margin: 0; + color: #c9d1d9; + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body .octicon { + display: inline-block; + fill: currentColor; + vertical-align: text-bottom; +} + +.markdown-body h1:hover .anchor .octicon-link:before, +.markdown-body h2:hover .anchor .octicon-link:before, +.markdown-body h3:hover .anchor .octicon-link:before, +.markdown-body h4:hover .anchor .octicon-link:before, +.markdown-body h5:hover .anchor .octicon-link:before, +.markdown-body h6:hover .anchor .octicon-link:before { + width: 16px; + height: 16px; + content: ' '; + display: inline-block; + background-color: currentColor; + -webkit-mask-image: url("data:image/svg+xml,"); + mask-image: url("data:image/svg+xml,"); +} + +.markdown-body details, +.markdown-body figcaption, +.markdown-body figure { + display: block; +} + +.markdown-body summary { + display: list-item; +} + +.markdown-body [hidden] { + display: none !important; +} + +.markdown-body a { + background-color: transparent; + color: #58a6ff; + text-decoration: none; +} + +.markdown-body a:active, +.markdown-body a:hover { + outline-width: 0; +} + +.markdown-body abbr[title] { + border-bottom: none; + text-decoration: underline dotted; +} + +.markdown-body b, +.markdown-body strong { + font-weight: 600; +} + +.markdown-body dfn { + font-style: italic; +} + +.markdown-body h1 { + margin: .67em 0; + font-weight: 600; + padding-bottom: .3em; + font-size: 2em; + border-bottom: 1px solid #21262d; +} + +.markdown-body mark { + background-color: rgba(187,128,9,0.15); + color: #c9d1d9; +} + +.markdown-body small { + font-size: 90%; +} + +.markdown-body sub, +.markdown-body sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +.markdown-body sub { + bottom: -0.25em; +} + +.markdown-body sup { + top: -0.5em; +} + +.markdown-body img { + border-style: none; + max-width: 100%; + box-sizing: content-box; + background-color: #0d1117; +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre, +.markdown-body samp { + font-family: monospace,monospace; + font-size: 1em; +} + +.markdown-body figure { + margin: 1em 40px; +} + +.markdown-body hr { + box-sizing: content-box; + overflow: hidden; + background: transparent; + border-bottom: 1px solid #21262d; + height: .25em; + padding: 0; + margin: 24px 0; + background-color: #30363d; + border: 0; +} + +.markdown-body input { + font: inherit; + margin: 0; + overflow: visible; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.markdown-body [type=button], +.markdown-body [type=reset], +.markdown-body [type=submit] { + -webkit-appearance: button; +} + +.markdown-body [type=button]::-moz-focus-inner, +.markdown-body [type=reset]::-moz-focus-inner, +.markdown-body [type=submit]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +.markdown-body [type=button]:-moz-focusring, +.markdown-body [type=reset]:-moz-focusring, +.markdown-body [type=submit]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +.markdown-body [type=checkbox], +.markdown-body [type=radio] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body [type=number]::-webkit-inner-spin-button, +.markdown-body [type=number]::-webkit-outer-spin-button { + height: auto; +} + +.markdown-body [type=search] { + -webkit-appearance: textfield; + outline-offset: -2px; +} + +.markdown-body [type=search]::-webkit-search-cancel-button, +.markdown-body [type=search]::-webkit-search-decoration { + -webkit-appearance: none; +} + +.markdown-body ::-webkit-input-placeholder { + color: inherit; + opacity: .54; +} + +.markdown-body ::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body hr::before { + display: table; + content: ""; +} + +.markdown-body hr::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; + display: block; + width: max-content; + max-width: 100%; + overflow: auto; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body details summary { + cursor: pointer; +} + +.markdown-body details:not([open])>*:not(summary) { + display: none !important; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; + line-height: 10px; + color: #c9d1d9; + vertical-align: middle; + background-color: #161b22; + border: solid 1px rgba(110,118,129,0.4); + border-bottom-color: rgba(110,118,129,0.4); + border-radius: 6px; + box-shadow: inset 0 -1px 0 rgba(110,118,129,0.4); +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; +} + +.markdown-body h2 { + font-weight: 600; + padding-bottom: .3em; + font-size: 1.5em; + border-bottom: 1px solid #21262d; +} + +.markdown-body h3 { + font-weight: 600; + font-size: 1.25em; +} + +.markdown-body h4 { + font-weight: 600; + font-size: 1em; +} + +.markdown-body h5 { + font-weight: 600; + font-size: .875em; +} + +.markdown-body h6 { + font-weight: 600; + font-size: .85em; + color: #8b949e; +} + +.markdown-body p { + margin-top: 0; + margin-bottom: 10px; +} + +.markdown-body blockquote { + margin: 0; + padding: 0 1em; + color: #8b949e; + border-left: .25em solid #30363d; +} + +.markdown-body ul, +.markdown-body ol { + margin-top: 0; + margin-bottom: 0; + padding-left: 2em; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ul ul ol, +.markdown-body ul ol ol, +.markdown-body ol ul ol, +.markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body tt, +.markdown-body code { + font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; + font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; + font-size: 12px; + word-wrap: normal; +} + +.markdown-body .octicon { + display: inline-block; + overflow: visible !important; + vertical-align: text-bottom; + fill: currentColor; +} + +.markdown-body ::placeholder { + color: #484f58; + opacity: 1; +} + +.markdown-body input::-webkit-outer-spin-button, +.markdown-body input::-webkit-inner-spin-button { + margin: 0; + -webkit-appearance: none; + appearance: none; +} + +.markdown-body .pl-c { + color: #8b949e; +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: #79c0ff; +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: #d2a8ff; +} + +.markdown-body .pl-smi, +.markdown-body .pl-s .pl-s1 { + color: #c9d1d9; +} + +.markdown-body .pl-ent { + color: #7ee787; +} + +.markdown-body .pl-k { + color: #ff7b72; +} + +.markdown-body .pl-s, +.markdown-body .pl-pds, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sre, +.markdown-body .pl-sr .pl-sra { + color: #a5d6ff; +} + +.markdown-body .pl-v, +.markdown-body .pl-smw { + color: #ffa657; +} + +.markdown-body .pl-bu { + color: #f85149; +} + +.markdown-body .pl-ii { + color: #f0f6fc; + background-color: #8e1519; +} + +.markdown-body .pl-c2 { + color: #f0f6fc; + background-color: #b62324; +} + +.markdown-body .pl-sr .pl-cce { + font-weight: bold; + color: #7ee787; +} + +.markdown-body .pl-ml { + color: #f2cc60; +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + font-weight: bold; + color: #1f6feb; +} + +.markdown-body .pl-mi { + font-style: italic; + color: #c9d1d9; +} + +.markdown-body .pl-mb { + font-weight: bold; + color: #c9d1d9; +} + +.markdown-body .pl-md { + color: #ffdcd7; + background-color: #67060c; +} + +.markdown-body .pl-mi1 { + color: #aff5b4; + background-color: #033a16; +} + +.markdown-body .pl-mc { + color: #ffdfb6; + background-color: #5a1e02; +} + +.markdown-body .pl-mi2 { + color: #c9d1d9; + background-color: #1158c7; +} + +.markdown-body .pl-mdr { + font-weight: bold; + color: #d2a8ff; +} + +.markdown-body .pl-ba { + color: #8b949e; +} + +.markdown-body .pl-sg { + color: #484f58; +} + +.markdown-body .pl-corl { + text-decoration: underline; + color: #a5d6ff; +} + +.markdown-body [data-catalyst] { + display: block; +} + +.markdown-body g-emoji { + font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; + font-size: 1em; + font-style: normal !important; + font-weight: 400; + line-height: 1; + vertical-align: -0.075em; +} + +.markdown-body g-emoji img { + width: 1em; + height: 1em; +} + +.markdown-body::before { + display: table; + content: ""; +} + +.markdown-body::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body>*:first-child { + margin-top: 0 !important; +} + +.markdown-body>*:last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body .absent { + color: #f85149; +} + +.markdown-body .anchor { + float: left; + padding-right: 4px; + margin-left: -20px; + line-height: 1; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre, +.markdown-body details { + margin-top: 0; + margin-bottom: 16px; +} + +.markdown-body blockquote>:first-child { + margin-top: 0; +} + +.markdown-body blockquote>:last-child { + margin-bottom: 0; +} + +.markdown-body sup>a::before { + content: "["; +} + +.markdown-body sup>a::after { + content: "]"; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: #c9d1d9; + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1 tt, +.markdown-body h1 code, +.markdown-body h2 tt, +.markdown-body h2 code, +.markdown-body h3 tt, +.markdown-body h3 code, +.markdown-body h4 tt, +.markdown-body h4 code, +.markdown-body h5 tt, +.markdown-body h5 code, +.markdown-body h6 tt, +.markdown-body h6 code { + padding: 0 .2em; + font-size: inherit; +} + +.markdown-body ul.no-list, +.markdown-body ol.no-list { + padding: 0; + list-style-type: none; +} + +.markdown-body ol[type="1"] { + list-style-type: decimal; +} + +.markdown-body ol[type=a] { + list-style-type: lower-alpha; +} + +.markdown-body ol[type=i] { + list-style-type: lower-roman; +} + +.markdown-body div>ol:not([type]) { + list-style-type: decimal; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li>p { + margin-top: 16px; +} + +.markdown-body li+li { + margin-top: .25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: 600; +} + +.markdown-body dl dd { + padding: 0 16px; + margin-bottom: 16px; +} + +.markdown-body table th { + font-weight: 600; +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid #30363d; +} + +.markdown-body table tr { + background-color: #0d1117; + border-top: 1px solid #21262d; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #161b22; +} + +.markdown-body table img { + background-color: transparent; +} + +.markdown-body img[align=right] { + padding-left: 20px; +} + +.markdown-body img[align=left] { + padding-right: 20px; +} + +.markdown-body .emoji { + max-width: none; + vertical-align: text-top; + background-color: transparent; +} + +.markdown-body span.frame { + display: block; + overflow: hidden; +} + +.markdown-body span.frame>span { + display: block; + float: left; + width: auto; + padding: 7px; + margin: 13px 0 0; + overflow: hidden; + border: 1px solid #30363d; +} + +.markdown-body span.frame span img { + display: block; + float: left; +} + +.markdown-body span.frame span span { + display: block; + padding: 5px 0 0; + clear: both; + color: #c9d1d9; +} + +.markdown-body span.align-center { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-center>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: center; +} + +.markdown-body span.align-center span img { + margin: 0 auto; + text-align: center; +} + +.markdown-body span.align-right { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-right>span { + display: block; + margin: 13px 0 0; + overflow: hidden; + text-align: right; +} + +.markdown-body span.align-right span img { + margin: 0; + text-align: right; +} + +.markdown-body span.float-left { + display: block; + float: left; + margin-right: 13px; + overflow: hidden; +} + +.markdown-body span.float-left span { + margin: 13px 0 0; +} + +.markdown-body span.float-right { + display: block; + float: right; + margin-left: 13px; + overflow: hidden; +} + +.markdown-body span.float-right>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: right; +} + +.markdown-body code, +.markdown-body tt { + padding: .2em .4em; + margin: 0; + font-size: 85%; + background-color: rgba(110,118,129,0.4); + border-radius: 6px; +} + +.markdown-body code br, +.markdown-body tt br { + display: none; +} + +.markdown-body del code { + text-decoration: inherit; +} + +.markdown-body pre code { + font-size: 100%; +} + +.markdown-body pre>code { + padding: 0; + margin: 0; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 16px; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #161b22; + border-radius: 6px; +} + +.markdown-body pre code, +.markdown-body pre tt { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; +} + +.markdown-body .csv-data td, +.markdown-body .csv-data th { + padding: 5px; + overflow: hidden; + font-size: 12px; + line-height: 1; + text-align: left; + white-space: nowrap; +} + +.markdown-body .csv-data .blob-num { + padding: 10px 8px 9px; + text-align: right; + background: #0d1117; + border: 0; +} + +.markdown-body .csv-data tr { + border-top: 0; +} + +.markdown-body .csv-data th { + font-weight: 600; + background: #161b22; + border-top: 0; +} + +.markdown-body .footnotes { + font-size: 12px; + color: #8b949e; + border-top: 1px solid #30363d; +} + +.markdown-body .footnotes ol { + padding-left: 16px; +} + +.markdown-body .footnotes li { + position: relative; +} + +.markdown-body .footnotes li:target::before { + position: absolute; + top: -8px; + right: -8px; + bottom: -8px; + left: -24px; + pointer-events: none; + content: ""; + border: 2px solid #1f6feb; + border-radius: 6px; +} + +.markdown-body .footnotes li:target { + color: #c9d1d9; +} + +.markdown-body .footnotes .data-footnote-backref g-emoji { + font-family: monospace; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item label { + font-weight: 400; +} + +.markdown-body .task-list-item.enabled label { + cursor: pointer; +} + +.markdown-body .task-list-item+.task-list-item { + margin-top: 3px; +} + +.markdown-body .task-list-item .handle { + display: none; +} + +.markdown-body .task-list-item-checkbox { + margin: 0 .2em .25em -1.6em; + vertical-align: middle; +} + +.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { + margin: 0 -1.6em .25em .2em; +} + +.markdown-body ::-webkit-calendar-picker-indicator { + filter: invert(50%); +} diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..1bf362d --- /dev/null +++ b/public/style.css @@ -0,0 +1,109 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + ul, ol { + list-style: revert; + } +} + +a:hover { + @apply text-primary-400; +} + +input { + @apply placeholder-gray-400; +} + +:not(pre) > code[class*="language-"], pre[class*="language-"] { + @apply bg-gray-900 mt-1 pt-1 !important; +} + +pre { + font-size: 0.8em !important; +} + +.code { + font-family: Menlo,Consolas,Liberation Mono,monospace; +} + +.code .line-num { + width: 4%; + text-align: right; +} + +.red-diff { + background-color: rgba(255, 0, 0, .1); +} + +.green-diff { + background-color: rgba(0, 255, 128, .1); +} + +.gray-diff { + background-color: rgba(143, 143, 143, 0.38); + @apply py-4 !important +} + +#logged-button:hover .username { + @apply hidden !important +} + +#logged-button:hover .logout { + @apply block !important +} + +.cm-line, .cm-gutter { + @apply bg-gray-900 !important; + caret-color: white !important; + padding: 0 !important; +} + +.cm-activeLine, .cm-activeLineGutter { + @apply bg-gray-800 !important; +} + +.cm-gutters { + border: none !important; +} + +.cm-gutterElement { + @apply text-gray-300 px-4 !important +} + +.code td { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.code tbody { + line-height: 18.2px; +} + +#editor { + height: 337px; + max-height: 337px; +} + +.cm-editor { + height: 337px; + max-height: 337px; +} + +.hljs { + background: none !important; +} + +.line-code.selected { + background-color: rgba(65, 25, 63, 0.46) !important; + box-shadow: inset 4px 0 0 rgb(107, 38, 102) !important; +} + +.line-code { + @apply pl-2; +} + +.line-num { + @apply cursor-pointer text-slate-400 hover:text-white; +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..5908e4b --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,37 @@ +const colors = require('tailwindcss/colors') + +module.exports = { + content: [ + "./templates/**/*.html", + ], + theme: { + colors: { + transparent: 'transparent', + current: 'currentColor', + white: colors.white, + black: colors.black, + gray: { + 50: "#EEEFF1", + 100: "#DEDFE3", + 200: "#BABCC5", + 300: "#999CA8", + 400: "#75798A", + 500: "#585B68", + 600: "#464853", + 700: "#363840", + 800: "#232429", + 900: "#131316" + }, + emerald: colors.emerald, + rose: colors.rose, + primary: colors.sky, + slate: colors.slate + }, + extend: { + borderWidth: { + '1': '1px', + } + }, + }, + plugins: [require("@tailwindcss/typography"),require('@tailwindcss/forms')], +} diff --git a/templates/base/admin_footer.html b/templates/base/admin_footer.html new file mode 100644 index 0000000..5c870da --- /dev/null +++ b/templates/base/admin_footer.html @@ -0,0 +1,9 @@ +{{ define "admin_footer" }} +{{ if .urlPage }} +
+ {{ template "pagination" . }} +
+{{ end }} + + +{{ end }} diff --git a/templates/base/admin_header.html b/templates/base/admin_header.html new file mode 100644 index 0000000..d7be0b2 --- /dev/null +++ b/templates/base/admin_header.html @@ -0,0 +1,22 @@ +{{ define "admin_header" }} +
+
+
+

Admin panel

+
+
+
+
+
+ +
+
+ +{{ end }} diff --git a/templates/base/base_footer.html b/templates/base/base_footer.html new file mode 100644 index 0000000..443c4eb --- /dev/null +++ b/templates/base/base_footer.html @@ -0,0 +1,21 @@ +{{ define "footer" }} +

+ + + Opengist + + + + + ⋅ + Load: {{ loadedTime .loadStartTime }} + +

+
+ + + + + + +{{ end }} diff --git a/templates/base/base_header.html b/templates/base/base_header.html new file mode 100644 index 0000000..a05d952 --- /dev/null +++ b/templates/base/base_header.html @@ -0,0 +1,142 @@ +{{ define "header" }} + + + + + + + + + + {{ if .htmlTitle }} + {{ .htmlTitle }} - Opengist + {{ else }} + Opengist + {{ end }} + + +
+
+ + +
+ + + + +
+ + +
+ {{range .flashErrors}} +
+
+
+ +
+
+

{{.}}

+
+
+
+ {{end}} + {{range .flashSuccess}} +
+
+
+ + + +
+
+

{{.}}

+
+
+
+ {{end}} +
+ + {{ end }} diff --git a/templates/base/gist_footer.html b/templates/base/gist_footer.html new file mode 100644 index 0000000..7e2646e --- /dev/null +++ b/templates/base/gist_footer.html @@ -0,0 +1,4 @@ +{{ define "gist_footer" }} + +
+{{ end }} diff --git a/templates/base/gist_header.html b/templates/base/gist_header.html new file mode 100644 index 0000000..c00b31d --- /dev/null +++ b/templates/base/gist_header.html @@ -0,0 +1,165 @@ +{{ define "gist_header" }} +
+
+
+ +

+ {{ .gist.User.Username }} / {{ .gist.Title }} +

+ {{ if .userLogged }} +
+ {{ .csrfHtml }} + + + {{ .gist.NbLikes }} + +
+ {{ else }} + + {{ end }} + {{ if .userLogged }}{{ if eq .gist.User.Username .userLogged.Username }} +
+ {{ .csrfHtml }} + +
+ + +
+ + + + + Edit + +
+
+ {{ .csrfHtml }} + +
+ {{ end }}{{ end }} + +
+

Last active {{ .gist.UpdatedAt }} • + {{ if .gist.Private }} Unlisted + {{else}} Public {{ end }} + +

+

{{ .gist.Description }}

+
+
+ +
+
+ + +
+ + {{ if .revision }} {{ if ne .revision "HEAD" }} +

Revision {{ .revision }}

+ {{ end }} {{ end }} +
+ + {{ end }} diff --git a/templates/base/pagination.html b/templates/base/pagination.html new file mode 100644 index 0000000..fa10302 --- /dev/null +++ b/templates/base/pagination.html @@ -0,0 +1,31 @@ +{{ define "pagination" }} +
+ {{ if .prevPage }} + + + + + + Newer + {{ else }} + + + + + Newer + {{ end }} + {{ if .nextPage }} + Older + + + + + {{ else }} + Older + + + + + {{ end }} +
+{{ end }} \ No newline at end of file diff --git a/templates/pages/admin_gists.html b/templates/pages/admin_gists.html new file mode 100644 index 0000000..2b67788 --- /dev/null +++ b/templates/pages/admin_gists.html @@ -0,0 +1,43 @@ +{{ template "header" .}} +{{ template "admin_header" .}} + +
+ + + + + + + + + + + + + + + {{ range $gist := .data }} + + + + + + + + + + + {{ end }} + +
IDTitleUserPrivate ?# files# likesCreated at + Delete +
{{ $gist.ID }}{{ $gist.Title }}{{ $gist.User.Username }}{{ $gist.Private }}{{ $gist.NbFiles }}{{ $gist.NbLikes }}{{ $gist.CreatedAt }} +
+ {{ $.csrfHtml }} + +
+
+
+ +{{ template "admin_footer" .}} +{{ template "footer" .}} diff --git a/templates/pages/admin_index.html b/templates/pages/admin_index.html new file mode 100644 index 0000000..92184a7 --- /dev/null +++ b/templates/pages/admin_index.html @@ -0,0 +1,55 @@ +{{ template "header" .}} +{{ template "admin_header" .}} + +
+
+
+
+ Versions +
+ + + + + + + + + + + + + + + +
Opengist{{ .opengistVersion }}
Go{{ .goVersion }}
Git{{ .gitVersion }}
+
+
+ +
+
+
+ Stats +
+ + + + + + + + + + + + + + + +
Users{{ .countUsers }}
Gists{{ .countGists }}
SSH keys{{ .countKeys }}
+
+
+
+ +{{ template "admin_footer" .}} +{{ template "footer" .}} diff --git a/templates/pages/admin_users.html b/templates/pages/admin_users.html new file mode 100644 index 0000000..24760b8 --- /dev/null +++ b/templates/pages/admin_users.html @@ -0,0 +1,35 @@ +{{ template "header" .}} +{{ template "admin_header" .}} + +
+ + + + + + + + + + + {{ range $user := .data }} + + + + + + + {{ end }} + +
IDUsernameCreated + Delete +
{{ $user.ID }}{{ $user.Username }}{{ $user.CreatedAt }} +
+ {{ $.csrfHtml }} + +
+
+
+ +{{ template "admin_footer" .}} +{{ template "footer" .}} diff --git a/templates/pages/all.html b/templates/pages/all.html new file mode 100644 index 0000000..c2860c5 --- /dev/null +++ b/templates/pages/all.html @@ -0,0 +1,109 @@ +{{ template "header" .}} +
+
+
+

All gists {{if .fromUser}} from {{.fromUser}} {{end}}

+
+
+
+
+ +
+ +
+ +
+
+
+
+ {{ if ne (len .gists) 0 }} + {{ range $gist := .gists }} +
+
+

+ {{ $gist.User.Username }} / {{ $gist.Title }} +

+
+
+ + + + {{ $gist.NbLikes }} likes +
+
+ + + + {{ $gist.NbFiles }} files +
+
+ +
+
Last active {{ $gist.UpdatedAt }} + {{ if $gist.Private }} • Unlisted {{ end }}
+
{{ $gist.Description }}
+ +
+
+ {{ if isMarkdown $gist.PreviewFilename }} +
{{ $gist.Preview }}
+ {{ else }} + + + {{ $ii := "1" }} + {{ $i := toInt $ii }} + {{ range $line := lines $gist.Preview }} + + + + + + {{ $i = inc $i }} + {{ end }} + +
{{$i}}{{ $line }}
+ {{ end }} +
+
+
+
+ {{ end }} + + {{ template "pagination" . }} + {{ else }} +
+ + + +

No gists

+
+ {{ end }} +
+
+
+{{ template "footer" .}} diff --git a/templates/pages/auth_form.html b/templates/pages/auth_form.html new file mode 100644 index 0000000..9d05793 --- /dev/null +++ b/templates/pages/auth_form.html @@ -0,0 +1,58 @@ +{{ template "header" .}} +
+
+ +

+ {{ .title }} +

+ +
+
+ {{ if and .signupDisabled (ne .title "Login") }} +

Administrator has disabled signing up

+ {{ else }} +
+
+
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+ {{ if eq .title "Login" }} +
+
+ +
+ {{ if not .signupDisabled }} + Register instead → + {{ end }} +
+ {{ else }} +
+
+ +
+ Login instead → + +
+ {{ end }} + {{ .csrfHtml }} +
+
+
+
+ {{ end }} +
+
+ +{{ template "footer" .}} diff --git a/templates/pages/create.html b/templates/pages/create.html new file mode 100644 index 0000000..ec8f6fa --- /dev/null +++ b/templates/pages/create.html @@ -0,0 +1,49 @@ +{{ template "header" .}} +
+
+ +

+ New Gist +

+ +
+
+
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+

+ +

+
+ +
+
+ +
+ + + +
+ {{ .csrfHtml }} +
+ +
+
+ + + +{{ template "footer" .}} diff --git a/templates/pages/edit.html b/templates/pages/edit.html new file mode 100644 index 0000000..5ae449e --- /dev/null +++ b/templates/pages/edit.html @@ -0,0 +1,57 @@ +{{ template "header" .}} +
+
+ +

+ Editing {{ .gist.Title }} +

+ +
+
+
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+ {{ range $filename, $content := .files }} +
+
+

+ + +

+
+ +
+ {{ end }} +
+ +
+ + Cancel + +
+ {{ .csrfHtml }} +
+ +
+
+ + + + +{{ template "footer" .}} diff --git a/templates/pages/error.html b/templates/pages/error.html new file mode 100644 index 0000000..5e839a3 --- /dev/null +++ b/templates/pages/error.html @@ -0,0 +1,14 @@ +{{ template "header" .}} + +
+ + + + +

Error {{ .error.Code }}

+

{{ httpStatusText .error.Code }}

+ {{ if lt .error.Code 500 }} +

{{ .error.Message }}

+ {{ end }} +
+{{ template "footer" .}} diff --git a/templates/pages/gist.html b/templates/pages/gist.html new file mode 100644 index 0000000..36aaabf --- /dev/null +++ b/templates/pages/gist.html @@ -0,0 +1,45 @@ +{{ template "header" .}} +{{ template "gist_header" .}} + {{ if .files }} +
+ {{ range $filename, $content := .files }} +
+
+
+ + + + {{ $filename }} + + + Raw + +
+
+
+ {{ if isMarkdown $filename }} +
{{ $content }}
+ {{ else }} + {{ $fileslug := slug $filename }} + + + {{ $ii := "1" }} + {{ $i := toInt $ii }} + {{ range $line := lines $content }}{{ $i = inc $i }}{{ end }} + +
{{$i}}{{ $line }}
+ {{ end }} +
+
+ {{ end }} +
+ {{ else }} +
+ + + +

No content

+
+ {{ end }} +{{ template "gist_footer" .}} +{{ template "footer" .}} diff --git a/templates/pages/likes.html b/templates/pages/likes.html new file mode 100644 index 0000000..74b6980 --- /dev/null +++ b/templates/pages/likes.html @@ -0,0 +1,27 @@ +{{ template "header" .}} +{{ template "gist_header" .}} + {{ if ne (len .likers) 0 }} +

Likes

+
+ {{ range $user := .likers }} + + {{ end }} +
+ {{ else }} +
+ + + + +

No likes yet

+
+ {{ end }} +{{ template "gist_footer" .}} +{{ template "footer" .}} diff --git a/templates/pages/revisions.html b/templates/pages/revisions.html new file mode 100644 index 0000000..00f96f5 --- /dev/null +++ b/templates/pages/revisions.html @@ -0,0 +1,110 @@ +{{ template "header" .}} +{{ template "gist_header" .}} +{{ if ne (len .commits) 0 }} + +
+ {{ range $commit := .commits }} +
+
+

+ + {{ $commit.Author }} revised this gist {{ $commit.Timestamp }}. Go to revision

+ {{ if ne $commit.Changed "" }} +

+ + + + {{ $commit.Changed }} + {{ end }} +

+
+
+ {{ if ne (len $commit.Files) 0 }} + {{ range $file := $commit.Files }} +
+
+

+ + + + {{ if eq $file.Filename $file.OldFilename }} + {{ $file.Filename }} + {{ else }} + {{ if eq $file.OldFilename "/dev/null" }} + {{ $file.Filename }}(file created) + {{ else if eq $file.Filename "/dev/null" }} + {{ $file.OldFilename }} (file deleted) + {{ else }} + {{ $file.OldFilename }} renamed to {{ $file.Filename }} + {{ end }} + {{ end }} +

+
+
+ {{ if eq $file.Content "" }} +

+ File renamed without changes. +

+ {{ else }} + + + {{ $left := 0 }} + {{ $right := 0 }} + {{ range $line := split $file.Content "\n" }} + {{ if ne $line "" }}{{ if ne (index $line 0) 92 }} + + {{ if eq (index $line 0) 64 }} + {{ $left = toInt (index (splitGit (index (split $line "-") 1)) 0) }} + {{ $right = toInt (index (splitGit (index (split $line "+") 1)) 0) }} + {{ end }} + + {{ if eq (index $line 0) 64 }} + + {{ else }} + {{ if eq (index $line 0) 43 }} + + + {{ $right = inc $right }} + {{ else if eq (index $line 0) 45 }} + + + {{ $left = inc $left }} + {{ else if eq (index $line 0) 32 }} + + + {{ $left = inc $left }} + {{ $right = inc $right }} + {{ end }} + {{ end }} + + + + {{end}} + {{end}}{{end}} + +
{{ $right }}{{ $left }}{{ $left }}{{ $right }}{{ if ne (index $line 0) 64 }}{{ slice $line 0 1 }}{{ end }}{{ if ne (index $line 0) 64 }}{{ slice $line 1 }}{{ else }}{{ $line }}{{ end }}
+ {{ end }} +
+
+ {{end}} + {{else}} +

No changes

+ {{end}} +
+
+ {{end}} +
+
+ {{ template "pagination" . }} +
+{{ else }} +
+ + + +

No revisions to show

+
+{{ end }} + +{{ template "gist_footer" .}} +{{ template "footer" .}} diff --git a/templates/pages/ssh_keys.html b/templates/pages/ssh_keys.html new file mode 100644 index 0000000..3e906e3 --- /dev/null +++ b/templates/pages/ssh_keys.html @@ -0,0 +1,73 @@ +{{ template "header" .}} +
+
+
+

SSH Keys

+

Used only to pull/push gists using Git via SSH

+
+
+
+
+
+
+
+

+ Add SSH Key +

+
+
+ +
+ +
+
+ +
+ +
+ +
+
+ + {{ .csrfHtml }} +
+
+
+
+
+
    + {{ if .sshKeys }} + {{ range $key := .sshKeys }} +
  • +
    + + + +
    +

    {{ .Title }}

    +

    SHA256:{{.SHA}}

    +

    Added {{ .CreatedAt }}

    + {{ if eq .LastUsedAt 0 }} +

    Never used

    + {{ else }} +

    Last used {{ .LastUsedAt }}

    + {{ end }} +
    +
    + + {{ $.csrfHtml }} + + +
    +
    +
  • + {{ end }} + {{ end }} +
+
+
+
+
+
+
+{{ template "footer" .}} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..868ff4a --- /dev/null +++ b/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + root: './public', + + build: { + // generate manifest.json in outDir + outDir: '', + assetsDir: 'assets', + manifest: true, + rollupOptions: { + // overwrite default .html entry + input: ['./public/main.js', './public/editor.js'] + } + } +}) \ No newline at end of file