From cb21a041ada3070c6dca5418e703cfd6af0ae3b8 Mon Sep 17 00:00:00 2001 From: Jonni Liljamo Date: Sun, 17 Nov 2024 16:36:16 +0200 Subject: [PATCH] feat: initial * templ rendering * oidc login --- .envrc | 1 + .gitignore | 7 + LICENSE | 621 +++++++++++++++++++++++++ NOTICE | 15 + README.md | 4 + cmd/emerwen-web/main.go | 53 +++ flake.lock | 263 +++++++++++ flake.nix | 83 ++++ go.mod | 46 ++ go.sum | 112 +++++ input.css | 3 + internal/auth/auth.go | 46 ++ internal/auth/claims.go | 14 + internal/components/base.templ | 85 ++++ internal/components/error.templ | 12 + internal/components/index.templ | 9 + internal/config/config.go | 80 ++++ internal/consts/consts.go | 15 + internal/handlers/context.go | 28 ++ internal/handlers/handlers.go | 9 + internal/handlers/index.go | 24 + internal/handlers/login.go | 38 ++ internal/handlers/logout.go | 24 + internal/handlers/noroute.go | 24 + internal/handlers/oauth2.go | 58 +++ internal/log/log.go | 37 ++ internal/middlewares/middlewares.go | 9 + internal/middlewares/requiresession.go | 28 ++ internal/middlewares/sessionexists.go | 37 ++ internal/renderer/renderer.go | 62 +++ internal/router/router.go | 45 ++ justfile | 14 + static/createdwith.jpeg | Bin 0 -> 29823 bytes static/htmx.min.js | 1 + static/svg/sheep.svg | 46 ++ tailwind.config.js | 69 +++ 36 files changed, 2022 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 cmd/emerwen-web/main.go create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 go.mod create mode 100644 go.sum create mode 100644 input.css create mode 100644 internal/auth/auth.go create mode 100644 internal/auth/claims.go create mode 100644 internal/components/base.templ create mode 100644 internal/components/error.templ create mode 100644 internal/components/index.templ create mode 100644 internal/config/config.go create mode 100644 internal/consts/consts.go create mode 100644 internal/handlers/context.go create mode 100644 internal/handlers/handlers.go create mode 100644 internal/handlers/index.go create mode 100644 internal/handlers/login.go create mode 100644 internal/handlers/logout.go create mode 100644 internal/handlers/noroute.go create mode 100644 internal/handlers/oauth2.go create mode 100644 internal/log/log.go create mode 100644 internal/middlewares/middlewares.go create mode 100644 internal/middlewares/requiresession.go create mode 100644 internal/middlewares/sessionexists.go create mode 100644 internal/renderer/renderer.go create mode 100644 internal/router/router.go create mode 100644 justfile create mode 100644 static/createdwith.jpeg create mode 100644 static/htmx.min.js create mode 100644 static/svg/sheep.svg create mode 100644 tailwind.config.js diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..c4b17d7 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use_flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0c9d0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.direnv/ +/tmp/ + +/.pre-commit-config.yaml +/internal/components/*_templ.go +/internal/components/*_templ.txt +/static/style.css diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..810fce6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,621 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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 diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..6940185 --- /dev/null +++ b/NOTICE @@ -0,0 +1,15 @@ +emerwen-web, a web interface for controlling emerwen +Copyright (C) 2024 Jonni Liljamo + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/README.md b/README.md new file mode 100644 index 0000000..1940c93 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# emerwen-web + +## Assets +- https://www.svgrepo.com/svg/481486/sheep - Public Domain diff --git a/cmd/emerwen-web/main.go b/cmd/emerwen-web/main.go new file mode 100644 index 0000000..53253e0 --- /dev/null +++ b/cmd/emerwen-web/main.go @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +// emerwen-web +package main + +import ( + "log/slog" + "net/http" + "time" + + "git.src.quest/~liljamo/emerwen-web/internal/auth" + "git.src.quest/~liljamo/emerwen-web/internal/config" + "git.src.quest/~liljamo/emerwen-web/internal/log" + "git.src.quest/~liljamo/emerwen-web/internal/router" + "github.com/alexedwards/scs/v2" + "golang.org/x/sync/errgroup" +) + +var ( + g errgroup.Group + sm *scs.SessionManager + a *auth.Auth +) + +func main() { + l := log.InitDefaultLogger() + + c := config.ParseFromArgs() + + a = auth.New(&c) + + sm = scs.New() + sm.Lifetime = 1 * time.Hour + + slog.Info("serving", slog.String("bindaddr", c.BindAddr)) + s := &http.Server{ + Addr: c.BindAddr, + Handler: sm.LoadAndSave(router.SetupRouter(l, a, sm)), + } + + g.Go(func() error { + return s.ListenAndServe() + }) + + if err := g.Wait(); err != nil { + slog.Error("encountered an error while running", slog.Any("err", err)) + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..142738b --- /dev/null +++ b/flake.lock @@ -0,0 +1,263 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1730504689, + "narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "506278e768c2a08bec68eb62932193e341f55c90", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "gitignore_2": { + "inputs": { + "nixpkgs": [ + "templ", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "gomod2nix": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": [ + "templ", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1722589758, + "narHash": "sha256-sbbA8b6Q2vB/t/r1znHawoXLysCyD4L/6n6/RykiSnA=", + "owner": "nix-community", + "repo": "gomod2nix", + "rev": "4e08ca09253ef996bd4c03afa383b23e35fe28a1", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "gomod2nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1731531548, + "narHash": "sha256-sz8/v17enkYmfpgeeuyzniGJU0QQBfmAjlemAUYhfy8=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "24f0d4acd634792badd6470134c387a3b039dace", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1730741070, + "narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d063c1dd113c91ab27959ba540c0d9753409edf3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ], + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1731363552, + "narHash": "sha256-vFta1uHnD29VUY4HJOO/D6p6rxyObnf+InnSMT4jlMU=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "cd1af27aa85026ac759d5d3fccf650abe7e1bbf0", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks", + "templ": "templ" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "templ": { + "inputs": { + "gitignore": "gitignore_2", + "gomod2nix": "gomod2nix", + "nixpkgs": [ + "nixpkgs" + ], + "xc": "xc" + }, + "locked": { + "lastModified": 1730278073, + "narHash": "sha256-0KGht5IMbJV8KkXgT5qJxA9bcmWevzXXAVPMQTm0ccw=", + "owner": "a-h", + "repo": "templ", + "rev": "d9eefff2eeea5c78c938baf556d7ded6880e2fca", + "type": "github" + }, + "original": { + "owner": "a-h", + "ref": "tags/v0.2.793", + "repo": "templ", + "type": "github" + } + }, + "xc": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": [ + "templ", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1724404748, + "narHash": "sha256-p6rXzNiDm2uBvO1MLzC5pJp/0zRNzj/snBzZI0ce62s=", + "owner": "joerdav", + "repo": "xc", + "rev": "960ff9f109d47a19122cfb015721a76e3a0f23a2", + "type": "github" + }, + "original": { + "owner": "joerdav", + "repo": "xc", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..186b8c7 --- /dev/null +++ b/flake.nix @@ -0,0 +1,83 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-parts = { + url = "github:hercules-ci/flake-parts"; + inputs.nixpkgs-lib.follows = "nixpkgs"; + }; + + pre-commit-hooks = { + url = "github:cachix/git-hooks.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + templ = { + url = "github:a-h/templ?ref=tags/v0.2.793"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = inputs @ { + self, + nixpkgs, + flake-parts, + pre-commit-hooks, + templ, + ... + }: + flake-parts.lib.mkFlake {inherit inputs;} { + systems = ["x86_64-linux"]; + perSystem = { + config, + lib, + pkgs, + system, + ... + }: let + libs = []; + in { + checks.pre-commit-check = pre-commit-hooks.lib.${system}.run { + src = ./.; + hooks = { + # Nix formatting + alejandra.enable = true; + + # Go formatting, linting, static checking + gofmt.enable = true; + govet.enable = true; + revive.enable = true; + + # Spell checking + typos = { + enable = true; + excludes = ["static/htmx.min.js"]; + }; + }; + }; + + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; + [ + just + + go + gopls + + air + + tailwindcss + tailwindcss-language-server + ] + ++ libs + ++ [ + templ.packages.${system}.default + self.checks.${system}.pre-commit-check.enabledPackages + ]; + LD_LIBRARY_PATH = lib.makeLibraryPath libs; + shellHook = '' + ${self.checks.${system}.pre-commit-check.shellHook} + ''; + }; + }; + }; +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..767ebd5 --- /dev/null +++ b/go.mod @@ -0,0 +1,46 @@ +module git.src.quest/~liljamo/emerwen-web + +go 1.23.2 + +require ( + github.com/a-h/templ v0.2.793 + github.com/alexedwards/scs/v2 v2.8.0 + github.com/coreos/go-oidc/v3 v3.11.0 + github.com/gin-gonic/gin v1.10.0 + github.com/samber/slog-gin v1.13.6 + golang.org/x/oauth2 v0.21.0 + golang.org/x/sync v0.8.0 +) + +require ( + github.com/bytedance/sonic v1.11.9 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.4 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e2341d1 --- /dev/null +++ b/go.sum @@ -0,0 +1,112 @@ +github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY= +github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= +github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= +github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= +github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg= +github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= +github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= +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/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= +github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +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/samber/slog-gin v1.13.6 h1:clWpgtdL/3KM1eGkUMUHTLNM1fG4nA3bHC1Pt7v0z44= +github.com/samber/slog-gin v1.13.6/go.mod h1:iicbXYT1DozbzsbLfpRdXkAal3zmzIjayQCV5YR+A6M= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/input.css b/input.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..404d36a --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +// Package auth provides authentication related types and mechanisms. +package auth + +import ( + "context" + "fmt" + + "git.src.quest/~liljamo/emerwen-web/internal/config" + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" +) + +// Auth holds the OIDC provider and OAuth2 config. +type Auth struct { + Provider *oidc.Provider + Config oauth2.Config +} + +// New constructs a new Auth struct. +// Panics if OIDC provider can't be constructed. +func New(c *config.Config) *Auth { + provider, err := oidc.NewProvider(context.Background(), c.OIDCProvider) + if err != nil { + panic(fmt.Sprintf("failed to create OIDC provider: %s", err)) + } + + config := oauth2.Config{ + ClientID: c.OIDCClientID, + ClientSecret: c.OIDCClientSecret, + RedirectURL: c.OIDCRedirectURL, + Endpoint: provider.Endpoint(), + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + } + + return &Auth{ + Provider: provider, + Config: config, + } +} diff --git a/internal/auth/claims.go b/internal/auth/claims.go new file mode 100644 index 0000000..db9deff --- /dev/null +++ b/internal/auth/claims.go @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +package auth + +// Claims holds claims of a user that are read after login. +type Claims struct { + Name string `json:"name"` + Email string `json:"email"` +} diff --git a/internal/components/base.templ b/internal/components/base.templ new file mode 100644 index 0000000..3648e4b --- /dev/null +++ b/internal/components/base.templ @@ -0,0 +1,85 @@ +package components + +import "context" +import "git.src.quest/~liljamo/emerwen-web/internal/consts" + +func loggedIn(ctx context.Context) bool { + if loggedIn, ok := ctx.Value(consts.KeyLoggedIn).(bool); ok { + return loggedIn + } + return false +} + +func userName(ctx context.Context) string { + if userName, ok := ctx.Value(consts.KeyUserName).(string); ok { + return userName + } + return "NIL_USERNAME" +} + +templ ButtonLink(visual string, href string) { + + { visual } + +} + +templ Base(title string) { + + + + + + + + + + + { title } + + +
+
+
+ + emerwen + + +
+ +
+
+ +
+
counting sheep
+
+ +
+ if loggedIn(ctx) { +
+ { userName(ctx) } +
+ @ButtonLink("logout", "/logout") + } else { + @ButtonLink("login", "/login") + } +
+
+ +
+ { children... } +
+ +
+ +
+
+ + +} diff --git a/internal/components/error.templ b/internal/components/error.templ new file mode 100644 index 0000000..9e43db2 --- /dev/null +++ b/internal/components/error.templ @@ -0,0 +1,12 @@ +package components + +import "strconv" + +templ Error(c int, m string) { + @Base("emerwen") { +
+

{ strconv.Itoa(c) }

+

{ m }

+
+ } +} diff --git a/internal/components/index.templ b/internal/components/index.templ new file mode 100644 index 0000000..5651572 --- /dev/null +++ b/internal/components/index.templ @@ -0,0 +1,9 @@ +package components + +templ Index() { + @Base("emerwen, counting sheep") { +
+ +
+ } +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..3fe56b4 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +// Package config provides a Config type and parsing function. +package config + +import ( + "flag" + "fmt" + "os" + "strings" +) + +// Config holds application configuration values. +type Config struct { + BindAddr string + + OIDCClientID string + OIDCClientSecret string + // protocol://domain.tld/oauth2/oidc/callback + OIDCRedirectURL string + OIDCProvider string +} + +// ParseFromArgs parses program flags into a Config. +// Panics if certain required values are not provided, or when fails to read +// a file. +func ParseFromArgs() Config { + bindAddrPtr := flag.String("bind_address", "127.0.0.1:3000", "bind address") + + oidcClientIDPtr := flag.String("oidc_client_id", "emerwen", "OIDC client ID") + oidcClientIDFilePtr := flag.String("oidc_client_id_file", "", "OIDC client ID file") + oidcClientSecretFilePtr := flag.String("oidc_client_secret_file", "", "OIDC client secret file") + oidcRedirectURLPtr := flag.String("oidc_redirect_url", "", "OIDC redirect URL") + oidcProviderPtr := flag.String("oidc_provider", "", "OIDC provider") + + flag.Parse() + + var oidcClientID string + if *oidcClientIDFilePtr == "" { + oidcClientID = *oidcClientIDPtr + } else { + b, err := os.ReadFile(*oidcClientIDFilePtr) + if err != nil { + panic(fmt.Sprintf("failed to read oidc_client_id_file: %s", err)) + } + oidcClientID = strings.TrimSpace(string(b)) + } + + var oidcClientSecret string + if *oidcClientSecretFilePtr == "" { + panic("oidc_client_secret_file is required") + } + b, err := os.ReadFile(*oidcClientSecretFilePtr) + if err != nil { + panic(fmt.Sprintf("failed to read oidc_client_secret_file: %s", err)) + } + oidcClientSecret = strings.TrimSpace(string(b)) + + if *oidcRedirectURLPtr == "" { + panic("oidc_redirect_url is required") + } + + if *oidcProviderPtr == "" { + panic("oidc_endpoint is required") + } + + return Config{ + BindAddr: *bindAddrPtr, + + OIDCClientID: oidcClientID, + OIDCClientSecret: oidcClientSecret, + OIDCRedirectURL: *oidcRedirectURLPtr, + OIDCProvider: *oidcProviderPtr, + } +} diff --git a/internal/consts/consts.go b/internal/consts/consts.go new file mode 100644 index 0000000..39ec0fa --- /dev/null +++ b/internal/consts/consts.go @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +// Package consts defines application wide constants. +package consts + +// KeyLoggedIn defines the "logged in" key. +const KeyLoggedIn = "logged_in" + +// KeyUserName defines the "user name" key. +const KeyUserName = "user_name" diff --git a/internal/handlers/context.go b/internal/handlers/context.go new file mode 100644 index 0000000..84a75d7 --- /dev/null +++ b/internal/handlers/context.go @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +package handlers + +import ( + "context" + + "git.src.quest/~liljamo/emerwen-web/internal/consts" + "github.com/gin-gonic/gin" +) + +// BaseContext constructs a new context with values that are needed for every +// page. +func BaseContext(c *gin.Context) context.Context { + var ctx context.Context + if loggedIn, ok := c.Value(consts.KeyLoggedIn).(bool); ok { + ctx = context.WithValue(c.Request.Context(), consts.KeyLoggedIn, loggedIn) + } + if userName, ok := c.Value(consts.KeyUserName).(string); ok { + ctx = context.WithValue(ctx, consts.KeyUserName, userName) + } + return ctx +} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go new file mode 100644 index 0000000..83883fd --- /dev/null +++ b/internal/handlers/handlers.go @@ -0,0 +1,9 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +// Package handlers provides route handlers. +package handlers diff --git a/internal/handlers/index.go b/internal/handlers/index.go new file mode 100644 index 0000000..02cdf61 --- /dev/null +++ b/internal/handlers/index.go @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +package handlers + +import ( + "net/http" + + "git.src.quest/~liljamo/emerwen-web/internal/components" + "git.src.quest/~liljamo/emerwen-web/internal/renderer" + "github.com/gin-gonic/gin" +) + +// Index returns a gin handler for the index route. +func Index() gin.HandlerFunc { + return func(c *gin.Context) { + r := renderer.New(BaseContext(c), http.StatusOK, components.Index()) + c.Render(http.StatusOK, r) + } +} diff --git a/internal/handlers/login.go b/internal/handlers/login.go new file mode 100644 index 0000000..32f60dc --- /dev/null +++ b/internal/handlers/login.go @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +package handlers + +import ( + "math/rand" + "net/http" + + "git.src.quest/~liljamo/emerwen-web/internal/auth" + "github.com/alexedwards/scs/v2" + "github.com/gin-gonic/gin" +) + +// Login returns a gin handler for the login route. +func Login(a *auth.Auth, sm *scs.SessionManager) gin.HandlerFunc { + return func(c *gin.Context) { + state := generateState() + + sm.Put(c.Request.Context(), "state", state) + + c.Redirect(http.StatusTemporaryRedirect, a.Config.AuthCodeURL(state)) + } +} + +const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" + +func generateState() string { + b := make([]byte, 32) + for i := range b { + b[i] = chars[rand.Intn(len(chars))] + } + return string(b) +} diff --git a/internal/handlers/logout.go b/internal/handlers/logout.go new file mode 100644 index 0000000..b5cfdb0 --- /dev/null +++ b/internal/handlers/logout.go @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +package handlers + +import ( + "net/http" + + "github.com/alexedwards/scs/v2" + "github.com/gin-gonic/gin" +) + +// Logout returns a gin handler for the logout route. +func Logout(sm *scs.SessionManager) gin.HandlerFunc { + return func(c *gin.Context) { + sm.Destroy(c.Request.Context()) + + c.Redirect(http.StatusTemporaryRedirect, "/") + } +} diff --git a/internal/handlers/noroute.go b/internal/handlers/noroute.go new file mode 100644 index 0000000..88a5391 --- /dev/null +++ b/internal/handlers/noroute.go @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +package handlers + +import ( + "net/http" + + "git.src.quest/~liljamo/emerwen-web/internal/components" + "git.src.quest/~liljamo/emerwen-web/internal/renderer" + "github.com/gin-gonic/gin" +) + +// NoRoute returns a gin handler for undefined routes. +func NoRoute() gin.HandlerFunc { + return func(c *gin.Context) { + r := renderer.New(BaseContext(c), http.StatusNotFound, components.Error(http.StatusNotFound, "you lost the sheep!")) + c.Render(http.StatusNotFound, r) + } +} diff --git a/internal/handlers/oauth2.go b/internal/handlers/oauth2.go new file mode 100644 index 0000000..65cd851 --- /dev/null +++ b/internal/handlers/oauth2.go @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +package handlers + +import ( + "net/http" + + "git.src.quest/~liljamo/emerwen-web/internal/auth" + "git.src.quest/~liljamo/emerwen-web/internal/components" + "github.com/alexedwards/scs/v2" + "github.com/coreos/go-oidc/v3/oidc" + "github.com/gin-gonic/gin" +) + +// OAuth2OIDCCallback returns a gin handler for the OAuth2 OIDC callback route. +func OAuth2OIDCCallback(a *auth.Auth, sm *scs.SessionManager) gin.HandlerFunc { + verifier := a.Provider.Verifier(&oidc.Config{ClientID: a.Config.ClientID}) + + return func(c *gin.Context) { + if c.Query("state") != sm.GetString(c.Request.Context(), "state") { + c.HTML(http.StatusBadRequest, "", components.Error(http.StatusBadRequest, "state mismatch")) + return + } + + oauth2Token, err := a.Config.Exchange(c.Request.Context(), c.Query("code")) + if err != nil { + c.HTML(http.StatusBadRequest, "", components.Error(http.StatusBadRequest, err.Error())) + return + } + + rawIDToken, ok := oauth2Token.Extra("id_token").(string) + if !ok { + c.HTML(http.StatusBadRequest, "", components.Error(http.StatusBadRequest, "no id_token")) + return + } + + idToken, err := verifier.Verify(c.Request.Context(), rawIDToken) + if err != nil { + c.HTML(http.StatusBadRequest, "", components.Error(http.StatusBadRequest, err.Error())) + return + } + + var claims auth.Claims + if err := idToken.Claims(&claims); err != nil { + c.HTML(http.StatusInternalServerError, "", components.Error(http.StatusInternalServerError, err.Error())) + return + } + + sm.Put(c.Request.Context(), "claims", claims) + + c.Redirect(http.StatusTemporaryRedirect, "/") + } +} diff --git a/internal/log/log.go b/internal/log/log.go new file mode 100644 index 0000000..b76a7a3 --- /dev/null +++ b/internal/log/log.go @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +// Package log provides logging utilities. +package log + +import ( + "log/slog" + "os" + "path/filepath" +) + +// InitDefaultLogger sets and returns the default slog logger. +func InitDefaultLogger() *slog.Logger { + replace := func( /* groups */ _ []string, a slog.Attr) slog.Attr { + if a.Key == slog.SourceKey { + source := a.Value.Any().(*slog.Source) + source.File = filepath.Base(source.File) + } + + return a + } + + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + AddSource: true, + ReplaceAttr: replace, + })) + + slog.SetDefault(logger) + + return logger +} diff --git a/internal/middlewares/middlewares.go b/internal/middlewares/middlewares.go new file mode 100644 index 0000000..1416bfe --- /dev/null +++ b/internal/middlewares/middlewares.go @@ -0,0 +1,9 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +// Package middlewares provides various gin middlewares. +package middlewares diff --git a/internal/middlewares/requiresession.go b/internal/middlewares/requiresession.go new file mode 100644 index 0000000..0c22f36 --- /dev/null +++ b/internal/middlewares/requiresession.go @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +package middlewares + +import ( + "net/http" + + "github.com/alexedwards/scs/v2" + "github.com/gin-gonic/gin" +) + +// RequireSession returns a gin middleware for requiring a session to be present. +func RequireSession(sm *scs.SessionManager) gin.HandlerFunc { + return func(c *gin.Context) { + claims := sm.Get(c.Request.Context(), "claims") + if claims != nil { + c.Next() + } else { + c.Redirect(http.StatusTemporaryRedirect, "/login") + c.Abort() + } + } +} diff --git a/internal/middlewares/sessionexists.go b/internal/middlewares/sessionexists.go new file mode 100644 index 0000000..07a6a8f --- /dev/null +++ b/internal/middlewares/sessionexists.go @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +package middlewares + +import ( + "git.src.quest/~liljamo/emerwen-web/internal/auth" + "git.src.quest/~liljamo/emerwen-web/internal/consts" + "github.com/alexedwards/scs/v2" + "github.com/gin-gonic/gin" +) + +// CheckForSession returns a gin middleware for checking if a session exists +// and setting context values if one exists. +func CheckForSession(sm *scs.SessionManager) gin.HandlerFunc { + return func(c *gin.Context) { + + if claims, ok := sm.Get(c.Request.Context(), "claims").(auth.Claims); ok { + c.Set(consts.KeyLoggedIn, true) + + if claims.Name != "" { + c.Set(consts.KeyUserName, claims.Name) + } else { + c.Set(consts.KeyUserName, claims.Email) + } + + c.Next() + } else { + c.Set(consts.KeyLoggedIn, false) + c.Next() + } + } +} diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go new file mode 100644 index 0000000..a236510 --- /dev/null +++ b/internal/renderer/renderer.go @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +// Package renderer provides a gin HTMLRender implementation. +package renderer + +import ( + "context" + "net/http" + + "github.com/a-h/templ" + "github.com/gin-gonic/gin/render" +) + +// TemplRender is a gin HTMLRender implementation for templ rendering. +type TemplRender struct { + Ctx context.Context + Status int + Component templ.Component +} + +// New constructs a new TemplRender struct. +func New(ctx context.Context, status int, component templ.Component) *TemplRender { + return &TemplRender{ + Ctx: ctx, + Status: status, + Component: component, + } +} + +// Instance returns a new TemplRender instance. +func (t *TemplRender) Instance( /* name */ _ string, component interface{}) render.Render { + if templComponent, ok := component.(templ.Component); ok { + return &TemplRender{ + Ctx: context.Background(), + Status: -1, + Component: templComponent, + } + } + return nil +} + +// Render executes a template and writes its result. +func (t TemplRender) Render(w http.ResponseWriter) error { + t.WriteContentType(w) + if t.Status != -1 { + w.WriteHeader(t.Status) + } + if t.Component != nil { + return t.Component.Render(t.Ctx, w) + } + return nil +} + +// WriteContentType writes writes the content type. +func (t TemplRender) WriteContentType(w http.ResponseWriter) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") +} diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 0000000..5de07a7 --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +// Package router provides functions for setting up the router. +package router + +import ( + "encoding/gob" + "log/slog" + + "git.src.quest/~liljamo/emerwen-web/internal/auth" + "git.src.quest/~liljamo/emerwen-web/internal/handlers" + "git.src.quest/~liljamo/emerwen-web/internal/middlewares" + "git.src.quest/~liljamo/emerwen-web/internal/renderer" + "github.com/alexedwards/scs/v2" + "github.com/gin-gonic/gin" + sloggin "github.com/samber/slog-gin" +) + +// SetupRouter returns a gin engine. +func SetupRouter(l *slog.Logger, a *auth.Auth, sm *scs.SessionManager) *gin.Engine { + r := gin.New() + r.Use(gin.Recovery()) + r.Use(sloggin.New(l)) + r.Static("/static", "./static") + r.HTMLRender = &renderer.TemplRender{} + + gob.Register(auth.Claims{}) + + r.Use(middlewares.CheckForSession(sm)) + + r.NoRoute(handlers.NoRoute()) + + r.GET("/", handlers.Index()) + + r.GET("/login", handlers.Login(a, sm)) + r.GET("/logout", handlers.Logout(sm)) + r.GET("/oauth2/oidc/callback", handlers.OAuth2OIDCCallback(a, sm)) + + return r +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..b441399 --- /dev/null +++ b/justfile @@ -0,0 +1,14 @@ +_default: + just --list + +air *ARGS: + #!/usr/bin/env bash + export GIN_MODE=release + air --build.cmd "go build -o ./tmp/emerwen-web cmd/emerwen-web/main.go" \ + --build.bin "./tmp/emerwen-web" \ + --build.exclude_dir "tmp" \ + --build.include_ext "go,tmpl" \ + -- {{ARGS}} + +watch-templ-tailwindcss: + templ generate --watch & tailwindcss -i input.css -o ./static/style.css --watch && fg diff --git a/static/createdwith.jpeg b/static/createdwith.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..8acd8c06ee7f2c8c62eddc0cefb3458a0136f469 GIT binary patch literal 29823 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�e%IJ-538 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 V&Ozd#<$>$}v68Zh#=E!G{{!u-(`Enw literal 0 HcmV?d00001 diff --git a/static/htmx.min.js b/static/htmx.min.js new file mode 100644 index 0000000..423cf01 --- /dev/null +++ b/static/htmx.min.js @@ -0,0 +1 @@ +var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.3"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=de;Q.ajax=Rn;Q.find=r;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=h;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:dn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:i,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:dt,triggerEvent:de,triggerErrorEvent:fe,withExtensions:Ft};const o=["get","post","put","delete","patch"];const R=o.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function h(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function i(e,t){while(e&&!t(e)){e=c(e)}return e||null}function H(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;i(t,function(e){return!!(r=H(t,ue(e),n))});if(r!=="unset"){return r}}function d(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function N(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function A(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(A(e)){const t=N(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){C(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function D(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e=0}function le(e){const t=e.getRootNode&&e.getRootNode();if(t&&t instanceof window.ShadowRoot){return ne().body.contains(t.host)}else{return ne().body.contains(e)}}function F(e){return e.trim().split(/\s+/)}function ce(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){C(e);return null}}function B(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function U(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return vn(ne().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function r(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return r(ne(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(ne(),e)}}function E(){return window}function z(e,t){e=y(e);if(t){E().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function f(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ue(y(e));if(!e){return}if(n){E().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ue(y(e));if(!r){return}if(n){E().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function Z(e,t){e=y(e);se(e.parentElement.children,function(e){G(e,t)});K(ue(e),t)}function g(e,t){e=ue(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||d(e,t)){return e}}while(e=e&&ue(c(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function ge(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function p(e,t,n){e=y(e);if(t.indexOf("closest ")===0){return[g(ue(e),ge(t.substr(8)))]}else if(t.indexOf("find ")===0){return[r(f(e),ge(t.substr(5)))]}else if(t==="next"){return[ue(e).nextElementSibling]}else if(t.indexOf("next ")===0){return[pe(e,ge(t.substr(5)),!!n)]}else if(t==="previous"){return[ue(e).previousElementSibling]}else if(t.indexOf("previous ")===0){return[me(e,ge(t.substr(9)),!!n)]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else if(t==="root"){return[m(e,!!n)]}else if(t==="host"){return[e.getRootNode().host]}else if(t.indexOf("global ")===0){return p(e,t.slice(7),true)}else{return M(f(m(e,!!n)).querySelectorAll(ge(t)))}}var pe=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return p(e,t)[0]}else{return p(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return r(f(t)||document,e)}else{return e}}function xe(e,t,n,r){if(k(t)){return{target:ne().body,event:J(e),listener:t,options:n}}else{return{target:y(e),event:J(t),listener:n,options:r}}}function ye(t,n,r,o){Vn(function(){const e=xe(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function be(t,n,r){Vn(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const ve=ne().createElement("output");function we(e,t){const n=re(e,t);if(n){if(n==="this"){return[Se(e,t)]}else{const r=p(e,n);if(r.length===0){C('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ue(i(e,function(e){return te(ue(e),t)!=null}))}function Ee(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Ce(t){const n=Q.config.attributesToSettle;for(let e=0;e0){s=e.substr(0,e.indexOf(":"));n=e.substr(e.indexOf(":")+1,e.length)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=p(t,n,false);if(r){se(r,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=f(n)}const r={shouldSwap:true,target:e,fragment:t};if(!de(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}se(i.elts,function(e){de(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=r("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=r("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){se(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){if(e.moveBefore){let e=r("#--htmx-preserve-pantry--");if(e==null){ne().body.insertAdjacentHTML("afterend","

");e=r("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Le(l,e,c){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=f(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Ne(e){return function(){G(e,Q.config.addedClass);kt(ue(e));Ae(f(e));de(e,"htmx:load")}}function Ae(e){const t="[autofocus]";const n=$(d(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function u(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ne(o))}}}function Ie(e,t){let n=0;while(n0}function $e(e,t,r,o){if(!o){o={}}e=y(e);const i=o.contextElement?m(o.contextElement,false):ne();const n=document.activeElement;let s={};try{s={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const l=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=P(t);l.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t0){E().setTimeout(c,r.settleDelay)}else{c()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(D(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}de(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function w(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=w(e,Qe).trim();e.shift()}else{t=w(e,b)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{w(o,v);const l=o.length;const c=w(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};w(o,v);u.pollInterval=h(w(o,/[,\[\s]/));w(o,v);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const a={trigger:c};var i=nt(e,o,"event");if(i){a.eventFilter=i}w(o,v);while(o.length>0&&o[0]!==","){const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=h(w(o,b))}else if(f==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=w(o,b);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const d=rt(o);if(d.length>0){s+=" "+d}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=rt(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=h(w(o,b))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=w(o,b)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=rt(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=w(o,b)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}w(o,v)}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}w(o,v)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(d(e,"form")){return[{trigger:"submit"}]}else if(d(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(d(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!gt(n,e,Mt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function at(e){return g(e,Q.config.disableSelector)}function ft(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){pt(t,function(e,t){const n=ue(e);if(at(n)){a(n);return}he(r,o,n,t)},n,e,true)})}}function dt(e,t){const n=ue(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(d(n,'input[type="submit"], button')&&g(n,"form")!==null){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function ht(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function gt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function pt(l,c,e,u,a){const f=ie(l);let t;if(u.from){t=p(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in f)){f.lastValue=new WeakMap}t.forEach(function(e){if(!f.lastValue.has(u)){f.lastValue.set(u,new WeakMap)}f.lastValue.get(u).set(e,e.value)})}se(t,function(i){const s=function(e){if(!le(l)){i.removeEventListener(u.trigger,s);return}if(ht(l,e)){return}if(a||dt(e,l)){e.preventDefault()}if(gt(u,l,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!d(ue(e.target),u.target)){return}}if(u.once){if(f.triggeredOnce){return}else{f.triggeredOnce=true}}if(u.changed){const n=event.target;const r=n.value;const o=f.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(f.delayed){clearTimeout(f.delayed)}if(f.throttle){return}if(u.throttle>0){if(!f.throttle){de(l,"htmx:trigger");c(l,e);f.throttle=E().setTimeout(function(){f.throttle=null},u.throttle)}}else if(u.delay>0){f.delayed=E().setTimeout(function(){de(l,"htmx:trigger");c(l,e)},u.delay)}else{de(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let xt=null;function yt(){if(!xt){xt=function(){mt=true};window.addEventListener("scroll",xt);window.addEventListener("resize",xt);setInterval(function(){if(mt){mt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&X(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){de(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){de(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;se(o,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(g(n,Q.config.disableSelector)){a(n);return}he(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){yt();pt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ue(r),n,e)}else{pt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Tt(e){const t=g(ue(e.target),"button, input[type='submit']");const n=Lt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Lt(e);if(t){t.lastButtonClicked=null}}function Lt(e){const t=g(ue(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function Nt(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function At(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(at(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function It(t){ke(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Vt(t){if(!B()){return null}t=U(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){de(ne().body,"htmx:historyCacheMissLoad",i);const e=P(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=Ut();const r=xn(n);kn(e.title);qe(e);Ve(n,t,r);Te();Kt(r.tasks);Bt=o;de(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Wt(e){zt();e=e||location.pathname+location.search;const t=Vt(e);if(t){const n=P(t.content);const r=Ut();const o=xn(r);kn(t.title);qe(n);Ve(r,n,o);Te();Kt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Bt=e;de(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Gt(e)}}}function Zt(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Yt(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function Qt(e,t){se(e.concat(t),function(e){const t=ie(e);t.requestCount=(t.requestCount||1)-1});se(e,function(e){const t=ie(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function en(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function on(t,n,r,o,i){if(o==null||en(t,o)){return}else{t.push(o)}if(tn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=M(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=M(o.files)}nn(s,e,n);if(i){sn(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){rn(e.name,e.value,n)}else{t.push(e)}if(i){sn(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}nn(t,e,n)})}}function sn(e,t){const n=e;if(n.willValidate){de(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});de(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function ln(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){on(n,o,i,g(e,"form"),l)}on(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const a=ee(u,"name");nn(a,u.value,o)}const c=we(e,"hx-include");se(c,function(e){on(n,r,i,ue(e),l);if(!d(e,"form")){se(f(e).querySelectorAll(ot),function(e){on(n,r,i,e,l)})}});ln(r,o);return{errors:i,formData:r,values:Nn(r)}}function un(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function an(e){e=qn(e);let n="";e.forEach(function(e,t){n=un(n,t,e)});return n}function fn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};bn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function dn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.substr(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function hn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function gn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!hn(e)){r.show="top"}if(n){const s=F(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.substr(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const d=l.substr("focus-scroll:".length);r.focusScroll=d=="true"}else if(e==0){r.swapStyle=l}else{C("Unknown modifier in hx-swap: "+l)}}}}return r}function pn(e){return re(e,"hx-encoding")==="multipart/form-data"||d(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function mn(t,n,r){let o=null;Ft(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(pn(n)){return ln(new FormData,qn(r))}else{return an(r)}}}function xn(e){return{tasks:[],elts:[e]}}function yn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function bn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.substr(11);t=true}else if(e.indexOf("js:")===0){e=e.substr(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return bn(ue(c(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function wn(e,t){return bn(e,"hx-vars",true,t)}function Sn(e,t){return bn(e,"hx-vals",false,t)}function En(e){return ce(wn(e),Sn(e))}function Cn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function On(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return t.test(e.getAllResponseHeaders())}function Rn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return he(t,n,null,null,{targetOverride:y(r)||ve,returnPromise:true})}else{let e=y(r.target);if(r.target&&!e||!e&&!y(r.source)){e=ve}return he(t,n,y(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(t,n,null,null,{returnPromise:true})}}function Hn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Tn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return de(e,"htmx:validateUrl",ce({url:o,sameHost:r},n))}function qn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Ln(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function Nn(r){return new Proxy(r,{get:function(e,t){if(typeof t==="symbol"){return Reflect.get(e,t)}if(t==="toJSON"){return()=>Object.fromEntries(r)}if(t in e){if(typeof e[t]==="function"){return function(){return r[t].apply(r,arguments)}}else{return e[t]}}const n=r.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Ln(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Dn;const X=i.select||null;if(!le(r)){oe(s);return e}const c=i.targetOverride||ue(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let u=ie(r);const a=u.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const N=ee(a,"formmethod");if(N!=null){if(N.toLowerCase()!=="dialog"){t=N}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(de(r,"htmx:confirm",G)===false){oe(s);return e}}let d=r;let h=re(r,"hx-sync");let g=null;let F=false;if(h){const A=h.split(":");const I=A[0].trim();if(I==="this"){d=Se(r,"hx-sync")}else{d=ue(ae(r,I))}h=(A[1]||"drop").trim();u=ie(d);if(h==="drop"&&u.xhr&&u.abortable!==true){oe(s);return e}else if(h==="abort"){if(u.xhr){oe(s);return e}else{F=true}}else if(h==="replace"){de(d,"htmx:abort")}else if(h.indexOf("queue")===0){const W=h.split(" ");g=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){de(d,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(g==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="all"){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){he(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;u.xhr=p;u.abortable=F;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var x=prompt(B);if(x===null||!de(r,"htmx:prompt",{prompt:x,target:c})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let y=fn(r,c,x);if(t!=="get"&&!pn(r)){y["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){y=ce(y,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){ln(j,qn(i.values))}const V=qn(En(r));const v=ln(j,V);let w=dn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=bn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:Nn(w),unfilteredFormData:v,unfilteredParameters:Nn(v),headers:y,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!de(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;y=C.headers;w=qn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){de(r,"htmx:validation:halted",C);oe(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=an(w);if(O){R+="#"+O}}}if(!Tn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in y){if(y.hasOwnProperty(k)){const Y=y[k];Cn(p,k,Y)}}}const H={xhr:p,target:c,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Hn(r);H.pathInfo.responsePath=On(p);M(r,H);if(H.keepIndicators!==true){Qt(T,q)}de(r,"htmx:afterRequest",H);de(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){de(e,"htmx:afterRequest",H);de(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ce({error:e},H));throw e}};p.onerror=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!de(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Zt(r);var q=Yt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){de(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});de(r,"htmx:beforeSend",H);const J=E?null:mn(p,r,w);p.send(J);return e}function An(e,t){const n=t.xhr;let r=null;let o=null;if(O(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(O(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(O(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const c=re(e,"hx-replace-url");const u=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(c){a="replace";f=c}else if(u){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function In(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Pn(e){for(var t=0;t0){E().setTimeout(e,x.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ce({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Mn={};function Xn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Fn(e,t){if(t.init){t.init(n)}Mn[e]=ce(Xn(),t)}function Bn(e){delete Mn[e]}function Un(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Mn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Un(ue(c(e)),n,r)}var jn=false;ne().addEventListener("DOMContentLoaded",function(){jn=true});function Vn(e){if(jn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function _n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend"," ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function $n(){const e=zn();if(e){Q.config=ce(Q.config,e)}}Vn(function(){$n();_n();let e=ne().body;kt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Wt();se(t,function(e){de(e,"htmx:restored",{document:ne(),triggerEvent:de})})}else{if(n){n(e)}}};E().setTimeout(function(){de(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file diff --git a/static/svg/sheep.svg b/static/svg/sheep.svg new file mode 100644 index 0000000..fad5de0 --- /dev/null +++ b/static/svg/sheep.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..ade4a2e --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,69 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./**/*.templ"], + theme: { + extend: { + backgroundSize: { + "size-200": "200% 200%", + }, + backgroundPosition: { + "pos-0": "0% 0%", + "pos-100": "100% 100%", + }, + keyframes: { + sheepspin: { + '0%': { + transform: 'rotate(0deg)', + opacity: '0', + }, + '25%': { + transform: 'rotate(90deg)', + opacity: '1', + }, + '50%': { + transform: 'rotate(180deg)', + opacity: '1', + }, + '75%': { + transform: 'rotate(270deg)', + opacity: '1', + }, + '100%': { + transform: 'rotate(360deg)', + opacity: '0', + }, + }, + sheepmove: { + '0%': { + transform: 'translateX(-25%) translateY(-250%)', + }, + '10%': { + transform: 'translateX(-80%) translateY(-162.5%)', + }, + '30%': { + transform: 'translateX(-110%) translateY(-125%)', + }, + '50%': { + transform: 'translateX(-80%) translateY(-50%)', + }, + '60%': { + transform: 'translateX(-25%) translateY(0)' + }, + '75%': { + transform: 'translateX(-10%) translateY(25%)' + }, + '90%': { + transform: 'translateX(-15%) translateY(50%)' + }, + '100%': { + transform: 'translateX(-10%) translateY(75%)' + }, + }, + }, + animation: { + sheepspin: 'sheepspin 750ms linear', + sheepmove: 'sheepmove 750ms linear', + }, + }, + }, +} -- 2.44.1