diff --git a/.gitconfig.save b/.gitconfig.save new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/.gitconfig.save @@ -0,0 +1 @@ + diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..fddefcef1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @portals diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 000000000..1079073a9 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,30 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 19 + uses: actions/setup-java@v2 + with: + java-version: "19" + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - name: Testing + working-directory: ./backend + run: ./gradlew test + - name: Cleanup Gradle Cache + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + run: | + rm -f ~/.gradle/caches/modules-2/modules-2.lock + rm -f ~/.gradle/caches/modules-2/gc.properties diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..fd7f2aafb --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +yarn-error.log +frontend/node_modules/ +.idea/* +.DS-Store diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..498c2e178 --- /dev/null +++ b/.mailmap @@ -0,0 +1 @@ +Theodor Angergård \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..661bfe23f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: java + +jdk: + - oraclejdk11 + +before_script: + - cd backend + +script: + - ./gradlew build --console 'plain' -s + +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ + +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ + +notifications: + email: false diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..be3f7b28e --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index d04263a92..98e49b3e7 100644 --- a/README.md +++ b/README.md @@ -1 +1,37 @@ -# Gamma \ No newline at end of file +# Gamma + +[![Build Status](https://travis-ci.com/cthit/Gamma.svg?branch=develop)](https://travis-ci.com/cthit/Gamma) + +# Gamma is Chalmers IT section account system + +Gamma is licensed under the GNU AGPL, see `LICENSE`. + +--- + +## More information about Gamma can be found on the [Wiki](https://github.com/cthit/Gamma/wiki) + +## Build setup + +### For production + +it's real easy. Just replace the environment variables with suitable value (see wiki) +and run: + +`docker-compose -f prod.docker-compose.yml up --build` + +Depending on your build system, things might be different, and a proxy is probably needed for a real production version of Gamma. + +### Development + +run + +`docker-compose up --build` to build the frontend, backend, database, databasemonitoring, and all microservices that's needed for Gamma. + +If developing on the backend, we recomend not running the backend in the docker-compose file. There is a docker-compose file that sets up all microservice but the backend, to use this run: `docker-compose -f no_backend.docker-compose.ym up --build` +then you will need to start the server, this is done by running the Java code in the backend, and is probably best done through an IDE. + +You'll need to run `docker-compose down` / `docker-compose -f no_backed.docker-compose.yml down` if you want to try the production build. Same if you're going from production to development. + +## API Documentation + +The API documentation is auto-generated by [Swagger](https://swagger.io/) and can be found under http://localhost:8081/api/swagger-ui.html while the application is running. diff --git a/backend/.dcignore b/backend/.dcignore new file mode 100644 index 000000000..e69de29bb diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 000000000..0cdbb81ed --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,33 @@ +.gradle/ +/build/ +!gradle/wrapper/gradle-wrapper.jar +/uploads/ + +/src/main/resources/secrets.properties + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +/out/ +.idea/ + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +secrets.properties diff --git a/backend/.jpb/jpb-settings.xml b/backend/.jpb/jpb-settings.xml new file mode 100644 index 000000000..935cf0d5a --- /dev/null +++ b/backend/.jpb/jpb-settings.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/backend/.run/GammaApplication.run.xml b/backend/.run/GammaApplication.run.xml new file mode 100644 index 000000000..b9fb53e2d --- /dev/null +++ b/backend/.run/GammaApplication.run.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/backend/.run/Tests in 'gamma.test'.run.xml b/backend/.run/Tests in 'gamma.test'.run.xml new file mode 100644 index 000000000..b6d0eed2d --- /dev/null +++ b/backend/.run/Tests in 'gamma.test'.run.xml @@ -0,0 +1,23 @@ + + + + + + + false + true + false + + + \ No newline at end of file diff --git a/backend/build.gradle b/backend/build.gradle new file mode 100644 index 000000000..bbeb3228a --- /dev/null +++ b/backend/build.gradle @@ -0,0 +1,130 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:3.0.1" + } +} + +plugins { + id "java" + id "eclipse" + id "application" + id "checkstyle" + id "pmd" + id "nebula.lint" version "16.7.0" +} + +apply plugin: "org.springframework.boot" +apply plugin: "io.spring.dependency-management" +group = "it.chalmers" +version = "1.0.0-SNAPSHOT" +sourceCompatibility = 19 +mainClassName = "it.chalmers.gamma.GammaApplication" + +ext { + springBootVersion = "3.0.4" + springSecurityVersion = "6.0.2" +} + +repositories { + mavenCentral() +} + +dependencies { + annotationProcessor( + // Used to generate Record Builder classes + "io.soabase.record-builder:record-builder-processor:35", + + // Used to generate useful configuration metadata for IDEs + "org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}", + ) + + implementation( + // Used to generate Record Builder classes + "io.soabase.record-builder:record-builder-core:35", + + // Used to handle SQL specific errors + "org.postgresql:postgresql:42.6.0", + + // Spring Boot + "org.springframework.boot:spring-boot:${springBootVersion}", + "org.springframework.boot:spring-boot-autoconfigure:${springBootVersion}", + "org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}", + "org.springframework.boot:spring-boot-starter-security:${springBootVersion}", + "org.springframework.boot:spring-boot-starter-validation:${springBootVersion}", + "org.springframework.boot:spring-boot-starter-web:${springBootVersion}", + + // OAuth2 Server + "org.springframework.security:spring-security-oauth2-authorization-server:1.0.1", + + // Spring session + "org.springframework.session:spring-session-core:3.0.0", + + // Jackson (JSON) + 'com.fasterxml.jackson.core:jackson-core:2.14.2', + + // Spring Data + "org.springframework.data:spring-data-jpa:3.0.3", + 'org.springframework.data:spring-data-redis:3.0.6' + ) + + runtimeOnly( + // FLywayDB (Database migration) + "org.flywaydb:flyway-core:9.16.0", + + // Spring Boot + "org.springframework.boot:spring-boot-devtools:${springBootVersion}", + "org.springframework.boot:spring-boot-starter-data-redis:${springBootVersion}", + "org.springframework.boot:spring-boot-starter-thymeleaf:${springBootVersion}", + ) + + testImplementation( + "com.tngtech.archunit:archunit:1.0.0", + "io.rest-assured:json-path:5.3.0", + "io.rest-assured:rest-assured:5.3.0", + "io.rest-assured:xml-path:5.3.0", + "org.flywaydb:flyway-core:9.16.0", + "org.mockito:mockito-inline:5.2.0", + "org.springframework.boot:spring-boot-starter-test:${springBootVersion}", + "org.springframework.security:spring-security-test:6.0.2", + "org.testcontainers:junit-jupiter:1.17.6", + "org.testcontainers:postgresql:1.17.6", + ) +} + +dependencyManagement { + imports { + mavenBom "org.testcontainers:testcontainers-bom:1.16.3" + } +} + +test { + useJUnitPlatform() +} + +repositories { + mavenCentral() +} + +checkstyle { + toolVersion = "8.11" + ignoreFailures = false + maxWarnings = 0 + configFile = project(":").file("config/checkstyle/checkstyle.xml") + configProperties = ["suppressionFile": project(":").file("config/checkstyle/suppressions.xml")] +} + +pmd { + toolVersion = "6.21.0" + consoleOutput = false + ignoreFailures = false + ruleSets = [] + ruleSetConfig = resources.text.fromFile("./config/pmd/ruleset.xml") +} + +// Is not perfect, but gives you an idea of what needs to be changed +//gradleLint { +// rules = ["dependency-parentheses", "archaic-wrapper"] +//} diff --git a/backend/config/checkstyle/checkstyle.xml b/backend/config/checkstyle/checkstyle.xml new file mode 100644 index 000000000..3f09dd734 --- /dev/null +++ b/backend/config/checkstyle/checkstyle.xml @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/config/checkstyle/suppressions.xml b/backend/config/checkstyle/suppressions.xml new file mode 100644 index 000000000..9b5539341 --- /dev/null +++ b/backend/config/checkstyle/suppressions.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/backend/config/pmd/ruleset.xml b/backend/config/pmd/ruleset.xml new file mode 100755 index 000000000..7672c612f --- /dev/null +++ b/backend/config/pmd/ruleset.xml @@ -0,0 +1,90 @@ + + + + + + PMD rule set for Gamma. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/dev.Dockerfile b/backend/dev.Dockerfile new file mode 100644 index 000000000..431553878 --- /dev/null +++ b/backend/dev.Dockerfile @@ -0,0 +1,21 @@ +FROM gradle:6.3.0-jdk11 +#Gradle docker has gradle user as default +USER root + + +WORKDIR /app + +#db is used to separate development and production databases + +# Environment variables needed for Gamma to run in Production env, These must be changed!!. +COPY . /app + +RUN mkdir -p /app +RUN chown -R gradle /app + +USER gradle + +RUN gradle :build -x test -x pmdMain -x checkstyleMain -x pmdTest +# This probably should not have a static path, but instead build in a custom path. +CMD sleep 5 && java -jar -Dspring.profiles.active=development build/libs/gamma-0.9.0-SNAPSHOT.jar + diff --git a/backend/dockerfile b/backend/dockerfile new file mode 100644 index 000000000..808e03333 --- /dev/null +++ b/backend/dockerfile @@ -0,0 +1,23 @@ +FROM gradle:7.6.0-jdk19-alpine +#Gradle docker has gradle user as default +USER root + + +WORKDIR /app + +#db is used to separate development and production databases + +# Environment variables needed for Gamma to run in Production env, These must be changed!!. +COPY . /app + +ENV LOGGING_FILE log/production.log + +RUN mkdir -p /app +RUN chown -R gradle /app + +USER gradle + +RUN gradle :build -x test -x pmdMain -x checkstyleMain -x pmdTest +# This probably should not have a static path, but instead build in a custom path. +CMD sleep 5 && java -jar -Dspring.profiles.active=production build/libs/gamma-1.0.0-SNAPSHOT.jar + diff --git a/backend/gradle/wrapper/gradle-wrapper.jar b/backend/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..41d9927a4 Binary files /dev/null and b/backend/gradle/wrapper/gradle-wrapper.jar differ diff --git a/backend/gradle/wrapper/gradle-wrapper.properties b/backend/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..070cb702f --- /dev/null +++ b/backend/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/backend/gradlew b/backend/gradlew new file mode 100755 index 000000000..1b6c78733 --- /dev/null +++ b/backend/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/backend/gradlew.bat b/backend/gradlew.bat new file mode 100644 index 000000000..ac1b06f93 --- /dev/null +++ b/backend/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend/settings.gradle b/backend/settings.gradle new file mode 100644 index 000000000..a6a599a2f --- /dev/null +++ b/backend/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'gamma' diff --git a/backend/src/main/java/it/chalmers/gamma/BootstrapRunner.java b/backend/src/main/java/it/chalmers/gamma/BootstrapRunner.java new file mode 100644 index 000000000..e9075aa8e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/BootstrapRunner.java @@ -0,0 +1,50 @@ +package it.chalmers.gamma; + + +import it.chalmers.gamma.adapter.secondary.redis.oauth2.AuthorizationRedisRepository; +import it.chalmers.gamma.bootstrap.*; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class BootstrapRunner { + + @Bean + public CommandLineRunner runBootStrap(ApiKeyBootstrap apiKeyBootstrap, + ClientBootstrap clientBootstrap, + EnsureAnAdminUserBootstrap ensureAnAdminUserBootstrap, + EnsureSettingsBootstrap ensureSettingsBootstrap, + GroupBootstrap groupBootstrap, + MiscBootstrap miscBootstrap, + PostBootstrap postBootstrap, + SuperGroupBootstrap superGroupBootstrap, + UserBootstrap userBootstrap) { + return (args) -> { + try { + SecurityContextHolder.createEmptyContext(); + SecurityContextHolder.getContext().setAuthentication(new BootstrapAuthenticated()); + + miscBootstrap.runImageBootstrap(); + + ensureSettingsBootstrap.ensureAppSettings(); + + ensureAnAdminUserBootstrap.ensureAnAdminUser(); + + userBootstrap.createUsers(); + postBootstrap.createPosts(); + superGroupBootstrap.createSuperGroups(); + groupBootstrap.createGroups(); + + clientBootstrap.runOauthClient(); + apiKeyBootstrap.ensureApiKeys(); + + SecurityContextHolder.clearContext(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + }; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/GammaApplication.java b/backend/src/main/java/it/chalmers/gamma/GammaApplication.java new file mode 100644 index 000000000..b9fc154e7 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/GammaApplication.java @@ -0,0 +1,18 @@ +package it.chalmers.gamma; + +import it.chalmers.gamma.util.controller.ErrorHandlingControllerAdvice; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.context.annotation.Import; + +@SpringBootApplication +@ConfigurationPropertiesScan +@Import(ErrorHandlingControllerAdvice.class) +public class GammaApplication { + + public static void main(String[] args) { + SpringApplication.run(GammaApplication.class, args); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/external/allowlist/AllowListV1ApiController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/external/allowlist/AllowListV1ApiController.java new file mode 100644 index 000000000..4fdcc195d --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/external/allowlist/AllowListV1ApiController.java @@ -0,0 +1,60 @@ +package it.chalmers.gamma.adapter.primary.external.allowlist; + +import it.chalmers.gamma.app.user.allowlist.AllowListFacade; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + +@RestController +@RequestMapping(AllowListV1ApiController.URI) +public class AllowListV1ApiController { + + public static final String URI = "/external/allowlist/v1"; + + private static final Logger LOGGER = LoggerFactory.getLogger(AllowListV1ApiController.class); + + private final AllowListFacade allowListFacade; + + public AllowListV1ApiController(AllowListFacade allowListFacade) { + this.allowListFacade = allowListFacade; + } + + @GetMapping() + public List getAllowList() { + return this.allowListFacade.getAllowList(); + } + + @PostMapping() + public ResponseEntity addAllowedUsers(@RequestBody AddListOfAllowListRequest request) { + List failedToAdd = new ArrayList<>(); + + for (String cid : request.cids) { + try { + this.allowListFacade.allow(cid); + LOGGER.info("Added user " + cid + " to allow list"); + } catch (Exception e) { + LOGGER.info("Failed to add " + cid + " to allow list"); + failedToAdd.add(cid); + } + } + + if (!failedToAdd.isEmpty()) { + return new ResponseEntity<>(failedToAdd, HttpStatus.PARTIAL_CONTENT); + } + + return new AllowListAddedResponse(); + } + + private record AddListOfAllowListRequest(List cids) { + } + + private static class AllowListAddedResponse extends SuccessResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/external/client/ClientApiV1Controller.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/external/client/ClientApiV1Controller.java new file mode 100644 index 000000000..7e7d6b72b --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/external/client/ClientApiV1Controller.java @@ -0,0 +1,64 @@ +package it.chalmers.gamma.adapter.primary.external.client; + +import it.chalmers.gamma.app.group.GroupFacade; +import it.chalmers.gamma.app.supergroup.SuperGroupFacade; +import it.chalmers.gamma.app.user.MeFacade; +import it.chalmers.gamma.app.user.UserFacade; +import it.chalmers.gamma.util.response.NotFoundResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.UUID; + +/** + * If you need changes, then create a new version of the API. + */ +@RestController +@RequestMapping(ClientApiV1Controller.URI) +public class ClientApiV1Controller { + + public static final String URI = "/external/client/v1"; + + private final UserFacade userFacade; + private final GroupFacade groupFacade; + private final SuperGroupFacade superGroupFacade; + private final MeFacade meFacade; + + public ClientApiV1Controller(UserFacade userFacade, + GroupFacade groupFacade, + SuperGroupFacade superGroupFacade, + MeFacade meFacade) { + this.userFacade = userFacade; + this.groupFacade = groupFacade; + this.superGroupFacade = superGroupFacade; + this.meFacade = meFacade; + } + + @GetMapping("/groups") + public List getGroups() { + return this.groupFacade.getAll(); + } + + @GetMapping("/superGroups") + public List getSuperGroups() { + return this.superGroupFacade.getAll(); + } + + @GetMapping("/users") + public List getUsersForClient() { + return this.userFacade.getAllByClientAccepting(); + } + + @GetMapping("/users/{id}") + public UserFacade.UserWithGroupsDTO getUser(@PathVariable("id") UUID id) { + return this.userFacade.get(id) + .orElseThrow(UserNotFoundResponse::new); + } + + private static class UserNotFoundResponse extends NotFoundResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/external/goldapps/GoldappsV1ApiController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/external/goldapps/GoldappsV1ApiController.java new file mode 100644 index 000000000..9293a08c7 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/external/goldapps/GoldappsV1ApiController.java @@ -0,0 +1,37 @@ +package it.chalmers.gamma.adapter.primary.external.goldapps; + +import it.chalmers.gamma.app.goldapps.GoldappsFacade; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * Used by cthit/goldapps to sync Google accounts for the student division. + * If you need changes, then create a new version of the API. + */ +@RestController +@RequestMapping(GoldappsV1ApiController.URI) +public class GoldappsV1ApiController { + + public static final String URI = "/external/goldapps/v1"; + + private final GoldappsFacade goldappsFacade; + + public GoldappsV1ApiController(GoldappsFacade goldappsFacade) { + this.goldappsFacade = goldappsFacade; + } + + @GetMapping("/supergroups") + public List getSuperGroups(@RequestParam String types) { + return this.goldappsFacade.getActiveSuperGroups(List.of(types.split(";"))); + } + + @GetMapping("/users") + public List getUsers(@RequestParam String types) { + return this.goldappsFacade.getActiveUsers(List.of(types.split(";"))); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/external/info/InfoV1ApiController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/external/info/InfoV1ApiController.java new file mode 100644 index 000000000..3fc1f7f24 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/external/info/InfoV1ApiController.java @@ -0,0 +1,50 @@ +package it.chalmers.gamma.adapter.primary.external.info; + +import it.chalmers.gamma.app.group.GroupFacade; +import it.chalmers.gamma.app.user.UserFacade; +import it.chalmers.gamma.util.response.NotFoundResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.UUID; + +/** + * Going to be used by chalmers.it to display groups and their members. + * A separate API since user info are going to be returned, and that should be used with caution. + * If you need changes, then create a new version of the API. + */ +@RestController +@RequestMapping(InfoV1ApiController.URI) +public class InfoV1ApiController { + + public static final String URI = "/external/info/v1"; + + private final GroupFacade groupFacade; + private final UserFacade userFacade; + + public InfoV1ApiController(GroupFacade groupFacade, UserFacade userFacade) { + this.groupFacade = groupFacade; + this.userFacade = userFacade; + } + + @GetMapping("/users/{id}") + public UserFacade.UserWithGroupsDTO getUser(@PathVariable("id") UUID id) { + return this.userFacade.get(id) + .orElseThrow(UserNotFoundResponse::new); + } + + @GetMapping("/groups") + public GroupsResponse getGroups() { + return new GroupsResponse(this.groupFacade.getAllForInfoApi()); + } + + record GroupsResponse(List groups) { + } + + private static class UserNotFoundResponse extends NotFoundResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/images/ImagesController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/images/ImagesController.java new file mode 100644 index 000000000..6fc9bb5ba --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/images/ImagesController.java @@ -0,0 +1,67 @@ +package it.chalmers.gamma.adapter.primary.images; + +import it.chalmers.gamma.app.image.ImageFacade; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.UUID; + +@RestController +@RequestMapping("/images") +public class ImagesController { + + private final ImageFacade imageFacade; + + public ImagesController(ImageFacade imageFacade) { + this.imageFacade = imageFacade; + } + + @GetMapping("/user/avatar/{id}") + public ResponseEntity getUserAvatar(@PathVariable("id") UUID id) { + ImageFacade.ImageDetails imageDetails = this.imageFacade.getAvatar(id); + String type = imageDetails.imageType(); + return ResponseEntity + .ok() + .contentType((type.equals("jpg") || type.equals("jpeg") ? MediaType.IMAGE_JPEG + : type.equals("png") ? MediaType.IMAGE_PNG + : MediaType.IMAGE_GIF) + ) + .body(imageDetails.data()); + } + + @GetMapping("/group/avatar/{id}") + public ResponseEntity getGroupAvatar(@PathVariable("id") UUID id) { + ImageFacade.ImageDetails imageDetails = this.imageFacade.getGroupAvatar(id); + String type = imageDetails.imageType(); + return ResponseEntity + .ok() + .contentType((type.equals("jpg") || type.equals("jpeg") ? MediaType.IMAGE_JPEG + : type.equals("png") ? MediaType.IMAGE_PNG + : MediaType.IMAGE_GIF) + ) + .body(imageDetails.data()); + } + + @GetMapping("/group/banner/{id}") + public ResponseEntity getGroupBanner(@PathVariable("id") UUID id) { + ImageFacade.ImageDetails imageDetails = this.imageFacade.getGroupBanner(id); + String type = imageDetails.imageType(); + return ResponseEntity + .ok() + .contentType( + (type.equals("jpg") || type.equals("jpeg") + ? MediaType.IMAGE_JPEG + : type.equals("png") + ? MediaType.IMAGE_PNG + : MediaType.IMAGE_GIF) + ) + .body(imageDetails.data()); + } + + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AllowListAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AllowListAdminController.java new file mode 100644 index 000000000..be9518055 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AllowListAdminController.java @@ -0,0 +1,74 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.user.allowlist.AllowListFacade; +import it.chalmers.gamma.app.user.allowlist.AllowListRepository; +import it.chalmers.gamma.util.response.ErrorResponse; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/internal/admin/users/allow-list") +public final class AllowListAdminController { + + private static final Logger LOGGER = LoggerFactory.getLogger(AllowListAdminController.class); + + private final AllowListFacade allowListFacade; + + public AllowListAdminController(AllowListFacade allowListFacade) { + this.allowListFacade = allowListFacade; + } + + @GetMapping() + public List getAllowList() { + return this.allowListFacade.getAllowList(); + } + + @PostMapping() + public AllowAddedResponse addAllowedUser(@RequestBody AddToAllowList request) { + try { + this.allowListFacade.allow(request.cid); + } catch (AllowListRepository.AlreadyAllowedException e) { + throw new CidAlreadyAllowedResponse(); + } + + return new AllowAddedResponse(); + } + + @DeleteMapping("/{cid}") + public CidRemovedFromAllowListResponse removeFromAllowList(@PathVariable("cid") String cid) { + try { + this.allowListFacade.removeFromAllowList(cid); + } catch (AllowListRepository.NotOnAllowListException e) { + throw new CidNotAllowedResponse(); + } + return new CidRemovedFromAllowListResponse(); + } + + private record AddToAllowList(String cid) { + } + + private static class CidRemovedFromAllowListResponse extends SuccessResponse { + } + + private static class AllowAddedResponse extends SuccessResponse { + } + + private static class CidNotAllowedResponse extends NotFoundResponse { + } + + private static class CidAlreadyAllowedResponse extends ErrorResponse { + private CidAlreadyAllowedResponse() { + super(HttpStatus.UNPROCESSABLE_ENTITY); + } + } + + private static class CidIsAllowListResponse extends SuccessResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AllowListController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AllowListController.java new file mode 100644 index 000000000..af6eddc5f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AllowListController.java @@ -0,0 +1,38 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.user.UserCreationFacade; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(value = "/internal/allow-list") +public final class AllowListController { + + private final UserCreationFacade userCreationFacade; + + public AllowListController(UserCreationFacade userCreationFacade) { + this.userCreationFacade = userCreationFacade; + } + + @PostMapping("/activate-cid") + public AllowListedCidActivatedResponse createActivationCode(@RequestBody AllowCodeRequest request) { + try { + this.userCreationFacade.tryToActivateUser(request.cid); + } finally { + //Gamma doesn't differentiate if activation of a cid was successful or not. + return new AllowListedCidActivatedResponse(); + } + } + + private record AllowCodeRequest(String cid) { + } + + // This will be thrown even if there was an error for security reasons. + private static class AllowListedCidActivatedResponse extends SuccessResponse { + } + +} + diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/ApiKeyAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/ApiKeyAdminController.java new file mode 100644 index 000000000..2e58a3666 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/ApiKeyAdminController.java @@ -0,0 +1,86 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.apikey.ApiKeyFacade; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/internal/admin/api-keys") +public final class ApiKeyAdminController { + + private final ApiKeyFacade apiKeyFacade; + + public ApiKeyAdminController(ApiKeyFacade apiKeyFacade) { + this.apiKeyFacade = apiKeyFacade; + } + + @PostMapping() + public String createApiKey(@RequestBody CreateApiKeyRequest request) { + return this.apiKeyFacade.create( + new ApiKeyFacade.NewApiKey( + request.prettyName, + request.svDescription, + request.enDescription, + request.keyType + ) + ); + } + + @PostMapping("/{id}/reset") + public String resetApiKey(@PathVariable("id") UUID id) { + String token; + + try { + token = this.apiKeyFacade.resetApiKeyToken(id); + } catch (ApiKeyFacade.ApiKeyNotFoundException e) { + throw new ApiKeyNotFoundResponse(); + } + + return token; + } + + @GetMapping() + public List getAllApiKeys() { + return this.apiKeyFacade.getAll(); + } + + @GetMapping("/types") + public String[] getTypes() { + return this.apiKeyFacade.getApiKeyTypes(); + } + + @GetMapping("/{id}") + public ApiKeyFacade.ApiKeyDTO getApiKey(@PathVariable("id") String id) { + return this.apiKeyFacade.getById(UUID.fromString(id)) + .orElseThrow(ApiKeyNotFoundResponse::new); + } + + @DeleteMapping("/{id}") + public ApiKeyDeletedResponse deleteApiKey(@PathVariable("id") UUID apiKeyId) { + try { + this.apiKeyFacade.delete(apiKeyId); + return new ApiKeyDeletedResponse(); + } catch (ApiKeyFacade.ApiKeyNotFoundException e) { + throw new ApiKeyNotFoundResponse(); + } + } + + private record CreateApiKeyRequest( + String prettyName, + String svDescription, + String enDescription, + String keyType // goldapps, info, allowlist + ) { + } + + private static class ApiKeyDeletedResponse extends SuccessResponse { + } + + private static class ApiKeyNotFoundResponse extends NotFoundResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AuthorityPostAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AuthorityPostAdminController.java new file mode 100644 index 000000000..f97f50859 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AuthorityPostAdminController.java @@ -0,0 +1,72 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.client.domain.ClientAuthorityFacade; +import it.chalmers.gamma.util.response.AlreadyExistsResponse; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/internal/admin/client/authority/post") +public final class AuthorityPostAdminController { + + private final ClientAuthorityFacade clientAuthorityFacade; + + public AuthorityPostAdminController(ClientAuthorityFacade clientAuthorityFacade) { + this.clientAuthorityFacade = clientAuthorityFacade; + } + + @PostMapping + public AuthorityPostCreatedResponse addAuthority(@RequestBody CreateAuthorityPostRequest request) { + try { + this.clientAuthorityFacade.addSuperGroupPostToClientAuthority( + request.clientUid, + request.authorityName, + request.superGroupId, + request.postId + ); + } catch (ClientAuthorityFacade.ClientAuthorityNotFoundException e) { + throw new AuthorityNotFoundResponse(); + } catch (ClientAuthorityFacade.SuperGroupNotFoundException | ClientAuthorityFacade.PostNotFoundException e) { + throw new AuthorityPostNotFoundResponse(); + } + return new AuthorityPostCreatedResponse(); + } + + @DeleteMapping + public AuthorityPostRemovedResponse removeAuthority( + @RequestParam("clientUid") UUID clientUid, + @RequestParam("superGroupId") UUID superGroupId, + @RequestParam("postId") UUID postId, + @RequestParam("authorityName") String authorityName) { + try { + this.clientAuthorityFacade.removeSuperGroupPostFromClientAuthority(clientUid, authorityName, superGroupId, postId); + } catch (ClientAuthorityFacade.ClientAuthorityNotFoundException e) { + throw new AuthorityPostNotFoundResponse(); + } + return new AuthorityPostRemovedResponse(); + } + + private record CreateAuthorityPostRequest(UUID clientUid, UUID postId, UUID superGroupId, + String authorityName) { + } + + private static class AuthorityNotFoundResponse extends NotFoundResponse { + } + + private static class AuthorityPostRemovedResponse extends SuccessResponse { + } + + private static class AuthorityPostCreatedResponse extends SuccessResponse { + } + + private static class AuthorityPostNotFoundResponse extends NotFoundResponse { + } + + private static class AuthorityPostAlreadyExistsResponse extends AlreadyExistsResponse { + } + +} + diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AuthoritySuperGroupAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AuthoritySuperGroupAdminController.java new file mode 100644 index 000000000..2c79a6947 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AuthoritySuperGroupAdminController.java @@ -0,0 +1,73 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.client.domain.ClientAuthorityFacade; +import it.chalmers.gamma.util.response.AlreadyExistsResponse; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/internal/admin/client/authority/super-group") +public final class AuthoritySuperGroupAdminController { + + private final ClientAuthorityFacade clientAuthorityFacade; + + public AuthoritySuperGroupAdminController(ClientAuthorityFacade clientAuthorityFacade) { + this.clientAuthorityFacade = clientAuthorityFacade; + } + + @PostMapping + public AuthoritySuperGroupCreatedResponse addAuthority(@RequestBody CreateAuthoritySuperGroupRequest request) { + try { + this.clientAuthorityFacade.addSuperGroupToClientAuthority( + request.clientUid, + request.authorityName, + request.superGroupId + ); + } catch (ClientAuthorityFacade.ClientAuthorityNotFoundException e) { + throw new ClientAuthorityNotFoundResponse(); + } catch (ClientAuthorityFacade.SuperGroupNotFoundException e) { + e.printStackTrace(); + } + return new AuthoritySuperGroupCreatedResponse(); + } + + @DeleteMapping + public AuthoritySuperGroupRemovedResponse removeAuthority( + @RequestParam("clientUid") UUID clientUid, + @RequestParam("superGroupId") UUID superGroupId, + @RequestParam("authorityName") String authorityName) { + try { + this.clientAuthorityFacade.removeSuperGroupFromClientAuthority( + clientUid, + authorityName, + superGroupId + ); + } catch (ClientAuthorityFacade.ClientAuthorityNotFoundException e) { + throw new ClientAuthorityNotFoundResponse(); + } + return new AuthoritySuperGroupRemovedResponse(); + } + + private record CreateAuthoritySuperGroupRequest(UUID clientUid, UUID superGroupId, String authorityName) { + } + + private static class AuthoritySuperGroupRemovedResponse extends SuccessResponse { + } + + private static class AuthoritySuperGroupCreatedResponse extends SuccessResponse { + } + + private static class ClientAuthorityNotFoundResponse extends NotFoundResponse { + } + + private static class AuthoritySuperGroupNotFoundResponse extends NotFoundResponse { + } + + private static class AuthoritySuperGroupAlreadyExistsResponse extends AlreadyExistsResponse { + } + + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AuthorityUserAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AuthorityUserAdminController.java new file mode 100644 index 000000000..1b35d3f00 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/AuthorityUserAdminController.java @@ -0,0 +1,72 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.client.domain.ClientAuthorityFacade; +import it.chalmers.gamma.util.response.AlreadyExistsResponse; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/internal/admin/client/authority/user") +public final class AuthorityUserAdminController { + + private final ClientAuthorityFacade clientAuthorityFacade; + + public AuthorityUserAdminController(ClientAuthorityFacade clientAuthorityFacade) { + this.clientAuthorityFacade = clientAuthorityFacade; + } + + @PostMapping + public ClientAuthorityUserCreatedResponse addAuthority(@RequestBody CreateClientAuthorityUserRequest request) { + try { + this.clientAuthorityFacade.addUserToClientAuthority( + request.clientUid, + request.authorityName, + request.userId + ); + } catch (ClientAuthorityFacade.ClientAuthorityNotFoundException e) { + throw new ClientAuthorityNotFoundResponse(); + } catch (ClientAuthorityFacade.UserNotFoundException e) { + throw new ClientAuthorityUserNotFoundResponse(); + } + return new ClientAuthorityUserCreatedResponse(); + } + + @DeleteMapping + public ClientAuthorityUserRemovedResponse removeAuthority( + @RequestParam("clientUid") UUID clientUid, + @RequestParam("userId") UUID userId, + @RequestParam("authorityName") String authorityName) { + try { + this.clientAuthorityFacade.removeUserFromClientAuthority( + clientUid, + authorityName, + userId + ); + } catch (ClientAuthorityFacade.ClientAuthorityNotFoundException e) { + throw new ClientAuthorityUserNotFoundResponse(); + } + return new ClientAuthorityUserRemovedResponse(); + } + + private record CreateClientAuthorityUserRequest(UUID clientUid, UUID userId, String authorityName) { + } + + private static class ClientAuthorityUserRemovedResponse extends SuccessResponse { + } + + private static class ClientAuthorityUserCreatedResponse extends SuccessResponse { + } + + private static class ClientAuthorityUserNotFoundResponse extends NotFoundResponse { + } + + private static class ClientAuthorityNotFoundResponse extends NotFoundResponse { + } + + private static class AuthorityUserAlreadyExistsResponse extends AlreadyExistsResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/ClientAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/ClientAdminController.java new file mode 100644 index 000000000..263588736 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/ClientAdminController.java @@ -0,0 +1,89 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.client.ClientFacade; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/internal/admin/client") +public final class ClientAdminController { + + private final ClientFacade clientFacade; + + public ClientAdminController(ClientFacade clientFacade) { + this.clientFacade = clientFacade; + } + + @PostMapping() + public ClientFacade.ClientAndApiKeySecrets addClient(@RequestBody CreateClientRequest request) { + return this.clientFacade.create( + new ClientFacade.NewClient( + request.webServerRedirectUrl, + request.prettyName, + request.svDescription, + request.enDescription, + request.generateApiKey, + request.emailScope, + request.restriction != null ? new ClientFacade.NewClientRestrictions( + request.restriction.superGroupIds + ) : null + ) + ); + } + + @PostMapping("/{clientUid}/reset") + public String resetClientCredentials(@PathVariable("clientUid") String clientUid) { + String clientSecret; + + try { + clientSecret = this.clientFacade.resetClientSecret(clientUid); + } catch (ClientFacade.ClientNotFoundException e) { + throw new ClientNotFoundResponse(); + } + + return clientSecret; + } + + @GetMapping() + public List getClients() { + return this.clientFacade.getAll(); + } + + @GetMapping("/{clientUid}") + public ClientFacade.ClientDTO getClient(@PathVariable("clientUid") String clientUid) { + return this.clientFacade.get(clientUid).orElseThrow(ClientNotFoundResponse::new); + } + + @DeleteMapping("/{clientUid}") + public ClientDeletedResponse deleteClient(@PathVariable("clientUid") String clientUid) { + try { + this.clientFacade.delete(clientUid); + return new ClientDeletedResponse(); + } catch (ClientFacade.ClientNotFoundException e) { + throw new ClientNotFoundResponse(); + } + } + + private record CreateClientRestriction(List superGroupIds) { } + + private record CreateClientRequest(String webServerRedirectUrl, + String prettyName, + String svDescription, + String enDescription, + boolean generateApiKey, + boolean emailScope, + CreateClientRestriction restriction + ) { + } + + private static class ClientDeletedResponse extends SuccessResponse { + } + + public static class ClientNotFoundResponse extends NotFoundResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/ClientAuthorityAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/ClientAuthorityAdminController.java new file mode 100644 index 000000000..8b91ec7db --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/ClientAuthorityAdminController.java @@ -0,0 +1,63 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.client.domain.ClientAuthorityFacade; +import it.chalmers.gamma.app.client.domain.authority.ClientAuthorityRepository; +import it.chalmers.gamma.util.response.AlreadyExistsResponse; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/internal/admin/client") +public final class ClientAuthorityAdminController { + + private final ClientAuthorityFacade clientAuthorityFacade; + + public ClientAuthorityAdminController(ClientAuthorityFacade clientAuthorityFacade) { + this.clientAuthorityFacade = clientAuthorityFacade; + } + + @PostMapping("/authority") + public ClientAuthorityCreatedResponse addClientAuthority(@RequestBody CreateClientAuthorityRequest request) { + try { + this.clientAuthorityFacade.create(request.clientUid, request.authorityName); + } catch (ClientAuthorityRepository.ClientAuthorityAlreadyExistsException e) { + throw new ClientAuthorityAlreadyExistsResponse(); + } + return new ClientAuthorityCreatedResponse(); + } + + @GetMapping("/{clientUid}/authority") + public List getAllClientAuthorities(@PathVariable("clientUid") UUID clientUid) { + return this.clientAuthorityFacade.getAll(clientUid); + } + + @DeleteMapping("/{clientUid}/authority/{authority}") + public ClientAuthorityDeletedResponse deleteClientAuthority(@PathVariable("clientUid") UUID clientUid, @PathVariable("authority") String authority) { + try { + this.clientAuthorityFacade.delete(clientUid, authority); + } catch (ClientAuthorityFacade.ClientAuthorityNotFoundException e) { + throw new ClientAuthorityAlreadyExistsResponse(); + } + return new ClientAuthorityDeletedResponse(); + } + + private record CreateClientAuthorityRequest(UUID clientUid, String authorityName) { + } + + private static class ClientAuthorityDeletedResponse extends SuccessResponse { + } + + private static class ClientAuthorityCreatedResponse extends SuccessResponse { + } + + private static class ClientAuthorityNotFoundResponse extends NotFoundResponse { + } + + private static class ClientAuthorityAlreadyExistsResponse extends AlreadyExistsResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/EditGroupImagesController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/EditGroupImagesController.java new file mode 100644 index 000000000..d40cabd38 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/EditGroupImagesController.java @@ -0,0 +1,74 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.adapter.secondary.image.ImageFile; +import it.chalmers.gamma.app.image.ImageFacade; +import it.chalmers.gamma.app.image.domain.ImageService; +import it.chalmers.gamma.util.response.ErrorResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.UUID; + +@RestController +@RequestMapping("/internal/groups") +public final class EditGroupImagesController { + + private final ImageFacade imageFacade; + + public EditGroupImagesController(ImageFacade imageFacade) { + this.imageFacade = imageFacade; + } + + @PutMapping("/avatar/{id}") + public GroupAvatarEdited editGroupAvatar(@RequestParam MultipartFile file, @PathVariable("id") UUID id) { + try { + this.imageFacade.setGroupAvatar(id, new ImageFile(file)); + } catch (ImageService.ImageCouldNotBeSavedException e) { + throw new FileIssueResponse(); + } + return new GroupAvatarEdited(); + } + + @PutMapping("/banner/{id}") + public GroupBannerEdited editGroupBanner(@RequestParam MultipartFile file, @PathVariable("id") UUID id) { + try { + this.imageFacade.setGroupBanner(id, new ImageFile(file)); + } catch (ImageService.ImageCouldNotBeSavedException e) { + throw new FileIssueResponse(); + } + return new GroupBannerEdited(); + } + + @DeleteMapping("/avatar/{id}") + public DeletedGroupAvatarResponse deleteGroupAvatar(@PathVariable("id") UUID id) { + this.imageFacade.removeGroupAvatar(id); + return new DeletedGroupAvatarResponse(); + } + + + @DeleteMapping("/banner/{id}") + public DeletedGroupBannerResponse deleteGroupBanner(@PathVariable("id") UUID id) { + this.imageFacade.removeGroupBanner(id); + return new DeletedGroupBannerResponse(); + } + + private static class DeletedGroupBannerResponse extends SuccessResponse { + } + + private static class DeletedGroupAvatarResponse extends SuccessResponse { + } + + private static class GroupBannerEdited extends SuccessResponse { + } + + private static class GroupAvatarEdited extends SuccessResponse { + } + + private static class FileIssueResponse extends ErrorResponse { + public FileIssueResponse() { + super(HttpStatus.UNPROCESSABLE_ENTITY); + } + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/FileController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/FileController.java new file mode 100644 index 000000000..f41a4790e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/FileController.java @@ -0,0 +1,33 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MaxUploadSizeExceededException; + +@ControllerAdvice +@RestController +@RequestMapping("/uploads") +public final class FileController { + + private static final Logger LOGGER = LoggerFactory.getLogger(FileController.class); + + @ExceptionHandler({MaxUploadSizeExceededException.class}) + public FileTooLargeResponse handleUploadSizeException() { + LOGGER.info("Too large file upload was attempted"); + return new FileTooLargeResponse(); + } + + public static class FileTooLargeResponse extends ResponseEntity { + public FileTooLargeResponse() { + super("FILE_UPLOAD_TOO_LARGE", HttpStatus.PAYLOAD_TOO_LARGE); + } + } + + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/GroupAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/GroupAdminController.java new file mode 100644 index 000000000..c5a0c43b8 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/GroupAdminController.java @@ -0,0 +1,95 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.group.GroupFacade; +import it.chalmers.gamma.util.response.AlreadyExistsResponse; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/internal/admin/groups") +public final class GroupAdminController { + + private final GroupFacade groupFacade; + + public GroupAdminController(GroupFacade groupFacade) { + this.groupFacade = groupFacade; + } + + @PostMapping() + public GroupCreatedResponse addNewGroup(@RequestBody CreateGroupRequest request) { + try { + this.groupFacade.create( + new GroupFacade.NewGroup( + request.name, + request.prettyName, + request.superGroup + ) + ); + } catch (GroupFacade.GroupAlreadyExistsException e) { + throw new GroupAlreadyExistsResponse(); + } + return new GroupCreatedResponse(); + } + + @PutMapping("/{id}") + public GroupUpdatedResponse editGroup(@RequestBody EditGroupRequest request, + @PathVariable("id") UUID id) { + try { + this.groupFacade.update( + new GroupFacade.UpdateGroup( + id, + request.version, + request.name, + request.prettyName, + request.superGroup + ) + ); + } catch (GroupFacade.GroupAlreadyExistsException e) { + throw new GroupAlreadyExistsResponse(); + } + return new GroupUpdatedResponse(); + } + + @DeleteMapping("/{id}") + public GroupDeletedResponse deleteGroup(@PathVariable("id") UUID id) { + try { + this.groupFacade.delete(id); + return new GroupDeletedResponse(); + } catch (GroupFacade.GroupNotFoundRuntimeException e) { + throw new GroupNotFoundResponse(); + } + } + + private record CreateGroupRequest(String name, + String prettyName, + UUID superGroup) { + } + + private record EditGroupRequest(int version, + String name, + String prettyName, + UUID superGroup) { + } + + private static class GroupCreatedResponse extends SuccessResponse { + } + + private static class GroupDeletedResponse extends SuccessResponse { + } + + private static class GroupUpdatedResponse extends SuccessResponse { + } + + private static class GroupNotFoundResponse extends NotFoundResponse { + } + + private static class SuperGroupNotFoundResponse extends NotFoundResponse { + } + + private static class GroupAlreadyExistsResponse extends AlreadyExistsResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/GroupController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/GroupController.java new file mode 100644 index 000000000..d291685df --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/GroupController.java @@ -0,0 +1,36 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.group.GroupFacade; +import it.chalmers.gamma.util.response.NotFoundResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/internal/groups") +public final class GroupController { + + private final GroupFacade groupFacade; + + public GroupController(GroupFacade groupFacade) { + this.groupFacade = groupFacade; + } + + @GetMapping() + public List getGroups() { + return this.groupFacade.getAll(); + } + + @GetMapping("/{id}") + public GroupFacade.GroupWithMembersDTO getGroup(@PathVariable("id") UUID id) { + return this.groupFacade.getWithMembers(id).orElseThrow(GroupNotFoundResponse::new); + } + + private static class GroupNotFoundResponse extends NotFoundResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/InfoApiSettingsAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/InfoApiSettingsAdminController.java new file mode 100644 index 000000000..5924279b4 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/InfoApiSettingsAdminController.java @@ -0,0 +1,36 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.settings.SettingsFacade; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/internal/admin/info-api-settings") +public class InfoApiSettingsAdminController { + + private final SettingsFacade settingsFacade; + + public InfoApiSettingsAdminController(SettingsFacade settingsFacade) { + this.settingsFacade = settingsFacade; + } + + @GetMapping("/super-group-types") + public List getSuperGroupTypes() { + return this.settingsFacade.getInfoApiSuperGroupTypes(); + } + + @PostMapping("/super-group-types") + public InfoApiSuperGroupTypesSetResponse setSuperGroupTypes(@RequestBody SetSuperGroupTypesRequest setSuperGroupTypesRequest) { + this.settingsFacade.setInfoSuperGroupTypes(setSuperGroupTypesRequest.superGroupTypes); + return new InfoApiSuperGroupTypesSetResponse(); + } + + private record SetSuperGroupTypesRequest(List superGroupTypes) { + } + + private static class InfoApiSuperGroupTypesSetResponse extends SuccessResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/MeAvatarController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/MeAvatarController.java new file mode 100644 index 000000000..0d6a281f6 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/MeAvatarController.java @@ -0,0 +1,54 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.adapter.secondary.image.ImageFile; +import it.chalmers.gamma.app.image.domain.ImageService; +import it.chalmers.gamma.app.user.MeFacade; +import it.chalmers.gamma.util.response.ErrorResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/internal/users/me/avatar") +public class MeAvatarController { + + private final MeFacade meFacade; + + public MeAvatarController(MeFacade meFacade) { + this.meFacade = meFacade; + } + + @PutMapping + public EditedProfilePictureResponse editProfileImage(@RequestParam MultipartFile file) { + try { + this.meFacade.setAvatar(new ImageFile(file)); + } catch (ImageService.ImageCouldNotBeSavedException e) { + throw new FileIssueResponse(); + } + return new EditedProfilePictureResponse(); + } + + @DeleteMapping + public DeletedProfilePictureResponse deleteProfileImage() { + try { + this.meFacade.deleteAvatar(); + } catch (ImageService.ImageCouldNotBeRemovedException e) { + throw new RuntimeException(e); + } + return new DeletedProfilePictureResponse(); + } + + private static class DeletedProfilePictureResponse extends SuccessResponse { + } + + private static class EditedProfilePictureResponse extends SuccessResponse { + } + + private static class FileIssueResponse extends ErrorResponse { + public FileIssueResponse() { + super(HttpStatus.UNPROCESSABLE_ENTITY); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/MeController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/MeController.java new file mode 100644 index 000000000..3a2745abf --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/MeController.java @@ -0,0 +1,124 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.user.MeFacade; +import it.chalmers.gamma.util.response.ErrorResponse; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/internal/users/me") +public final class MeController { + + private static final Logger LOGGER = LoggerFactory.getLogger(MeController.class); + + private final MeFacade meFacade; + private final CsrfTokenRepository csrfTokenRepository; + + public MeController(MeFacade meFacade, CsrfTokenRepository csrfTokenRepository) { + this.meFacade = meFacade; + this.csrfTokenRepository = csrfTokenRepository; + } + + @GetMapping() + public MeFacade.MeDTO getMe(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + csrfTokenRepository.saveToken(csrfTokenRepository.generateToken(httpRequest), httpRequest, httpResponse); + + return this.meFacade.getMe(); + } + + @PutMapping() + public UserEditedResponse editMe(@RequestBody EditMeRequest request) { + this.meFacade.updateMe( + new MeFacade.UpdateMe( + request.nick, + request.firstName, + request.lastName, + request.email, + request.language + ) + ); + return new UserEditedResponse(); + } + + @PutMapping("/change_password") + public PasswordChangedResponse changePassword(@RequestBody ChangeUserPassword request) { + this.meFacade.updatePassword( + new MeFacade.UpdatePassword( + request.oldPassword, + request.password + ) + ); + return new PasswordChangedResponse(); + } + + @DeleteMapping() + public UserDeletedResponse deleteMe(@RequestBody DeleteMeRequest request) { + this.meFacade.deleteMe(request.password); + return new UserDeletedResponse(); + } + + @PutMapping("/accept-user-agreement") + public UserAgreementAccepted acceptUserAgreement() { + this.meFacade.acceptUserAgreement(); + return new UserAgreementAccepted(); + } + + @GetMapping("/approval") + public List getApprovedClientsByUser() { + return this.meFacade.getSignedInUserApprovals(); + } + + @DeleteMapping("/approval/{clientUid}") + public ClientApprovalDeletedResponse deleteClientApproval(@PathVariable("clientUid") UUID clientUid) { + this.meFacade.deleteUserApproval(clientUid); + return new ClientApprovalDeletedResponse(); + } + + public record EditMeRequest(String nick, + String firstName, + String lastName, + String email, + String language) { + } + + record ChangeUserPassword(String oldPassword, String password) { + } + + record DeleteMeRequest(String password) { + } + + private static class UserAgreementAccepted extends SuccessResponse { + } + + private static class UserEditedResponse extends SuccessResponse { + } + + private static class PasswordChangedResponse extends SuccessResponse { + } + + private static class UserDeletedResponse extends SuccessResponse { + } + + private static class UserNotFoundResponse extends NotFoundResponse { + } + + private static class ClientApprovalDeletedResponse extends SuccessResponse { + } + + private static class IncorrectCidOrPasswordResponse extends ErrorResponse { + public IncorrectCidOrPasswordResponse() { + super(HttpStatus.UNPROCESSABLE_ENTITY); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/MembershipAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/MembershipAdminController.java new file mode 100644 index 000000000..dd2703502 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/MembershipAdminController.java @@ -0,0 +1,55 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.group.GroupFacade; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/internal/admin/groups") +public final class MembershipAdminController { + + private final GroupFacade groupFacade; + + public MembershipAdminController(GroupFacade groupFacade) { + this.groupFacade = groupFacade; + } + + @PutMapping("/{groupId}/members") + public EditedMembershipResponse editMembers(@PathVariable("groupId") UUID groupId, + @RequestBody List members) { + try { + this.groupFacade.setMembers(groupId, members.stream().map(member -> new GroupFacade.ShallowMember( + member.userId, member.postId, member.unofficialPostName + )).toList()); + } catch (GroupFacade.GroupNotFoundRuntimeException e) { + throw new GroupNotFoundResponse(); + } + + return new EditedMembershipResponse(); + } + + private record AddUserGroupRequest(UUID userId, UUID postId, String unofficialName) { + } + + private record Member(UUID userId, UUID postId, String unofficialPostName) { + } + + private record EditMembers() { + } + + private static class GroupNotFoundResponse extends NotFoundResponse { + } + + private static class EditedMembershipResponse extends SuccessResponse { + } + + private static class PostNotFoundResponse extends NotFoundResponse { + } + + private static class UserNotFoundResponse extends NotFoundResponse { + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/PostAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/PostAdminController.java new file mode 100644 index 000000000..e7e06bc1f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/PostAdminController.java @@ -0,0 +1,84 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.group.GroupFacade; +import it.chalmers.gamma.app.post.PostFacade; +import it.chalmers.gamma.app.post.domain.PostRepository; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/internal/admin/posts") +public final class PostAdminController { + + private final PostFacade postFacade; + + public PostAdminController(PostFacade postFacade) { + this.postFacade = postFacade; + } + + @PostMapping() + public PostCreatedResponse addPost(@RequestBody CreatePostRequest request) { + this.postFacade.create( + new PostFacade.NewPost(request.svName, request.enName, request.emailPrefix()) + ); + return new PostCreatedResponse(); + } + + @PutMapping("/{id}") + public PostEditedResponse editPost( + @RequestBody EditPostRequest request, + @PathVariable("id") UUID id) { + try { + this.postFacade.update( + new PostFacade.UpdatePost( + id, + request.version, + request.svName, + request.enName, + request.emailPrefix + ) + ); + return new PostEditedResponse(); + } catch (PostRepository.PostNotFoundException e) { + throw new PostNotFoundResponse(); + } + } + + @DeleteMapping("/{id}") + public PostDeletedResponse deletePost(@PathVariable("id") UUID id) { + try { + this.postFacade.delete(id); + } catch (PostRepository.PostNotFoundException e) { + throw new PostNotFoundResponse(); + } + return new PostDeletedResponse(); + } + + @GetMapping("/{id}/usage") + public List getUsages(@PathVariable("id") UUID id) { + return this.postFacade.getPostUsages(id); + } + + private record CreatePostRequest(String svName, String enName, String emailPrefix) { + } + + private record EditPostRequest(int version, String svName, String enName, String emailPrefix) { + } + + private static class PostEditedResponse extends SuccessResponse { + } + + private static class PostDeletedResponse extends SuccessResponse { + } + + private static class PostCreatedResponse extends SuccessResponse { + } + + private static class PostNotFoundResponse extends NotFoundResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/PostController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/PostController.java new file mode 100644 index 000000000..e48f49265 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/PostController.java @@ -0,0 +1,37 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.post.PostFacade; +import it.chalmers.gamma.util.response.NotFoundResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/internal/posts") +public final class PostController { + + private final PostFacade postFacade; + + public PostController(PostFacade postFacade) { + this.postFacade = postFacade; + } + + @GetMapping("/{id}") + public PostFacade.PostDTO getPost(@PathVariable("id") UUID id) { + return this.postFacade.get(id) + .orElseThrow(PostNotFoundResponse::new); + } + + @GetMapping() + public List getPosts() { + return this.postFacade.getAll(); + } + + private static class PostNotFoundResponse extends NotFoundResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/SuperGroupAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/SuperGroupAdminController.java new file mode 100644 index 000000000..37888c512 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/SuperGroupAdminController.java @@ -0,0 +1,108 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.supergroup.SuperGroupFacade; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupRepository; +import it.chalmers.gamma.util.response.AlreadyExistsResponse; +import it.chalmers.gamma.util.response.BadRequestResponse; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/internal/admin/super-groups") +public class SuperGroupAdminController { + + private final SuperGroupFacade superGroupFacade; + + public SuperGroupAdminController(SuperGroupFacade superGroupFacade) { + this.superGroupFacade = superGroupFacade; + } + + @PostMapping() + public SuperGroupCreatedResponse createSuperGroup(@RequestBody CreateSuperGroupRequest request) { + try { + this.superGroupFacade.createSuperGroup( + new SuperGroupFacade.NewSuperGroup( + request.name, + request.prettyName, + request.type, + request.svDescription, + request.enDescription + ) + ); + } catch (SuperGroupRepository.SuperGroupAlreadyExistsException e) { + throw new SuperGroupDoesNotFoundResponse(); + } + return new SuperGroupCreatedResponse(); + } + + @DeleteMapping("/{id}") + public SuperGroupDeletedResponse removeSuperGroup(@PathVariable("id") SuperGroupId id) { + try { + this.superGroupFacade.deleteSuperGroup(id); + return new SuperGroupDeletedResponse(); + } catch (SuperGroupFacade.SuperGroupIsUsedException e) { + throw new SuperGroupIsUsedResponse(); + } catch (SuperGroupFacade.SuperGroupNotFoundException e) { + throw new SuperGroupDoesNotFoundResponse(); + } + } + + @PutMapping("/{id}") + public SuperGroupUpdatedResponse updateSuperGroup(@PathVariable("id") UUID id, + @RequestBody EditSuperGroupRequest request) { + try { + this.superGroupFacade.updateSuperGroup( + new SuperGroupFacade.UpdateSuperGroup( + id, + request.version, + request.name, + request.prettyName, + request.type, + request.svDescription, + request.enDescription + ) + ); + } catch (SuperGroupRepository.SuperGroupNotFoundException e) { + throw new SuperGroupDoesNotFoundResponse(); + } + return new SuperGroupUpdatedResponse(); + } + + private record CreateSuperGroupRequest(String name, + String prettyName, + String type, + String svDescription, + String enDescription) { + } + + private record EditSuperGroupRequest(int version, + String name, + String prettyName, + String type, + String svDescription, + String enDescription) { + } + + private static class SuperGroupUpdatedResponse extends SuccessResponse { + } + + private static class SuperGroupDeletedResponse extends SuccessResponse { + } + + private static class SuperGroupCreatedResponse extends SuccessResponse { + } + + private static class SuperGroupAlreadyExistsResponse extends AlreadyExistsResponse { + } + + private static class SuperGroupDoesNotFoundResponse extends NotFoundResponse { + } + + private static class SuperGroupIsUsedResponse extends BadRequestResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/SuperGroupController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/SuperGroupController.java new file mode 100644 index 000000000..b95165757 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/SuperGroupController.java @@ -0,0 +1,46 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.group.GroupFacade; +import it.chalmers.gamma.app.supergroup.SuperGroupFacade; +import it.chalmers.gamma.util.response.NotFoundResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/internal/super-groups") +public class SuperGroupController { + + private final SuperGroupFacade superGroupFacade; + private final GroupFacade groupFacade; + + public SuperGroupController(SuperGroupFacade superGroupFacade, + GroupFacade groupFacade) { + this.superGroupFacade = superGroupFacade; + this.groupFacade = groupFacade; + } + + @GetMapping() + public List getAllSuperGroups() { + return this.superGroupFacade.getAll(); + } + + @GetMapping("/{id}") + public SuperGroupFacade.SuperGroupDTO getSuperGroup(@PathVariable("id") UUID id) { + return this.superGroupFacade.get(id) + .orElseThrow(SuperGroupDoesNotExistResponse::new); + } + + @GetMapping("/{id}/subgroups") + public List getSubgroups(@PathVariable("id") UUID id) { + return this.groupFacade.getAllBySuperGroup(id); + } + + private static class SuperGroupDoesNotExistResponse extends NotFoundResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/SuperGroupTypeAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/SuperGroupTypeAdminController.java new file mode 100644 index 000000000..bd8fed8d6 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/SuperGroupTypeAdminController.java @@ -0,0 +1,77 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.supergroup.SuperGroupFacade; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupTypeRepository; +import it.chalmers.gamma.util.response.AlreadyExistsResponse; +import it.chalmers.gamma.util.response.ErrorResponse; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping(value = "/internal/admin/supergrouptype") +public class SuperGroupTypeAdminController { + + private final SuperGroupFacade superGroupFacade; + + public SuperGroupTypeAdminController(SuperGroupFacade superGroupFacade) { + this.superGroupFacade = superGroupFacade; + } + + @GetMapping + public List getSuperGroupTypes() { + return this.superGroupFacade.getAllTypes(); + } + + @GetMapping("/{type}/usage") + public List getSuperGroupTypeUsage(@PathVariable("type") String type) { + return this.superGroupFacade.getAllSuperGroupsByType(type); + } + + @PostMapping + public SuperGroupTypeAddedResponse addSuperGroupType(@RequestBody AddSuperGroupType request) { + try { + this.superGroupFacade.addType(request.type); + } catch (SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException e) { + throw new SuperGroupTypeAlreadyExistsResponse(); + } + return new SuperGroupTypeAddedResponse(); + } + + @DeleteMapping("/{name}") + public SuperGroupTypeRemovedResponse removeSuperGroupType(@PathVariable("name") String name) { + try { + this.superGroupFacade.removeType(name); + } catch (SuperGroupTypeRepository.SuperGroupTypeNotFoundException e) { + throw new SuperGroupTypeDoesNotExistResponse(); + } catch (SuperGroupTypeRepository.SuperGroupTypeHasUsagesException e) { + throw new SuperGroupTypeIsUsedResponse(); + } + return new SuperGroupTypeRemovedResponse(); + } + + private record AddSuperGroupType(String type) { + } + + private static class SuperGroupTypeAddedResponse extends SuccessResponse { + } + + private static class SuperGroupTypeRemovedResponse extends SuccessResponse { + } + + private static class SuperGroupTypeAlreadyExistsResponse extends AlreadyExistsResponse { + } + + private static class SuperGroupTypeDoesNotExistResponse extends NotFoundResponse { + } + + private static class SuperGroupTypeIsUsedResponse extends ErrorResponse { + private SuperGroupTypeIsUsedResponse() { + super(HttpStatus.UNPROCESSABLE_ENTITY); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserActivationAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserActivationAdminController.java new file mode 100644 index 000000000..293f69d4a --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserActivationAdminController.java @@ -0,0 +1,43 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.user.activation.ActivationCodeFacade; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/internal/admin/user-activation") +public final class UserActivationAdminController { + + private final ActivationCodeFacade activationCodeFacade; + + public UserActivationAdminController(ActivationCodeFacade activationCodeFacade) { + this.activationCodeFacade = activationCodeFacade; + } + + @GetMapping() + public List getAll() { + return this.activationCodeFacade.getAllUserActivations(); + } + + @GetMapping("/{cid}") + public ActivationCodeFacade.UserActivationDTO get(@PathVariable("cid") String cid) { + return this.activationCodeFacade.get(cid) + .orElseThrow(UserActivationNotFoundResponse::new); + } + + @DeleteMapping("/{cid}") + public UserActivationDeletedResponse removeUserActivation(@PathVariable("cid") String cid) { + this.activationCodeFacade.removeUserActivation(cid); + return new UserActivationDeletedResponse(); + } + + private static class UserActivationDeletedResponse extends SuccessResponse { + } + + private static class UserActivationNotFoundResponse extends NotFoundResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserAdminController.java new file mode 100644 index 000000000..e4ae5cab9 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserAdminController.java @@ -0,0 +1,157 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.admin.AdminFacade; +import it.chalmers.gamma.app.image.ImageFacade; +import it.chalmers.gamma.app.user.UserCreationFacade; +import it.chalmers.gamma.app.user.UserFacade; +import it.chalmers.gamma.util.response.NotFoundResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/internal/admin/users") +public final class UserAdminController { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserAdminController.class); + + private final UserFacade userFacade; + private final UserCreationFacade userCreationFacade; + private final ImageFacade imageFacade; + private final AdminFacade adminFacade; + + public UserAdminController(UserFacade userFacade, + UserCreationFacade userCreationFacade, + ImageFacade imageFacade, + AdminFacade adminFacade) { + this.userFacade = userFacade; + this.userCreationFacade = userCreationFacade; + this.imageFacade = imageFacade; + this.adminFacade = adminFacade; + } + + @PutMapping("/{id}/change_password") + public PasswordChangedResponse changePassword( + @PathVariable("id") UUID id, + @RequestBody AdminChangePasswordRequest request) { + this.userFacade.setUserPassword(id, request.password); + return new PasswordChangedResponse(); + } + + @DeleteMapping("/{id}") + public UserDeletedResponse deleteUser(@PathVariable("id") UUID id) { + this.userFacade.deleteUser(id); + return new UserDeletedResponse(); + } + + @GetMapping("/{id}") + public UserFacade.UserExtendedWithGroupsDTO getUser(@PathVariable("id") UUID id) { + return this.userFacade.getAsAdmin(id) + .orElseThrow(); + } + + @PostMapping() + public UserCreatedResponse addUser(@RequestBody AdminViewCreateUserRequest request) { + try { + this.userCreationFacade.createUser( + new UserCreationFacade.NewUser( + request.password, + request.nick, + request.firstName, + request.email, + request.lastName, + request.acceptanceYear, + request.cid, + request.language + ) + ); + } catch (UserCreationFacade.SomePropertyNotUniqueException e) { + e.printStackTrace(); + } + return new UserCreatedResponse(); + } + + @PutMapping("/{id}") + public UserEditedResponse editUser(@PathVariable("id") UUID id, + @RequestBody EditUserRequest request) { + this.userFacade.updateUser( + new UserFacade.UpdateUser( + id, + request.nick, + request.firstName, + request.lastName, + request.email, + request.language, + request.acceptanceYear + ) + ); + return new UserEditedResponse(); + } + + @DeleteMapping("/{id}/remove-avatar") + public UserAvatarRemovedResponse removeUserAvatar(@PathVariable("id") UUID userId) { + this.imageFacade.removeUserAvatar(userId); + return new UserAvatarRemovedResponse(); + } + + @GetMapping("/admins") + public List getAdmins() { + return this.adminFacade.getAllAdmins(); + } + + @PutMapping("/admins/{id}") + public void setAdmin(@PathVariable("id") UUID id, @RequestBody SetAdmin request) { + this.adminFacade.setAdmin(id, request.isAdmin); + } + + record SetAdmin(boolean isAdmin) { + } + + record AdminChangePasswordRequest(String password) { + } + + record AdminViewCreateUserRequest(String cid, + String password, + String nick, + String firstName, + String lastName, + String email, + int acceptanceYear, + String language) { + } + + record EditUserRequest(String nick, + String firstName, + String lastName, + String email, + String language, + int acceptanceYear) { + } + + private static class UserAdminChangeResponse extends SuccessResponse { + + } + + private static class PasswordChangedResponse extends SuccessResponse { + } + + private static class UserDeletedResponse extends SuccessResponse { + } + + private static class UserCreatedResponse extends SuccessResponse { + } + + private static class UserEditedResponse extends SuccessResponse { + } + + private static class UserAvatarRemovedResponse extends SuccessResponse { + } + + private static class UserNotFoundResponse extends NotFoundResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserAgreementAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserAgreementAdminController.java new file mode 100644 index 000000000..223ee4110 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserAgreementAdminController.java @@ -0,0 +1,36 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.settings.SettingsFacade; +import it.chalmers.gamma.util.response.BadRequestResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/internal/admin/useragreement") +public class UserAgreementAdminController { + + private final SettingsFacade settingsFacade; + + public UserAgreementAdminController(SettingsFacade settingsFacade) { + this.settingsFacade = settingsFacade; + } + + @PostMapping + public UserAgreementHasBeenResetResponse resetUserAgreement(@RequestBody ConfirmPassword password) { + this.settingsFacade.resetUserAgreement(password.password); + return new UserAgreementHasBeenResetResponse(); + } + + private record ConfirmPassword(String password) { + } + + public static class UserAgreementHasBeenResetResponse extends SuccessResponse { + } + + public static class IncorrectPasswordResponse extends BadRequestResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserAvatarAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserAvatarAdminController.java new file mode 100644 index 000000000..674d616d1 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserAvatarAdminController.java @@ -0,0 +1,31 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.image.ImageFacade; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.UUID; + +@RestController +@RequestMapping("/internal/admin/users/avatar") +public class UserAvatarAdminController { + + private final ImageFacade imageFacade; + + public UserAvatarAdminController(ImageFacade imageFacade) { + this.imageFacade = imageFacade; + } + + @DeleteMapping("/{userId}") + public UserAvatarDeletedResponse deleteUserAvatar(@PathVariable("userId") UUID userId) { + this.imageFacade.removeUserAvatar(userId); + return new UserAvatarDeletedResponse(); + } + + public static class UserAvatarDeletedResponse extends SuccessResponse { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserController.java new file mode 100644 index 000000000..6cb3cb4f0 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserController.java @@ -0,0 +1,84 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.user.UserCreationFacade; +import it.chalmers.gamma.app.user.UserFacade; +import it.chalmers.gamma.util.response.ErrorResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/internal/users") +public final class UserController { + + private final UserFacade userFacade; + private final UserCreationFacade userCreationFacade; + + public UserController(UserFacade userFacade, + UserCreationFacade userCreationFacade) { + this.userFacade = userFacade; + this.userCreationFacade = userCreationFacade; + } + + @GetMapping() + public List getAllUsers() { + return this.userFacade.getAll(); + } + + @GetMapping("/{id}") + public UserFacade.UserWithGroupsDTO getUser(@PathVariable("id") UUID id) { + return this.userFacade.get(id).orElseThrow(); + } + + @PostMapping("/create") + public UserCreatedResponse createUser(@RequestBody CreateUserRequest request) { + if(!request.userAgreement) { + throw new NotAcceptedUserAgreementResponse(); + } + + //TODO: Check for any exceptions, and throw a generic error if something goes wrong + try { + this.userCreationFacade.createUserWithCode( + new UserCreationFacade.NewUser( + request.password, + request.nick, + request.firstName, + request.email, + request.lastName, + request.acceptanceYear, + request.cid, + request.language + ), request.code + ); + } catch (UserCreationFacade.SomePropertyNotUniqueException e) { + e.printStackTrace(); + } + return new UserCreatedResponse(); + } + + record CreateUserRequest(String code, + String password, + String nick, + String firstName, + String email, + String lastName, + boolean userAgreement, + int acceptanceYear, + String cid, + String language) { + } + + private static class UserCreatedResponse extends SuccessResponse { + } + + private static class NotAcceptedUserAgreementResponse extends ErrorResponse { + + public NotAcceptedUserAgreementResponse() { + super(HttpStatus.BAD_REQUEST); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserGDPRAdminController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserGDPRAdminController.java new file mode 100644 index 000000000..b2270ad60 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserGDPRAdminController.java @@ -0,0 +1,39 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.user.UserGdprTrainingFacade; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController() +@RequestMapping("/internal/admin/gdpr") +public class UserGDPRAdminController { + + private final UserGdprTrainingFacade userGdprTrainingFacade; + + public UserGDPRAdminController(UserGdprTrainingFacade userGdprTrainingFacade) { + this.userGdprTrainingFacade = userGdprTrainingFacade; + } + + @GetMapping() + public List getUsersWithGdprTraining() { + return this.userGdprTrainingFacade.getGdprTrained(); + } + + @PutMapping("/{id}") + public GdprStatusEditedResponse editGDPRStatus(@PathVariable("id") UUID id, + @RequestBody ChangeGDPRStatusRequest request) { + this.userGdprTrainingFacade.updateGdprTrainedStatus(id, request.gdpr); + return new GdprStatusEditedResponse(); + } + + private record ChangeGDPRStatusRequest(boolean gdpr) { + } + + private static class GdprStatusEditedResponse extends SuccessResponse { + } + +} + diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserPasswordResetController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserPasswordResetController.java new file mode 100644 index 000000000..06f935377 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/internal/UserPasswordResetController.java @@ -0,0 +1,61 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.user.passwordreset.UserResetPasswordFacade; +import it.chalmers.gamma.util.response.ErrorResponse; +import it.chalmers.gamma.util.response.SuccessResponse; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequestMapping("/internal/users/reset_password") +public class UserPasswordResetController { + + private final UserResetPasswordFacade userResetPasswordFacade; + + public UserPasswordResetController(UserResetPasswordFacade userResetPasswordFacade) { + this.userResetPasswordFacade = userResetPasswordFacade; + } + + @PostMapping() + public PasswordRestLinkSentResponse resetPasswordRequest(@RequestBody ResetPasswordRequest request) { + try { + this.userResetPasswordFacade.startResetPasswordProcess(request.email); + } catch (UserResetPasswordFacade.PasswordResetProcessException e) { + throw new PasswordResetProcessErrorResponse(); + } + return new PasswordRestLinkSentResponse(); + } + + @PutMapping("/finish") + public PasswordChangedResponse resetPassword(@RequestBody ResetPasswordFinishRequest request) { + try { + this.userResetPasswordFacade.finishResetPasswordProcess(request.email, request.token, request.password); + } catch (UserResetPasswordFacade.PasswordResetProcessException e) { + throw new PasswordResetProcessErrorResponse(); + } + return new PasswordChangedResponse(); + } + + private record ResetPasswordRequest(String email) { + } + + private record ResetPasswordFinishRequest(String password, + String email, + String token) { + } + + private static class PasswordRestLinkSentResponse extends SuccessResponse { + } + + private static class PasswordChangedResponse extends SuccessResponse { + } + + private static class PasswordResetProcessErrorResponse extends ErrorResponse { + private PasswordResetProcessErrorResponse() { + super(HttpStatus.UNPROCESSABLE_ENTITY); + } + } + + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/thymeleaf/AccountDeletedController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/thymeleaf/AccountDeletedController.java new file mode 100644 index 000000000..f78fac7c3 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/thymeleaf/AccountDeletedController.java @@ -0,0 +1,14 @@ +package it.chalmers.gamma.adapter.primary.thymeleaf; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class AccountDeletedController { + + @GetMapping("/account-deleted") + public String getAccountDeleted() { + //TODO: Remove cookie + return "accountdeleted"; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/thymeleaf/ApiRedirectController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/thymeleaf/ApiRedirectController.java new file mode 100644 index 000000000..31c3e372e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/thymeleaf/ApiRedirectController.java @@ -0,0 +1,24 @@ +package it.chalmers.gamma.adapter.primary.thymeleaf; + +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/") +public class ApiRedirectController { + + private final String frontendUrl; + + public ApiRedirectController(@Value("${application.frontend-client-details.successful-login-uri}") String frontendUrl) { + this.frontendUrl = frontendUrl; + } + + @GetMapping() + public void redirectToFrontend(HttpServletResponse httpServletResponse) { + httpServletResponse.setHeader("Location", frontendUrl); + httpServletResponse.setStatus(301); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/thymeleaf/LoginController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/thymeleaf/LoginController.java new file mode 100644 index 000000000..728ee8746 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/thymeleaf/LoginController.java @@ -0,0 +1,53 @@ +package it.chalmers.gamma.adapter.primary.thymeleaf; + +import it.chalmers.gamma.security.GammaRequestCache; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +public class LoginController { + + private final GammaRequestCache gammaRequestCache; + + @Value("${application.frontend-client-details.successful-login-uri}") + private String baseFrontendUrl; + + public LoginController(GammaRequestCache gammaRequestCache) { + this.gammaRequestCache = gammaRequestCache; + } + + @GetMapping("/login") + public String getLogin(@RequestParam(value = "error", required = false) String error, + @RequestParam(value = "logout", required = false) String logout, + @RequestParam(value = "authorizing", required = false) String authorizing, + Model model, + HttpServletRequest request, + HttpServletResponse response) { + boolean isAuthorizing = authorizing != null; + + model.addAttribute("createAccountUrl", this.baseFrontendUrl + "/create-account"); + model.addAttribute("forgotPasswordUrl", this.baseFrontendUrl + "/reset-password"); + model.addAttribute("error", error); + model.addAttribute("logout", logout); + model.addAttribute("authorizing", isAuthorizing); + + /* + * There might be a situation where a user starts an authorizing + * against a client, but stops while the redirect request has been cached. + * This makes sure that the user when actually trying to login to the + * Gamma frontend that they're redirect to that, and not redirected + * to the consent page for example. + */ + if (!isAuthorizing) { + gammaRequestCache.removeRequest(request, response); + } + + return "login"; + } + +} \ No newline at end of file diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/primary/thymeleaf/OAuth2ConsentController.java b/backend/src/main/java/it/chalmers/gamma/adapter/primary/thymeleaf/OAuth2ConsentController.java new file mode 100644 index 000000000..2b86a7501 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/primary/thymeleaf/OAuth2ConsentController.java @@ -0,0 +1,45 @@ +package it.chalmers.gamma.adapter.primary.thymeleaf; + +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.security.Principal; + +@Controller +public class OAuth2ConsentController { + + private final RegisteredClientRepository registeredClientRepository; + + public OAuth2ConsentController(RegisteredClientRepository registeredClientRepository) { + this.registeredClientRepository = registeredClientRepository; + } + + @GetMapping("/oauth2/consent") + public String getOAuth2Consent(Principal principal, Model model, + @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId, + @RequestParam(OAuth2ParameterNames.SCOPE) String scope, + @RequestParam(OAuth2ParameterNames.STATE) String state) { + + RegisteredClient client = this.registeredClientRepository.findByClientId(clientId); + + //TODO: Do something better than this. + if (client == null) { + return null; + } + + model.addAttribute("clientId", clientId); + model.addAttribute("clientName", client.getClientName()); + model.addAttribute("state", state); + model.addAttribute("hasEmailScope", scope.contains("email")); + model.addAttribute("scopes", scope.split(" ")); + + + return "consent"; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/image/ImageFile.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/image/ImageFile.java new file mode 100644 index 000000000..b43e98796 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/image/ImageFile.java @@ -0,0 +1,19 @@ +package it.chalmers.gamma.adapter.secondary.image; + +import it.chalmers.gamma.app.image.domain.Image; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Arrays; +import java.util.Objects; + +public record ImageFile(MultipartFile file) implements Image { + + public ImageFile { + Objects.requireNonNull(file); + if (!Arrays.asList("image/jpeg", "image/png", "image/gif") + .contains(file.getContentType())) { + throw new IllegalStateException("File must be an Image"); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/image/LocalImageService.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/image/LocalImageService.java new file mode 100644 index 000000000..7f7432166 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/image/LocalImageService.java @@ -0,0 +1,107 @@ +package it.chalmers.gamma.adapter.secondary.image; + +import it.chalmers.gamma.app.image.domain.Image; +import it.chalmers.gamma.app.image.domain.ImageService; +import it.chalmers.gamma.app.image.domain.ImageUri; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.StreamUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.UUID; + + +@Component +public class LocalImageService implements ImageService { + + private static final Logger LOGGER = LoggerFactory.getLogger(LocalImageService.class); + + private final String relativePath; + + public LocalImageService(@Value("${application.files.path}") String relativePath) { + this.relativePath = relativePath; + } + + public ImageUri saveImage(Image image) throws ImageCouldNotBeSavedException { + if (image instanceof ImageFile) { + MultipartFile file = ((ImageFile) image).file(); + + this.checkIfValidImageContent(file); + + String filePathString = UUID.randomUUID() + "/" + file.getName() + "." + getExtension(file.getOriginalFilename()); + + File filePath = new File(this.relativePath + filePathString); + + File fileFolderPath = new File(filePath.getParent()); + if (!fileFolderPath.mkdirs()) { + throw new ImageCouldNotBeSavedException("File folder could not be created"); + } + + try { + if (!filePath.createNewFile()) { + throw new ImageCouldNotBeSavedException("(1) File could not be created"); + } + + try (OutputStream fos = Files.newOutputStream(Path.of(filePath.getPath()))) { + fos.write(file.getBytes()); + } + + } catch (IOException e) { + throw new ImageCouldNotBeSavedException("(2) File could not be created", e); + } + + LOGGER.info("Image " + file.getOriginalFilename() + " was uploaded."); + + return new ImageUri(filePathString); + } + throw new RuntimeException("Image not of type ImageFile"); + } + + public void removeImage(ImageUri imageUri) throws ImageCouldNotBeRemovedException { + File f = new File(relativePath + imageUri.value()); + if (!f.delete()) { + LOGGER.error("Could not delete the file: " + imageUri); + throw new ImageCouldNotBeRemovedException(); + } + } + + @Override + public ImageDetails getImage(ImageUri imageUri) { + try { + return new ImageDetails(StreamUtils.copyToByteArray(Files.newInputStream(Paths.get(this.relativePath + imageUri.value()))), getType(imageUri)); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + private String getExtension(String fileName) { + if (fileName == null) { + throw new IllegalArgumentException(); + } + + return fileName.substring(fileName.lastIndexOf('.') + 1); + } + + private String getType(ImageUri imageUri) { + String[] split = imageUri.value().split("\\."); + return split[split.length - 1]; + } + + private void checkIfValidImageContent(MultipartFile file) throws ImageCouldNotBeSavedException { + String contentType = file.getContentType(); + if (!List.of("image/jpeg", "image/png", "image/gif").contains(contentType)) { + throw new ImageCouldNotBeSavedException("Image content not valid"); + } + } + +} \ No newline at end of file diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/allowlist/AllowListEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/allowlist/AllowListEntity.java new file mode 100644 index 000000000..291bf346e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/allowlist/AllowListEntity.java @@ -0,0 +1,30 @@ +package it.chalmers.gamma.adapter.secondary.jpa.allowlist; + +import it.chalmers.gamma.adapter.secondary.jpa.util.AbstractEntity; +import it.chalmers.gamma.app.user.domain.Cid; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + + +@Entity +@Table(name = "g_allowlist") +public class AllowListEntity extends AbstractEntity { + + @Id + @Column(name = "cid") + private String cid; + + protected AllowListEntity() { + } + + protected AllowListEntity(String cid) { + this.cid = cid; + } + + @Override + public String getId() { + return this.cid; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/allowlist/AllowListJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/allowlist/AllowListJpaRepository.java new file mode 100644 index 000000000..324ba7ed8 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/allowlist/AllowListJpaRepository.java @@ -0,0 +1,8 @@ +package it.chalmers.gamma.adapter.secondary.jpa.allowlist; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AllowListJpaRepository extends JpaRepository { +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/allowlist/AllowListRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/allowlist/AllowListRepositoryAdapter.java new file mode 100644 index 000000000..2e20821d7 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/allowlist/AllowListRepositoryAdapter.java @@ -0,0 +1,61 @@ +package it.chalmers.gamma.adapter.secondary.jpa.allowlist; + +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorHelper; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorState; +import it.chalmers.gamma.app.user.domain.Cid; +import it.chalmers.gamma.app.user.allowlist.AllowListRepository; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class AllowListRepositoryAdapter implements AllowListRepository { + + private final AllowListJpaRepository allowListJpaRepository; + + private final PersistenceErrorState CID_ALREADY_ALLOWED = new PersistenceErrorState( + null, PersistenceErrorState.Type.NOT_UNIQUE + ); + + public AllowListRepositoryAdapter(AllowListJpaRepository allowListJpaRepository) { + this.allowListJpaRepository = allowListJpaRepository; + } + + @Override + public void allow(Cid cid) throws AlreadyAllowedException { + try { + this.allowListJpaRepository.save(new AllowListEntity(cid.value())); + } catch (DataIntegrityViolationException e) { + PersistenceErrorState state = PersistenceErrorHelper.getState(e); + + if (CID_ALREADY_ALLOWED.equals(state)) { + throw new AlreadyAllowedException(); + } + } + } + + @Override + public void remove(Cid cid) throws NotOnAllowListException { + try { + this.allowListJpaRepository.deleteById(cid.value()); + } catch(EmptyResultDataAccessException e) { + throw new NotOnAllowListException(); + } + } + + @Override + public boolean isAllowed(Cid cid) { + return this.allowListJpaRepository.existsById(cid.value()); + } + + @Override + public List getAllowList() { + return this.allowListJpaRepository.findAll() + .stream() + .map(AllowListEntity::getId) + .map(Cid::new) + .toList(); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyEntity.java new file mode 100644 index 000000000..43c0b0af3 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyEntity.java @@ -0,0 +1,68 @@ +package it.chalmers.gamma.adapter.secondary.jpa.apikey; + +import it.chalmers.gamma.adapter.secondary.jpa.text.TextEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.MutableEntity; +import it.chalmers.gamma.app.apikey.domain.ApiKeyType; +import jakarta.persistence.*; + +import java.util.UUID; + +@Entity +@Table(name = "g_apikey") +public class ApiKeyEntity extends MutableEntity { + + @Id + @Column(name = "api_key_id", columnDefinition = "uuid") + protected UUID id; + + @Column(name = "token") + protected String token; + + @Column(name = "pretty_name") + protected String prettyName; + + @Enumerated(EnumType.STRING) + protected ApiKeyType keyType; + + @JoinColumn(name = "description") + @OneToOne(cascade = CascadeType.ALL) + protected TextEntity description; + + public ApiKeyEntity() { + description = new TextEntity(); + } + + public ApiKeyEntity(UUID id, String token, String prettyName, ApiKeyType keyType, TextEntity description) { + this.id = id; + this.token = token; + this.prettyName = prettyName; + this.keyType = keyType; + this.description = description; + } + + @Override + public UUID getId() { + return this.id; + } + + public void setApiKeyToken(String token) { + this.token = token; + } + + public String getToken() { + return token; + } + + public String getPrettyName() { + return prettyName; + } + + public ApiKeyType getKeyType() { + return keyType; + } + + public TextEntity getDescription() { + return description; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyEntityConverter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyEntityConverter.java new file mode 100644 index 000000000..55b52fa60 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyEntityConverter.java @@ -0,0 +1,22 @@ +package it.chalmers.gamma.adapter.secondary.jpa.apikey; + +import it.chalmers.gamma.app.apikey.domain.ApiKey; +import it.chalmers.gamma.app.apikey.domain.ApiKeyId; +import it.chalmers.gamma.app.apikey.domain.ApiKeyToken; +import it.chalmers.gamma.app.common.PrettyName; +import org.springframework.stereotype.Service; + +@Service +public class ApiKeyEntityConverter { + + public ApiKey toDomain(ApiKeyEntity entity) { + return new ApiKey( + new ApiKeyId(entity.getId()), + new PrettyName(entity.getPrettyName()), + entity.getDescription().toDomain(), + entity.getKeyType(), + new ApiKeyToken(entity.getToken()) + ); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyJpaRepository.java new file mode 100644 index 000000000..63820d707 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyJpaRepository.java @@ -0,0 +1,10 @@ +package it.chalmers.gamma.adapter.secondary.jpa.apikey; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface ApiKeyJpaRepository extends JpaRepository { + Optional findByToken(String token); +} \ No newline at end of file diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyRepositoryAdapter.java new file mode 100644 index 000000000..cec111f7f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyRepositoryAdapter.java @@ -0,0 +1,109 @@ +package it.chalmers.gamma.adapter.secondary.jpa.apikey; + +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientApiKeyEntity; +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientApiKeyJpaRepository; +import it.chalmers.gamma.app.apikey.domain.ApiKey; +import it.chalmers.gamma.app.apikey.domain.ApiKeyId; +import it.chalmers.gamma.app.apikey.domain.ApiKeyRepository; +import it.chalmers.gamma.app.apikey.domain.ApiKeyToken; +import jakarta.persistence.EntityExistsException; +import jakarta.persistence.EntityNotFoundException; +import jakarta.transaction.Transactional; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Repository +public class ApiKeyRepositoryAdapter implements ApiKeyRepository { + + private final ApiKeyJpaRepository repository; + private final ApiKeyEntityConverter apiKeyEntityConverter; + private final ClientApiKeyJpaRepository clientApiKeyJpaRepository; + + public ApiKeyRepositoryAdapter(ApiKeyJpaRepository repository, + ApiKeyEntityConverter apiKeyEntityConverter, + ClientApiKeyJpaRepository clientApiKeyJpaRepository) { + this.repository = repository; + this.apiKeyEntityConverter = apiKeyEntityConverter; + this.clientApiKeyJpaRepository = clientApiKeyJpaRepository; + } + + @Override + public void create(ApiKey apiKey) throws ApiKeyAlreadyExistRuntimeException { + try { + this.repository.saveAndFlush(toEntity(apiKey)); + } catch (DataIntegrityViolationException e) { + if (e.getCause() instanceof EntityExistsException) { + throw new ApiKeyAlreadyExistRuntimeException(); + } + throw e; + } + } + + @Transactional + @Override + public void delete(ApiKeyId apiKeyId) throws ApiKeyNotFoundException { + try { + + //First check if this ApiKey is connected to a Client. + ClientApiKeyEntity clientApiKeyEntity = this.clientApiKeyJpaRepository.getByApiKey_Id(apiKeyId.value()); + if (clientApiKeyEntity != null) { + clientApiKeyEntity.removeApiKey(); + this.clientApiKeyJpaRepository.saveAndFlush(clientApiKeyEntity); + } else { + this.repository.deleteById(apiKeyId.value()); + } + + } catch (EmptyResultDataAccessException e) { + throw new ApiKeyNotFoundException(); + } + } + + @Override + public ApiKeyToken resetApiKeyToken(ApiKeyId apiKeyId) throws ApiKeyNotFoundException { + ApiKeyToken newToken = ApiKeyToken.generate(); + ApiKeyEntity entity = this.repository.findById(apiKeyId.value()) + .orElseThrow(ApiKeyNotFoundException::new); + entity.setApiKeyToken(newToken.value()); + this.repository.saveAndFlush(entity); + return newToken; + } + + @Override + public List getAll() { + return this.repository + .findAll() + .stream() + .map(this.apiKeyEntityConverter::toDomain) + .collect(Collectors.toList()); + } + + @Override + public Optional getById(ApiKeyId apiKeyId) { + return this.repository.findById(apiKeyId.value()) + .map(this.apiKeyEntityConverter::toDomain); + } + + @Override + public Optional getByToken(ApiKeyToken apiKeyToken) { + return this.repository.findByToken(apiKeyToken.value()) + .map(this.apiKeyEntityConverter::toDomain); + } + + private ApiKeyEntity toEntity(ApiKey apiKey) { + ApiKeyEntity apiKeyEntity = new ApiKeyEntity(); + + apiKeyEntity.id = apiKey.id().value(); + apiKeyEntity.token = apiKey.apiKeyToken().value(); + apiKeyEntity.prettyName = apiKey.prettyName().value(); + apiKeyEntity.keyType = apiKey.keyType(); + apiKeyEntity.description.apply(apiKey.description()); + + return apiKeyEntity; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientApiKeyEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientApiKeyEntity.java new file mode 100644 index 000000000..20180fde3 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientApiKeyEntity.java @@ -0,0 +1,51 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client; + +import it.chalmers.gamma.adapter.secondary.jpa.apikey.ApiKeyEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import jakarta.persistence.*; + +import java.util.UUID; + +@Entity +@Table(name = "g_client_apikey") +public class ClientApiKeyEntity extends ImmutableEntity { + + @Id + @Column(name = "client_uid", columnDefinition = "uuid") + private UUID clientUid; + + @OneToOne + @PrimaryKeyJoinColumn(name = "client_uid") + private ClientEntity client; + + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "api_key_id") + private ApiKeyEntity apiKey; + + protected ClientApiKeyEntity() { + + } + + protected ClientApiKeyEntity(ClientEntity clientEntity, ApiKeyEntity apiKeyEntity) { + this.client = clientEntity; + this.clientUid = clientEntity.clientUid; + this.apiKey = apiKeyEntity; + } + + @Override + public UUID getId() { + return this.clientUid; + } + + public ClientEntity getClient() { + return this.client; + } + + public ApiKeyEntity getApiKeyEntity() { + return this.apiKey; + } + + public void removeApiKey() { + this.apiKey = null; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientApiKeyJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientApiKeyJpaRepository.java new file mode 100644 index 000000000..038715401 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientApiKeyJpaRepository.java @@ -0,0 +1,14 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface ClientApiKeyJpaRepository extends JpaRepository { + Optional findByApiKey_Token(String apiKeyToken); + + ClientApiKeyEntity getByApiKey_Id(UUID apiKeyId); +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientApprovalsRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientApprovalsRepositoryAdapter.java new file mode 100644 index 000000000..1af060959 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientApprovalsRepositoryAdapter.java @@ -0,0 +1,41 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client; + +import it.chalmers.gamma.adapter.secondary.jpa.user.UserApprovalEntity; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserApprovalJpaRepository; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntityConverter; +import it.chalmers.gamma.app.client.domain.ClientId; +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.client.domain.approval.ClientApprovalsRepository; +import it.chalmers.gamma.app.user.domain.GammaUser; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ClientApprovalsRepositoryAdapter implements ClientApprovalsRepository { + + private final UserApprovalJpaRepository userApprovalJpaRepository; + private final UserEntityConverter userEntityConverter; + + public ClientApprovalsRepositoryAdapter(UserApprovalJpaRepository userApprovalJpaRepository, + UserEntityConverter userEntityConverter) { + this.userApprovalJpaRepository = userApprovalJpaRepository; + this.userEntityConverter = userEntityConverter; + } + + @Override + public List getAllByClientId(ClientId clientId) { + throw new UnsupportedOperationException(); + } + + @Override + public List getAllByClientUid(ClientUid clientUid) { + List userApprovalEntities = this.userApprovalJpaRepository.findAllById_Client_ClientUid(clientUid.value()); + + return userApprovalEntities + .stream() + .map(UserApprovalEntity::getUserEntity) + .map(this.userEntityConverter::toDomain) + .toList(); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientEntity.java new file mode 100644 index 000000000..66c701fc7 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientEntity.java @@ -0,0 +1,54 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client; + +import it.chalmers.gamma.adapter.secondary.jpa.client.restriction.ClientRestrictionEntity; +import it.chalmers.gamma.adapter.secondary.jpa.text.TextEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "g_client") +public class ClientEntity extends ImmutableEntity { + + @Id + @Column(name = "client_uid", columnDefinition = "uuid") + protected UUID clientUid; + + @Column(name = "client_id") + protected String clientId; + + @Column(name = "client_secret") + protected String clientSecret; + + @Column(name = "redirect_uri") + protected String webServerRedirectUrl; + + @Column(name = "pretty_name") + protected String prettyName; + + @JoinColumn(name = "description") + @OneToOne(cascade = CascadeType.ALL) + protected TextEntity description; + + @OneToMany(mappedBy = "id.client", cascade = CascadeType.ALL, orphanRemoval = true) + protected List scopes; + + @OneToOne(mappedBy = "client", cascade = CascadeType.ALL, orphanRemoval = true) + protected ClientApiKeyEntity clientsApiKey; + + @OneToOne(mappedBy = "client", cascade = CascadeType.ALL, orphanRemoval = true) + protected ClientRestrictionEntity clientRestriction; + + protected ClientEntity() { + this.scopes = new ArrayList<>(); + } + + @Override + public UUID getId() { + return this.clientUid; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientEntityConverter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientEntityConverter.java new file mode 100644 index 000000000..539370ef3 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientEntityConverter.java @@ -0,0 +1,56 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client; + +import it.chalmers.gamma.adapter.secondary.jpa.apikey.ApiKeyEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntityConverter; +import it.chalmers.gamma.app.client.domain.*; +import it.chalmers.gamma.app.client.domain.restriction.ClientRestriction; +import it.chalmers.gamma.app.client.domain.restriction.ClientRestrictionId; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class ClientEntityConverter { + + private final ApiKeyEntityConverter apiKeyEntityConverter; + private final SuperGroupRepository superGroupRepository; + + public ClientEntityConverter(ApiKeyEntityConverter apiKeyEntityConverter, + SuperGroupRepository superGroupRepository) { + this.apiKeyEntityConverter = apiKeyEntityConverter; + this.superGroupRepository = superGroupRepository; + } + + public Client toDomain(ClientEntity clientEntity) { + List scopes = clientEntity.scopes + .stream() + .map(ClientScopeEntity::getScope) + .toList(); + + return new Client( + new ClientUid(clientEntity.getId()), + new ClientId(clientEntity.clientId), + new ClientSecret(clientEntity.clientSecret), + new ClientRedirectUrl(clientEntity.webServerRedirectUrl), + new PrettyName(clientEntity.prettyName), + clientEntity.description.toDomain(), + scopes, + Optional.ofNullable(clientEntity.clientsApiKey) + .map(ClientApiKeyEntity::getApiKeyEntity) + .map(apiKeyEntityConverter::toDomain) + .orElse(null), + new ClientOwnerOfficial(), + clientEntity.clientRestriction == null ? null : new ClientRestriction( + new ClientRestrictionId(clientEntity.clientRestriction.getRestrictionId()), + clientEntity.clientRestriction.getSuperGroupRestrictions() + .stream() + .map(clientRestrictionSuperGroupEntity -> this.superGroupRepository.get(clientRestrictionSuperGroupEntity.getId().getValue().superGroupId()).orElseThrow()) + .toList() + ) + ); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientJpaRepository.java new file mode 100644 index 000000000..ecae2a7db --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientJpaRepository.java @@ -0,0 +1,10 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface ClientJpaRepository extends JpaRepository { + Optional findByClientId(String clientId); +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientRepositoryAdapter.java new file mode 100644 index 000000000..991f258a1 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientRepositoryAdapter.java @@ -0,0 +1,208 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client; + +import it.chalmers.gamma.adapter.secondary.jpa.apikey.ApiKeyEntity; +import it.chalmers.gamma.adapter.secondary.jpa.client.restriction.ClientRestrictionEntity; +import it.chalmers.gamma.adapter.secondary.jpa.client.restriction.ClientRestrictionSuperGroupEntity; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupJpaRepository; +import it.chalmers.gamma.adapter.secondary.jpa.text.TextEntity; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserApprovalEntity; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserApprovalJpaRepository; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserJpaRepository; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorHelper; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorState; +import it.chalmers.gamma.app.apikey.domain.ApiKeyToken; +import it.chalmers.gamma.app.client.domain.Client; +import it.chalmers.gamma.app.client.domain.ClientId; +import it.chalmers.gamma.app.client.domain.ClientRepository; +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupRepository; +import it.chalmers.gamma.app.user.domain.UserId; +import jakarta.transaction.Transactional; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional +public class ClientRepositoryAdapter implements ClientRepository { + + private static final PersistenceErrorState authorityNotFound = new PersistenceErrorState( + "itclient_authority_level_restriction_authority_fkey", + PersistenceErrorState.Type.FOREIGN_KEY_VIOLATION + ); + private static final PersistenceErrorState userNotFound = new PersistenceErrorState( + "it_user_approval_user_id_fkey", + PersistenceErrorState.Type.FOREIGN_KEY_VIOLATION + ); + private static final PersistenceErrorState clientIdAlreadyExists = new PersistenceErrorState( + "itclient_client_id_key", + PersistenceErrorState.Type.NOT_UNIQUE + ); + private final ClientJpaRepository clientJpaRepository; + private final ClientApiKeyJpaRepository clientApiKeyJpaRepository; + private final ClientEntityConverter clientEntityConverter; + private final UserApprovalJpaRepository userApprovalJpaRepository; + private final UserJpaRepository userJpaRepository; + private final SuperGroupJpaRepository superGroupJpaRepository; + + public ClientRepositoryAdapter(ClientJpaRepository clientJpaRepository, + ClientApiKeyJpaRepository clientApiKeyJpaRepository, + ClientEntityConverter clientEntityConverter, + UserApprovalJpaRepository userApprovalJpaRepository, + UserJpaRepository userJpaRepository, + SuperGroupJpaRepository superGroupJpaRepository) { + this.clientJpaRepository = clientJpaRepository; + this.clientApiKeyJpaRepository = clientApiKeyJpaRepository; + this.clientEntityConverter = clientEntityConverter; + this.userApprovalJpaRepository = userApprovalJpaRepository; + this.userJpaRepository = userJpaRepository; + this.superGroupJpaRepository = superGroupJpaRepository; + } + + @Override + public void save(Client client) { + try { + this.clientJpaRepository.saveAndFlush(toEntity(client)); + } catch (Exception e) { + PersistenceErrorState state = PersistenceErrorHelper.getState(e); + + if (state.equals(authorityNotFound)) { + throw new AuthorityNotFoundRuntimeException(); + } else if (state.equals(userNotFound)) { + throw new UserNotFoundRuntimeException(); + } else if (state.equals(clientIdAlreadyExists)) { + throw new ClientIdAlreadyExistsRuntimeException(); + } + + throw e; + } + } + + @Override + public void delete(ClientUid clientUid) throws ClientNotFoundException { + try { + this.clientJpaRepository.deleteById(clientUid.value()); + } catch (EmptyResultDataAccessException e) { + throw new ClientNotFoundException(); + } + } + + @Override + public List getAll() { + return this.clientJpaRepository.findAll() + .stream() + .map(clientEntityConverter::toDomain) + .toList(); + } + + @Override + public Optional get(ClientUid clientUid) { + return this.clientJpaRepository.findById(clientUid.value()) + .map(this.clientEntityConverter::toDomain); + } + + @Override + public Optional get(ClientId clientId) { + return this.clientJpaRepository.findByClientId(clientId.value()) + .map(this.clientEntityConverter::toDomain); + } + + @Override + public void addClientApproval(UserId userId, ClientUid clientUid) { + UserApprovalEntity userApprovalEntity = new UserApprovalEntity( + this.userJpaRepository.findById(userId.value()).orElseThrow(), + this.clientJpaRepository.findById(clientUid.value()).orElseThrow() + ); + this.userApprovalJpaRepository.save(userApprovalEntity); + } + + @Override + public boolean isApprovedByUser(UserId userId, ClientUid clientUid) { + return this.userApprovalJpaRepository.existsById_Client_ClientUidAndId_User_Id(clientUid.value(), userId.value()); + } + + @Override + public List getClientsByUserApproved(UserId id) { + return this.userApprovalJpaRepository.findAllById_User_Id(id.value()) + .stream() + .map(UserApprovalEntity::getClientEntity) + .map(this.clientEntityConverter::toDomain) + .toList(); + } + + @Override + public void deleteUserApproval(ClientUid clientUid, UserId userId) { + this.userApprovalJpaRepository.deleteById_Client_ClientUidAndId_User_Id(clientUid.value(), userId.value()); + } + + @Override + public Optional getByApiKey(ApiKeyToken apiKeyToken) { + return this.clientApiKeyJpaRepository + .findByApiKey_Token(apiKeyToken.value()) + .map(ClientApiKeyEntity::getClient) + .map(this.clientEntityConverter::toDomain); + } + + private ClientEntity toEntity(Client client) { + ClientEntity clientEntity = this.clientJpaRepository + .findById(client.clientUid().value()) + .orElse(new ClientEntity()); + + clientEntity.clientUid = client.clientUid().value(); + clientEntity.clientId = client.clientId().value(); + clientEntity.clientSecret = client.clientSecret().value(); + clientEntity.prettyName = client.prettyName().value(); + clientEntity.webServerRedirectUrl = client.clientRedirectUrl().value(); + + if (clientEntity.description == null) { + clientEntity.description = new TextEntity(); + } + + clientEntity.description.apply(client.description()); + + clientEntity.scopes.clear(); + clientEntity.scopes.addAll( + client.scopes() + .stream() + .map(scope -> new ClientScopeEntity( + clientEntity, + scope) + ).toList() + ); + + client.clientApiKey().ifPresent( + apiKey -> { + ApiKeyEntity apiKeyEntity = new ApiKeyEntity( + apiKey.id().value(), + apiKey.apiKeyToken().value(), + apiKey.prettyName().value(), + apiKey.keyType(), + new TextEntity(apiKey.description()) + ); + + clientEntity.clientsApiKey = new ClientApiKeyEntity(clientEntity, apiKeyEntity); + } + ); + + client.restrictions().ifPresent( + clientRestriction -> { + clientEntity.clientRestriction = new ClientRestrictionEntity(clientRestriction.id().value(), client.clientUid().value()); + + List clientRestrictionSuperGroupEntities = clientRestriction.superGroups() + .stream() + .map(superGroup -> new ClientRestrictionSuperGroupEntity( + clientEntity.clientRestriction, + this.superGroupJpaRepository.findById(superGroup.id().value()).orElseThrow() + ) + ).toList(); + + clientEntity.clientRestriction.setSuperGroupRestrictions(clientRestrictionSuperGroupEntities); + } + ); + + return clientEntity; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientScopeEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientScopeEntity.java new file mode 100644 index 000000000..7845e56c8 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientScopeEntity.java @@ -0,0 +1,31 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client; + +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import it.chalmers.gamma.app.client.domain.Scope; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "g_client_scope") +public class ClientScopeEntity extends ImmutableEntity { + + @EmbeddedId + private ClientScopePK id; + + protected ClientScopeEntity() { + } + + protected ClientScopeEntity(ClientEntity clientEntity, Scope scope) { + this.id = new ClientScopePK(clientEntity, scope); + } + + public ClientScopePK getId() { + return this.id; + } + + public Scope getScope() { + return this.id.getValue().scope(); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientScopePK.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientScopePK.java new file mode 100644 index 000000000..554cfaf5d --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientScopePK.java @@ -0,0 +1,39 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client; + +import it.chalmers.gamma.adapter.secondary.jpa.util.PKId; +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.client.domain.Scope; +import jakarta.persistence.*; + +@Embeddable +public class ClientScopePK extends PKId { + + @ManyToOne + @JoinColumn(name = "client_uid") + private ClientEntity client; + + @Column(name = "scope") + @Enumerated(EnumType.STRING) + private Scope scope; + + protected ClientScopePK() { + } + + protected ClientScopePK(ClientEntity clientEntity, Scope scope) { + this.client = clientEntity; + this.scope = scope; + } + + @Override + public ClientScopePKDTO getValue() { + return new ClientScopePKDTO( + new ClientUid(this.client.getId()), + this.scope + ); + } + + protected record ClientScopePKDTO(ClientUid clientUid, + Scope scope) { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/PostFK.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/PostFK.java new file mode 100644 index 000000000..cffc0a0d2 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/PostFK.java @@ -0,0 +1,36 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client; + +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntity; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntity; +import jakarta.persistence.Embeddable; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +@Embeddable +public class PostFK { + + @JoinColumn(name = "super_group_id") + @ManyToOne(fetch = FetchType.EAGER) + private SuperGroupEntity superGroupEntity; + + @JoinColumn(name = "post_id") + @ManyToOne(fetch = FetchType.EAGER) + private PostEntity postEntity; + + public PostFK() { + } + + public PostFK(SuperGroupEntity superGroupEntity, PostEntity postEntity) { + this.superGroupEntity = superGroupEntity; + this.postEntity = postEntity; + } + + public SuperGroupEntity getSuperGroupEntity() { + return superGroupEntity; + } + + public PostEntity getPostEntity() { + return postEntity; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityEntity.java new file mode 100644 index 000000000..969ebb4b8 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityEntity.java @@ -0,0 +1,53 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import jakarta.persistence.*; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Entity +@Table(name = "g_client_authority") +public class ClientAuthorityEntity extends ImmutableEntity { + + @OneToMany(mappedBy = "id.clientAuthority", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + protected Set postEntityList; + + @OneToMany(mappedBy = "id.clientAuthority", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + protected Set userEntityList; + + @OneToMany(mappedBy = "id.clientAuthority", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + protected Set superGroupEntityList; + + @EmbeddedId + private ClientAuthorityEntityPK id; + + protected ClientAuthorityEntity() { + } + + public ClientAuthorityEntity(ClientEntity client, String name) { + this.id = new ClientAuthorityEntityPK(client, name); + this.postEntityList = new HashSet<>(); + this.userEntityList = new HashSet<>(); + this.superGroupEntityList = new HashSet<>(); + } + + @Override + public ClientAuthorityEntityPK getId() { + return this.id; + } + + public List getPosts() { + return postEntityList.stream().toList(); + } + + public List getUsers() { + return userEntityList.stream().toList(); + } + + public List getSuperGroups() { + return superGroupEntityList.stream().toList(); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityEntityConverter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityEntityConverter.java new file mode 100644 index 000000000..a87fcfd2e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityEntityConverter.java @@ -0,0 +1,60 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntityConverter; +import it.chalmers.gamma.app.client.domain.authority.Authority; +import it.chalmers.gamma.app.client.domain.authority.AuthorityName; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +@Component +public class ClientAuthorityEntityConverter { + + private final ClientEntityConverter clientEntityConverter; + private final UserEntityConverter userEntityConverter; + private final SuperGroupEntityConverter superGroupEntityConverter; + private final PostEntityConverter postEntityConverter; + + public ClientAuthorityEntityConverter(ClientEntityConverter clientEntityConverter, + UserEntityConverter userEntityConverter, + SuperGroupEntityConverter superGroupEntityConverter, + PostEntityConverter postEntityConverter) { + this.clientEntityConverter = clientEntityConverter; + this.userEntityConverter = userEntityConverter; + this.superGroupEntityConverter = superGroupEntityConverter; + this.postEntityConverter = postEntityConverter; + } + + public Authority toDomain(ClientAuthorityEntity clientAuthorityEntity) { + Objects.requireNonNull(clientAuthorityEntity.getId()); + + var id = clientAuthorityEntity.getId(); + + return new Authority( + this.clientEntityConverter.toDomain(id.client), + AuthorityName.valueOf(id.name), + clientAuthorityEntity.getPosts() + .stream() + .map(authorityPostEntity -> new Authority.SuperGroupPost( + this.superGroupEntityConverter.toDomain(authorityPostEntity.getId().postFK.getSuperGroupEntity()), + this.postEntityConverter.toDomain(authorityPostEntity.getId().postFK.getPostEntity()) + )) + .toList(), + clientAuthorityEntity.getSuperGroups() + .stream() + .map(ClientAuthoritySuperGroupEntity::getSuperGroup) + .map(this.superGroupEntityConverter::toDomain) + .toList(), + clientAuthorityEntity.getUsers() + .stream() + .map(ClientAuthorityUserEntity::getUserEntity) + .map(this.userEntityConverter::toDomain) + .filter(Objects::nonNull) + .toList() + ); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityEntityPK.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityEntityPK.java new file mode 100644 index 000000000..9f25bef72 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityEntityPK.java @@ -0,0 +1,37 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.PKId; +import it.chalmers.gamma.app.client.domain.authority.AuthorityName; +import it.chalmers.gamma.app.client.domain.ClientUid; +import jakarta.persistence.*; + +@Embeddable +public class ClientAuthorityEntityPK extends PKId { + + @Column(name = "authority_name") + protected String name; + + @JoinColumn(name = "client_uid", columnDefinition = "uuid") + @ManyToOne(fetch = FetchType.EAGER) + protected ClientEntity client; + + protected ClientAuthorityEntityPK() { + } + + protected ClientAuthorityEntityPK(ClientEntity client, String name) { + this.client = client; + this.name = name; + } + + @Override + public AuthorityEntityPKRecord getValue() { + return new AuthorityEntityPKRecord( + new ClientUid(this.client.getId()), + new AuthorityName(this.name) + ); + } + + public record AuthorityEntityPKRecord(ClientUid clientUid, AuthorityName authorityName) { } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityJpaRepository.java new file mode 100644 index 000000000..5fc427831 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityJpaRepository.java @@ -0,0 +1,12 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface ClientAuthorityJpaRepository extends JpaRepository { + List findAllById_Client_Id(UUID clientUid); +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityPostEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityPostEntity.java new file mode 100644 index 000000000..2c7fe8a61 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityPostEntity.java @@ -0,0 +1,30 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntity; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "g_client_authority_post") +public class ClientAuthorityPostEntity extends ImmutableEntity { + + @EmbeddedId + protected ClientAuthorityPostPK id; + + protected ClientAuthorityPostEntity() { + } + + public ClientAuthorityPostEntity(SuperGroupEntity superGroup, PostEntity postEntity, ClientAuthorityEntity clientAuthorityEntity) { + this.id = new ClientAuthorityPostPK(superGroup, postEntity, clientAuthorityEntity); + } + + @Override + public ClientAuthorityPostPK getId() { + return this.id; + } + +} + diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityPostJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityPostJpaRepository.java new file mode 100644 index 000000000..e4c8886e5 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityPostJpaRepository.java @@ -0,0 +1,10 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface ClientAuthorityPostJpaRepository extends JpaRepository { } diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityPostPK.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityPostPK.java new file mode 100644 index 000000000..867dcbf6f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityPostPK.java @@ -0,0 +1,46 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import it.chalmers.gamma.adapter.secondary.jpa.client.PostFK; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntity; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.PKId; +import it.chalmers.gamma.app.client.domain.authority.AuthorityName; +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.post.domain.PostId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import jakarta.persistence.*; + +@Embeddable +public class ClientAuthorityPostPK extends PKId { + + @Embedded + protected PostFK postFK; + + @Embedded + protected ClientAuthorityEntityPK clientAuthority; + + protected ClientAuthorityPostPK() { + } + + public ClientAuthorityPostPK(SuperGroupEntity superGroupEntity, PostEntity postEntity, ClientAuthorityEntity clientAuthority) { + this.postFK = new PostFK(superGroupEntity, postEntity); + this.clientAuthority = clientAuthority.getId(); + } + + @Override + public AuthorityPostPKRecord getValue() { + return new AuthorityPostPKRecord( + new SuperGroupId(this.postFK.getSuperGroupEntity().getId()), + new PostId(this.postFK.getPostEntity().getId()), + new AuthorityName(this.clientAuthority.name), + new ClientUid(this.clientAuthority.client.getId()) + ); + } + + public record AuthorityPostPKRecord(SuperGroupId superGroupId, + PostId postId, + AuthorityName authorityName, + ClientUid clientUid) { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityRepositoryAdapter.java new file mode 100644 index 000000000..290753faf --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityRepositoryAdapter.java @@ -0,0 +1,173 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientJpaRepository; +import it.chalmers.gamma.adapter.secondary.jpa.group.MembershipJpaRepository; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntity; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostJpaRepository; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntity; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupJpaRepository; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntity; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserJpaRepository; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorHelper; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorState; +import it.chalmers.gamma.app.client.domain.authority.Authority; +import it.chalmers.gamma.app.client.domain.authority.AuthorityName; +import it.chalmers.gamma.app.client.domain.authority.ClientAuthorityRepository; +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.post.domain.Post; +import it.chalmers.gamma.app.supergroup.domain.SuperGroup; +import it.chalmers.gamma.app.user.domain.GammaUser; +import it.chalmers.gamma.app.user.domain.UserId; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Transactional +@Service +public class ClientAuthorityRepositoryAdapter implements ClientAuthorityRepository { + + private static final Logger LOGGER = LoggerFactory.getLogger(ClientAuthorityRepositoryAdapter.class); + private static final PersistenceErrorState notFoundError = new PersistenceErrorState(null, PersistenceErrorState.Type.FOREIGN_KEY_VIOLATION); + private final ClientAuthorityJpaRepository repository; + private final ClientAuthorityPostJpaRepository authorityPostRepository; + private final ClientAuthoritySuperGroupJpaRepository authoritySuperGroupRepository; + private final ClientAuthorityUserJpaRepository authorityUserRepository; + private final MembershipJpaRepository membershipJpaRepository; + private final UserJpaRepository userJpaRepository; + private final SuperGroupJpaRepository superGroupJpaRepository; + private final ClientJpaRepository clientJpaRepository; + private final PostJpaRepository postJpaRepository; + private final ClientAuthorityEntityConverter clientAuthorityEntityConverter; + private final SuperGroupEntityConverter superGroupEntityConverter; + + public ClientAuthorityRepositoryAdapter(ClientAuthorityJpaRepository repository, ClientAuthorityPostJpaRepository authorityPostRepository, ClientAuthoritySuperGroupJpaRepository authoritySuperGroupRepository, ClientAuthorityUserJpaRepository authorityUserRepository, MembershipJpaRepository membershipJpaRepository, UserJpaRepository userJpaRepository, SuperGroupJpaRepository superGroupJpaRepository, ClientJpaRepository clientJpaRepository, PostJpaRepository postJpaRepository, ClientAuthorityEntityConverter clientAuthorityEntityConverter, SuperGroupEntityConverter superGroupEntityConverter) { + this.repository = repository; + this.authorityPostRepository = authorityPostRepository; + this.authoritySuperGroupRepository = authoritySuperGroupRepository; + this.authorityUserRepository = authorityUserRepository; + this.membershipJpaRepository = membershipJpaRepository; + this.userJpaRepository = userJpaRepository; + this.superGroupJpaRepository = superGroupJpaRepository; + this.clientJpaRepository = clientJpaRepository; + this.postJpaRepository = postJpaRepository; + this.clientAuthorityEntityConverter = clientAuthorityEntityConverter; + this.superGroupEntityConverter = superGroupEntityConverter; + } + + @Override + public void create(ClientUid clientUid, AuthorityName authorityName) throws ClientAuthorityAlreadyExistsException { + if (repository.existsById(toAuthorityEntityPK(clientUid, authorityName))) { + throw new ClientAuthorityAlreadyExistsException(authorityName.value()); + } + + repository.saveAndFlush(new ClientAuthorityEntity(this.clientJpaRepository.findById(clientUid.value()).orElseThrow(), authorityName.getValue())); + } + + @Override + public void delete(ClientUid clientUid, AuthorityName authorityName) throws ClientAuthorityNotFoundException { + try { + repository.deleteById(toAuthorityEntityPK(clientUid, authorityName)); + } catch (EmptyResultDataAccessException e) { + throw new ClientAuthorityNotFoundException(); + } + } + + @Override + public void save(Authority authority) throws ClientAuthorityNotFoundRuntimeException { + ClientAuthorityEntity entity = toEntity(authority); + + try { + this.repository.saveAndFlush(entity); + } catch (Exception e) { + PersistenceErrorState state = PersistenceErrorHelper.getState(e); + + if (state.equals(notFoundError)) { + throw new NotCompleteClientAuthorityException(); + } + + throw e; + } + } + + @Override + public List getAllByClient(ClientUid clientUid) { + return this.repository.findAllById_Client_Id(clientUid.value()).stream().map(this.clientAuthorityEntityConverter::toDomain).toList(); + } + + @Override + public List getAllByUser(ClientUid clientUid, UserId userId) { + + throw new UnsupportedOperationException(); +// Set names = new HashSet<>(); +// +// this.authorityUserRepository.findAllById_UserEntity_Id(userId.value()).forEach(authorityUserEntity -> names.add(new UserAuthority(authorityUserEntity.getId().getValue().authorityName(), AuthorityType.AUTHORITY))); +// +// Set userSuperGroups = new HashSet<>(); +// +// this.membershipJpaRepository.findAllById_User_Id(userId.value()).forEach(membershipEntity -> { +// names.add(new UserAuthority(new AuthorityName(membershipEntity.getId().getGroup().getName()), AuthorityType.GROUP)); +// +// SuperGroupEntity superGroupEntity = membershipEntity.getId().getGroup().getSuperGroup(); +// userSuperGroups.add(this.superGroupEntityConverter.toDomain(superGroupEntity)); +// +// PostEntity postEntity = membershipEntity.getId().getPost(); +// +// this.authorityPostRepository.findAllById_SuperGroupEntity_Id_AndId_PostEntity_Id(superGroupEntity.getId(), postEntity.getId()).forEach(authorityPostEntity -> names.add(new UserAuthority(authorityPostEntity.getId().getValue().authorityName(), AuthorityType.AUTHORITY))); +// }); +// +// userSuperGroups.forEach(superGroup -> names.add(new UserAuthority(new AuthorityName(superGroup.name().value()), AuthorityType.SUPERGROUP))); +// +// userSuperGroups.forEach(superGroupId -> names.addAll(this.authoritySuperGroupRepository.findAllById_SuperGroupEntity_Id(superGroupId.id().value()).stream().map(AuthoritySuperGroupEntity::getId).map(AuthoritySuperGroupPK::getValue).map(AuthoritySuperGroupPK.AuthoritySuperGroupPKDTO::authorityName).map(clientAuthority -> new UserAuthority(clientAuthority, AuthorityType.AUTHORITY)).toList())); +// +// return new ArrayList<>(names); + } + + @Override + public Optional get(ClientUid clientUid, AuthorityName authorityName) { + return this.repository.findById(toAuthorityEntityPK(clientUid, authorityName)).map(this.clientAuthorityEntityConverter::toDomain); + } + + private ClientAuthorityEntity toEntity(Authority authority) throws ClientAuthorityNotFoundRuntimeException { + String name = authority.name().getValue(); + + ClientAuthorityEntity clientAuthorityEntity = this.repository.findById(new ClientAuthorityEntityPK(this.clientJpaRepository.findById(authority.client().clientUid().value()).orElseThrow(), authority.name().value())).orElseThrow(ClientAuthorityNotFoundRuntimeException::new); + + List users = authority.users().stream().map(user -> new ClientAuthorityUserEntity(toEntity(user), clientAuthorityEntity)).toList(); + List posts = authority.posts().stream().map(post -> new ClientAuthorityPostEntity(toEntity(post.superGroup()), toEntity(post.post()), clientAuthorityEntity)).toList(); + List superGroups = authority.superGroups().stream().map(superGroup -> new ClientAuthoritySuperGroupEntity(toEntity(superGroup), clientAuthorityEntity)).toList(); + + clientAuthorityEntity.postEntityList.clear(); + clientAuthorityEntity.postEntityList.addAll(posts); + + clientAuthorityEntity.userEntityList.clear(); + clientAuthorityEntity.userEntityList.addAll(users); + + clientAuthorityEntity.superGroupEntityList.clear(); + clientAuthorityEntity.superGroupEntityList.addAll(superGroups); + + return clientAuthorityEntity; + } + + + private UserEntity toEntity(GammaUser user) { + return this.userJpaRepository.getById(user.id().getValue()); + } + + private PostEntity toEntity(Post post) { + return this.postJpaRepository.getById(post.id().getValue()); + } + + private SuperGroupEntity toEntity(SuperGroup superGroup) { + return this.superGroupJpaRepository.getById(superGroup.id().getValue()); + } + + private ClientAuthorityEntityPK toAuthorityEntityPK(ClientUid clientUid, AuthorityName authorityName) { + return new ClientAuthorityEntityPK(this.clientJpaRepository.findById(clientUid.value()).orElseThrow(), authorityName.value()); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthoritySuperGroupEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthoritySuperGroupEntity.java new file mode 100644 index 000000000..33178349b --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthoritySuperGroupEntity.java @@ -0,0 +1,32 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "g_client_authority_super_group") +public class ClientAuthoritySuperGroupEntity extends ImmutableEntity { + + @EmbeddedId + protected ClientAuthoritySuperGroupPK id; + + protected ClientAuthoritySuperGroupEntity() { + } + + public ClientAuthoritySuperGroupEntity(SuperGroupEntity superGroup, ClientAuthorityEntity clientAuthorityEntity) { + this.id = new ClientAuthoritySuperGroupPK(superGroup, clientAuthorityEntity); + } + + @Override + public ClientAuthoritySuperGroupPK getId() { + return this.id; + } + + protected SuperGroupEntity getSuperGroup() { + return this.id.superGroupEntity; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthoritySuperGroupJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthoritySuperGroupJpaRepository.java new file mode 100644 index 000000000..c2e617110 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthoritySuperGroupJpaRepository.java @@ -0,0 +1,12 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface ClientAuthoritySuperGroupJpaRepository extends JpaRepository { + List findAllById_SuperGroupEntity_Id(UUID id); +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthoritySuperGroupPK.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthoritySuperGroupPK.java new file mode 100644 index 000000000..393c36853 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthoritySuperGroupPK.java @@ -0,0 +1,43 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.PKId; +import it.chalmers.gamma.app.client.domain.authority.AuthorityName; +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import jakarta.persistence.*; + +@Embeddable +public class ClientAuthoritySuperGroupPK extends PKId { + + @JoinColumn(name = "super_group_id") + @ManyToOne(fetch = FetchType.EAGER) + protected SuperGroupEntity superGroupEntity; + + @Embedded + protected ClientAuthorityEntityPK clientAuthority; + + protected ClientAuthoritySuperGroupPK() { + + } + + protected ClientAuthoritySuperGroupPK(SuperGroupEntity superGroupEntity, + ClientAuthorityEntity clientAuthority) { + this.superGroupEntity = superGroupEntity; + this.clientAuthority = clientAuthority.getId(); + } + + @Override + public AuthoritySuperGroupPKDTO getValue() { + return new AuthoritySuperGroupPKDTO( + new SuperGroupId(this.superGroupEntity.getId()), + new AuthorityName(this.clientAuthority.name), + new ClientUid(this.clientAuthority.client.getId()) + ); + } + + protected record AuthoritySuperGroupPKDTO(SuperGroupId superGroupId, + AuthorityName authorityName, + ClientUid clientUid) { + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityUserEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityUserEntity.java new file mode 100644 index 000000000..4f86384a8 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityUserEntity.java @@ -0,0 +1,33 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "g_client_authority_user") +public class ClientAuthorityUserEntity extends ImmutableEntity { + + @EmbeddedId + private ClientAuthorityUserPK id; + + protected ClientAuthorityUserEntity() { + + } + + public ClientAuthorityUserEntity(UserEntity user, ClientAuthorityEntity clientAuthorityEntity) { + this.id = new ClientAuthorityUserPK(user, clientAuthorityEntity); + } + + @Override + public ClientAuthorityUserPK getId() { + return this.id; + } + + public UserEntity getUserEntity() { + return this.id.getUserEntity(); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityUserJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityUserJpaRepository.java new file mode 100644 index 000000000..ac1d88124 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityUserJpaRepository.java @@ -0,0 +1,13 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface ClientAuthorityUserJpaRepository extends JpaRepository { + + List findAllById_UserEntity_Id(UUID userId); +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityUserPK.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityUserPK.java new file mode 100644 index 000000000..90e784de5 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/authority/ClientAuthorityUserPK.java @@ -0,0 +1,48 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.authority; + +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.PKId; +import it.chalmers.gamma.app.client.domain.authority.AuthorityName; +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.user.domain.UserId; +import jakarta.persistence.*; + +@Embeddable +public class ClientAuthorityUserPK extends PKId { + + @JoinColumn(name = "user_id") + @ManyToOne(fetch = FetchType.EAGER) + private UserEntity userEntity; + + @Embedded + protected ClientAuthorityEntityPK clientAuthority; + + protected ClientAuthorityUserPK() { + + } + + public ClientAuthorityUserPK(UserEntity userEntity, ClientAuthorityEntity clientAuthority) { + this.userEntity = userEntity; + this.clientAuthority = clientAuthority.getId(); + } + + @Override + public AuthorityUserPKRecord getValue() { + return new AuthorityUserPKRecord( + new UserId(this.userEntity.getId()), + new AuthorityName(this.clientAuthority.name), + new ClientUid(this.clientAuthority.client.getId()) + ); + } + + public UserEntity getUserEntity() { + return this.userEntity; + } + + protected record AuthorityUserPKRecord( + UserId userId, + AuthorityName authorityName, + ClientUid clientUid + ) { + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/restriction/ClientRestrictionEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/restriction/ClientRestrictionEntity.java new file mode 100644 index 000000000..1408bca35 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/restriction/ClientRestrictionEntity.java @@ -0,0 +1,48 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.restriction; + +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientEntity; +import it.chalmers.gamma.adapter.secondary.jpa.group.GroupEntity; +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "g_client_restriction") +public class ClientRestrictionEntity { + + @Column(name = "restriction_id", columnDefinition = "uuid") + protected UUID restrictionId; + + @Id + @Column(name = "client_uid", columnDefinition = "uuid") + protected UUID clientUid; + + @OneToOne + @PrimaryKeyJoinColumn(name = "client_uid") + protected ClientEntity client; + + @OneToMany(mappedBy = "id.clientRestriction", cascade = CascadeType.ALL, orphanRemoval = true) + protected List superGroupRestrictions; + + public ClientRestrictionEntity() { } + + public ClientRestrictionEntity(UUID restrictionId, UUID clientUid) { + this.restrictionId = restrictionId; + this.clientUid = clientUid; + this.superGroupRestrictions = new ArrayList<>(); + } + + public void setSuperGroupRestrictions(List superGroupRestrictions) { + this.superGroupRestrictions = superGroupRestrictions; + } + + public UUID getRestrictionId() { + return restrictionId; + } + + public List getSuperGroupRestrictions() { + return superGroupRestrictions; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/restriction/ClientRestrictionSuperGroupEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/restriction/ClientRestrictionSuperGroupEntity.java new file mode 100644 index 000000000..af83af76d --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/restriction/ClientRestrictionSuperGroupEntity.java @@ -0,0 +1,28 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.restriction; + +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientEntity; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "g_client_restriction_super_group") +public class ClientRestrictionSuperGroupEntity extends ImmutableEntity { + + @EmbeddedId + private ClientRestrictionSuperGroupPK id; + + protected ClientRestrictionSuperGroupEntity() { + } + + public ClientRestrictionSuperGroupEntity(ClientRestrictionEntity clientRestrictionEntity, SuperGroupEntity superGroupEntity) { + this.id = new ClientRestrictionSuperGroupPK(clientRestrictionEntity, superGroupEntity); + } + + public ClientRestrictionSuperGroupPK getId() { + return this.id; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/restriction/ClientRestrictionSuperGroupPK.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/restriction/ClientRestrictionSuperGroupPK.java new file mode 100644 index 000000000..6a8f1064c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/restriction/ClientRestrictionSuperGroupPK.java @@ -0,0 +1,41 @@ +package it.chalmers.gamma.adapter.secondary.jpa.client.restriction; + +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.PKId; +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.client.domain.restriction.ClientRestrictionId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import jakarta.persistence.*; + +@Embeddable +public class ClientRestrictionSuperGroupPK extends PKId { + + @ManyToOne + @JoinColumn(name = "restriction_id") + private ClientRestrictionEntity clientRestriction; + + @JoinColumn(name = "super_group_id") + @ManyToOne(fetch = FetchType.EAGER) + protected SuperGroupEntity superGroupEntity; + + protected ClientRestrictionSuperGroupPK() { + } + + protected ClientRestrictionSuperGroupPK(ClientRestrictionEntity clientRestrictionEntity, SuperGroupEntity superGroupEntity) { + this.clientRestriction = clientRestrictionEntity; + this.superGroupEntity = superGroupEntity; + } + + @Override + public ClientRestrictionPKDTO getValue() { + return new ClientRestrictionPKDTO( + new ClientRestrictionId(this.clientRestriction.restrictionId), + new SuperGroupId(this.superGroupEntity.getId()) + ); + } + + public record ClientRestrictionPKDTO(ClientRestrictionId clientRestrictionId, + SuperGroupId superGroupId) { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupEntity.java new file mode 100644 index 000000000..c7a677c63 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupEntity.java @@ -0,0 +1,54 @@ +package it.chalmers.gamma.adapter.secondary.jpa.group; + +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.MutableEntity; +import jakarta.persistence.*; + +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "g_group") +public class GroupEntity extends MutableEntity { + + @Id + @Column(name = "group_id", columnDefinition = "uuid") + protected UUID id; + + @Column(name = "e_name") + protected String name; + + @Column(name = "pretty_name") + protected String prettyName; + + @ManyToOne + @JoinColumn(name = "super_group_id") + protected SuperGroupEntity superGroup; + + @OneToMany(mappedBy = "id.group", cascade = CascadeType.ALL, orphanRemoval = true) + protected List members; + + @OneToOne(mappedBy = "group", cascade = CascadeType.ALL, orphanRemoval = true) + protected GroupImagesEntity groupImages; + + protected GroupEntity() { + } + + protected List getMembers() { + return members; + } + + @Override + public UUID getId() { + return this.id; + } + + public SuperGroupEntity getSuperGroup() { + return this.superGroup; + } + + public String getName() { + return this.name; + } + +} \ No newline at end of file diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupEntityConverter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupEntityConverter.java new file mode 100644 index 000000000..6720516e6 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupEntityConverter.java @@ -0,0 +1,72 @@ +package it.chalmers.gamma.adapter.secondary.jpa.group; + +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntityConverter; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.group.domain.Group; +import it.chalmers.gamma.app.group.domain.GroupId; +import it.chalmers.gamma.app.group.domain.GroupMember; +import it.chalmers.gamma.app.group.domain.UnofficialPostName; +import it.chalmers.gamma.app.image.domain.ImageUri; +import it.chalmers.gamma.app.user.domain.GammaUser; +import it.chalmers.gamma.app.user.domain.Name; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@Service +public class GroupEntityConverter { + + private final UserEntityConverter userEntityConverter; + private final PostEntityConverter postEntityConverter; + private final SuperGroupEntityConverter superGroupEntityConverter; + + public GroupEntityConverter(UserEntityConverter userEntityConverter, + PostEntityConverter postEntityConverter, + SuperGroupEntityConverter superGroupEntityConverter) { + this.userEntityConverter = userEntityConverter; + this.postEntityConverter = postEntityConverter; + this.superGroupEntityConverter = superGroupEntityConverter; + } + + public Group toDomain(GroupEntity entity) { + List members = entity.getMembers() + .stream() + .map(membershipEntity -> { + GammaUser user = this.userEntityConverter.toDomain(membershipEntity.getId().getUser()); + if (user == null) { + return null; + } + + return new GroupMember( + this.postEntityConverter.toDomain(membershipEntity.getId().getPost()), + new UnofficialPostName(membershipEntity.getUnofficialPostName()), + user + ); + }) + .filter(Objects::nonNull) + .toList(); + + Optional avatarUri = Optional.empty(); + Optional bannerUri = Optional.empty(); + + //TODO: Remove Optional from Group (and User) + if (entity.groupImages != null) { + avatarUri = Optional.ofNullable(entity.groupImages.avatarUri).map(ImageUri::new); + bannerUri = Optional.ofNullable(entity.groupImages.bannerUri).map(ImageUri::new); + } + + return new Group( + new GroupId(entity.getId()), + entity.getVersion(), + new Name(entity.name), + new PrettyName(entity.prettyName), this.superGroupEntityConverter.toDomain(entity.superGroup), + members, + avatarUri, + bannerUri + ); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupImagesEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupImagesEntity.java new file mode 100644 index 000000000..8c329b19c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupImagesEntity.java @@ -0,0 +1,35 @@ +package it.chalmers.gamma.adapter.secondary.jpa.group; + +import it.chalmers.gamma.adapter.secondary.jpa.util.MutableEntity; +import it.chalmers.gamma.app.group.domain.GroupId; +import jakarta.persistence.*; + +import java.util.UUID; + +@Entity +@Table(name = "g_group_images_uri") +public class GroupImagesEntity extends MutableEntity { + + @Id + @Column(name = "group_id", columnDefinition = "uuid") + protected UUID groupId; + + @OneToOne + @PrimaryKeyJoinColumn(name = "group_id") + protected GroupEntity group; + + @Column(name = "avatar_uri") + protected String avatarUri; + + @Column(name = "banner_uri") + protected String bannerUri; + + protected GroupImagesEntity() { + } + + @Override + public GroupId getId() { + return new GroupId(this.groupId); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupJpaRepository.java new file mode 100644 index 000000000..dcfab62bc --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupJpaRepository.java @@ -0,0 +1,10 @@ +package it.chalmers.gamma.adapter.secondary.jpa.group; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.UUID; + +public interface GroupJpaRepository extends JpaRepository { + List findAllBySuperGroupId(UUID id); +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupRepositoryAdapter.java new file mode 100644 index 000000000..6074fc1ed --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/GroupRepositoryAdapter.java @@ -0,0 +1,181 @@ +package it.chalmers.gamma.adapter.secondary.jpa.group; + +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupJpaRepository; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserJpaRepository; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorHelper; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorState; +import it.chalmers.gamma.app.group.domain.Group; +import it.chalmers.gamma.app.group.domain.GroupId; +import it.chalmers.gamma.app.group.domain.GroupRepository; +import it.chalmers.gamma.app.group.domain.UnofficialPostName; +import it.chalmers.gamma.app.image.domain.ImageUri; +import it.chalmers.gamma.app.post.domain.PostId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserMembership; +import jakarta.transaction.Transactional; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +@Transactional +public class GroupRepositoryAdapter implements GroupRepository { + + private static final PersistenceErrorState SUPER_GROUP_NOT_FOUND = new PersistenceErrorState( + "fkit_group_super_group_id_fkey", + PersistenceErrorState.Type.FOREIGN_KEY_VIOLATION + ); + private static final PersistenceErrorState GROUP_NAME_ALREADY_EXISTS = new PersistenceErrorState( + "fkit_group_e_name_key", + PersistenceErrorState.Type.NOT_UNIQUE + ); + private static final PersistenceErrorState USER_NOT_FOUND = new PersistenceErrorState( + "membership_user_id_fkey", + PersistenceErrorState.Type.FOREIGN_KEY_VIOLATION + ); + private static final PersistenceErrorState POST_NOT_FOUND = new PersistenceErrorState( + "membership_post_id_fkey", + PersistenceErrorState.Type.FOREIGN_KEY_VIOLATION + ); + private final GroupJpaRepository groupJpaRepository; + private final GroupEntityConverter groupEntityConverter; + private final MembershipJpaRepository membershipJpaRepository; + private final PostEntityConverter postEntityConverter; + private final SuperGroupJpaRepository superGroupJpaRepository; + private final PostJpaRepository postJpaRepository; + private final UserJpaRepository userJpaRepository; + + public GroupRepositoryAdapter(GroupJpaRepository groupJpaRepository, + GroupEntityConverter groupEntityConverter, + MembershipJpaRepository membershipJpaRepository, + PostEntityConverter postEntityConverter, + SuperGroupJpaRepository superGroupJpaRepository, + PostJpaRepository postJpaRepository, + UserJpaRepository userJpaRepository) { + this.groupJpaRepository = groupJpaRepository; + this.groupEntityConverter = groupEntityConverter; + this.membershipJpaRepository = membershipJpaRepository; + this.postEntityConverter = postEntityConverter; + this.superGroupJpaRepository = superGroupJpaRepository; + this.postJpaRepository = postJpaRepository; + this.userJpaRepository = userJpaRepository; + } + + @Override + public void save(Group group) throws GroupNameAlreadyExistsException { + try { + this.groupJpaRepository.saveAndFlush(toEntity(group)); + } catch (DataIntegrityViolationException e) { + PersistenceErrorState state = PersistenceErrorHelper.getState(e); + + if (state.equals(SUPER_GROUP_NOT_FOUND)) { + throw new SuperGroupNotFoundRuntimeException(); + } else if (state.equals(GROUP_NAME_ALREADY_EXISTS)) { + throw new GroupNameAlreadyExistsException(); + } else if (state.equals(USER_NOT_FOUND)) { + throw new UserNotFoundRuntimeException(); + } else if (state.equals(POST_NOT_FOUND)) { + throw new PostNotFoundRuntimeException(); + } + + throw e; + } + } + + @Override + public void delete(GroupId groupId) throws GroupNotFoundException { + try { + this.groupJpaRepository.deleteById(groupId.value()); + } catch (EmptyResultDataAccessException e) { + throw new GroupNotFoundException(); + } + } + + @Override + public List getAll() { + return this.groupJpaRepository.findAll() + .stream() + .map(this.groupEntityConverter::toDomain) + .toList(); + } + + @Override + public List getAllBySuperGroup(SuperGroupId superGroupId) { + return this.groupJpaRepository.findAllBySuperGroupId(superGroupId.value()) + .stream() + .map(this.groupEntityConverter::toDomain) + .toList(); + } + + @Override + public List getAllByPost(PostId postId) { + return this.membershipJpaRepository.findAllById_Post_Id(postId.value()) + .stream() + .map(membershipEntity -> membershipEntity.getId().getGroup()) + .map(this.groupEntityConverter::toDomain) + .distinct() + .toList(); + } + + @Override + public List getAllByUser(UserId userId) { + return this.membershipJpaRepository.findAllById_User_Id(userId.value()) + .stream() + .map(membershipEntity -> new UserMembership( + this.postEntityConverter.toDomain(membershipEntity.getId().getPost()), + this.groupEntityConverter.toDomain(membershipEntity.getId().getGroup()), + new UnofficialPostName(membershipEntity.getUnofficialPostName()) + )) + .toList(); + } + + @Override + public Optional get(GroupId groupId) { + return this.groupJpaRepository.findById(groupId.value()).map(this.groupEntityConverter::toDomain); + } + + + private GroupEntity toEntity(Group group) { + GroupEntity entity = this.groupJpaRepository.findById(group.id().value()) + .orElse(new GroupEntity()); + + entity.increaseVersion(group.version()); + + entity.id = group.id().getValue(); + entity.name = group.name().value(); + entity.prettyName = group.prettyName().value(); + entity.superGroup = superGroupJpaRepository.getById(group.superGroup().id().value()); + + if (entity.members == null) { + entity.members = new ArrayList<>(); + } + + entity.members.clear(); + entity.members.addAll(group.groupMembers() + .stream() + .map(groupMember -> new MembershipEntity( + new MembershipPK( + this.postJpaRepository.getById(groupMember.post().id().value()), + entity, + this.userJpaRepository.getById(groupMember.user().id().value())), + groupMember.unofficialPostName().value() + )).toList()); + + if (entity.groupImages == null) { + entity.groupImages = new GroupImagesEntity(); + } + + entity.groupImages.group = entity; + entity.groupImages.groupId = entity.id; + entity.groupImages.avatarUri = group.avatarUri().map(ImageUri::value).orElse(null); + entity.groupImages.bannerUri = group.bannerUri().map(ImageUri::value).orElse(null); + + return entity; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/MembershipEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/MembershipEntity.java new file mode 100644 index 000000000..c894fad27 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/MembershipEntity.java @@ -0,0 +1,36 @@ +package it.chalmers.gamma.adapter.secondary.jpa.group; + +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import jakarta.persistence.Column; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "g_membership") +public class MembershipEntity extends ImmutableEntity { + + @EmbeddedId + private MembershipPK id; + + @Column(name = "unofficial_post_name") + private String unofficialPostName; + + protected MembershipEntity() { + } + + protected MembershipEntity(MembershipPK id, String unofficialPostName) { + this.id = id; + this.unofficialPostName = unofficialPostName; + } + + @Override + public MembershipPK getId() { + return this.id; + } + + public String getUnofficialPostName() { + return unofficialPostName; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/MembershipJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/MembershipJpaRepository.java new file mode 100644 index 000000000..740795ef2 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/MembershipJpaRepository.java @@ -0,0 +1,18 @@ +package it.chalmers.gamma.adapter.secondary.jpa.group; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface MembershipJpaRepository extends JpaRepository { + List findAllById_Post_Id(UUID postId); + + List findAllById_User_Id(UUID userId); + + List findAllById_Group_Id(UUID groupId); + + List findAllById_GroupIdAndId_PostId(UUID groupId, UUID postId); +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/MembershipPK.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/MembershipPK.java new file mode 100644 index 000000000..519866052 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/MembershipPK.java @@ -0,0 +1,62 @@ +package it.chalmers.gamma.adapter.secondary.jpa.group; + +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.PKId; +import it.chalmers.gamma.app.group.domain.GroupId; +import it.chalmers.gamma.app.post.domain.PostId; +import it.chalmers.gamma.app.user.domain.UserId; +import jakarta.persistence.Embeddable; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +@Embeddable +public class MembershipPK extends PKId { + + @JoinColumn(name = "post_id") + @ManyToOne + private PostEntity post; + + @JoinColumn(name = "group_id") + @ManyToOne + private GroupEntity group; + + @JoinColumn(name = "user_id") + @ManyToOne + private UserEntity user; + + protected MembershipPK() { + + } + + public MembershipPK(PostEntity post, + GroupEntity group, + UserEntity user) { + this.post = post; + this.group = group; + this.user = user; + } + + public PostEntity getPost() { + return post; + } + + public GroupEntity getGroup() { + return group; + } + + public UserEntity getUser() { + return user; + } + + @Override + public MembershipPK.MembershipPKDTO getValue() { + return new MembershipPKDTO( + new PostId(this.post.getId()), + new GroupId(this.group.getId()), + new UserId(this.user.getId()) + ); + } + + public record MembershipPKDTO(PostId postId, GroupId groupId, UserId userId) { + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/PostEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/PostEntity.java new file mode 100644 index 000000000..3ad8aebe6 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/PostEntity.java @@ -0,0 +1,36 @@ +package it.chalmers.gamma.adapter.secondary.jpa.group; + +import it.chalmers.gamma.adapter.secondary.jpa.text.TextEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.MutableEntity; +import jakarta.persistence.*; + +import java.util.UUID; + +@Entity +@Table(name = "g_post") +public class PostEntity extends MutableEntity { + + @Id + @Column(name = "post_id", columnDefinition = "uuid") + protected UUID id; + + @JoinColumn(name = "post_name") + @OneToOne(cascade = CascadeType.ALL) + protected TextEntity postName; + + @Column(name = "email_prefix") + protected String emailPrefix; + + protected PostEntity() { + } + + protected PostEntity(UUID id) { + this.id = id; + } + + @Override + public UUID getId() { + return this.id; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/PostEntityConverter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/PostEntityConverter.java new file mode 100644 index 000000000..05bdfa76f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/PostEntityConverter.java @@ -0,0 +1,20 @@ +package it.chalmers.gamma.adapter.secondary.jpa.group; + +import it.chalmers.gamma.app.group.domain.EmailPrefix; +import it.chalmers.gamma.app.post.domain.Post; +import it.chalmers.gamma.app.post.domain.PostId; +import org.springframework.stereotype.Service; + +@Service +public class PostEntityConverter { + + public Post toDomain(PostEntity postEntity) { + return new Post( + new PostId(postEntity.id), + postEntity.getVersion(), + postEntity.postName.toDomain(), + new EmailPrefix(postEntity.emailPrefix) + ); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/PostJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/PostJpaRepository.java new file mode 100644 index 000000000..3291b4ad5 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/PostJpaRepository.java @@ -0,0 +1,11 @@ +package it.chalmers.gamma.adapter.secondary.jpa.group; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface PostJpaRepository extends JpaRepository { + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/PostRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/PostRepositoryAdapter.java new file mode 100644 index 000000000..61578b44f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/group/PostRepositoryAdapter.java @@ -0,0 +1,68 @@ +package it.chalmers.gamma.adapter.secondary.jpa.group; + +import it.chalmers.gamma.adapter.secondary.jpa.text.TextEntity; +import it.chalmers.gamma.app.post.domain.Post; +import it.chalmers.gamma.app.post.domain.PostId; +import it.chalmers.gamma.app.post.domain.PostRepository; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class PostRepositoryAdapter implements PostRepository { + + private final PostJpaRepository repository; + private final PostEntityConverter postEntityConverter; + + public PostRepositoryAdapter(PostJpaRepository repository, + PostEntityConverter postEntityConverter) { + this.repository = repository; + this.postEntityConverter = postEntityConverter; + } + + @Override + public void save(Post post) { + this.repository.saveAndFlush(toEntity(post)); + } + + @Override + public void delete(PostId postId) throws PostNotFoundException { + try { + this.repository.deleteById(postId.value()); + } catch (EmptyResultDataAccessException e) { + throw new PostNotFoundException(); + } + } + + @Override + public List getAll() { + return this.repository.findAll().stream().map(this.postEntityConverter::toDomain).toList(); + } + + @Override + public Optional get(PostId postId) { + return this.repository.findById(postId.value()).map(this.postEntityConverter::toDomain); + } + + private PostEntity toEntity(Post post) { + PostEntity postEntity = this.repository.findById(post.id().value()) + .orElse(new PostEntity()); + + postEntity.increaseVersion(post.version()); + + postEntity.id = post.id().value(); + postEntity.emailPrefix = post.emailPrefix().value(); + + if (postEntity.postName == null) { + postEntity.postName = new TextEntity(); + } + + postEntity.postName.apply(post.name()); + + return postEntity; + } + + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/package-info.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/package-info.java new file mode 100644 index 000000000..5f4c61128 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/package-info.java @@ -0,0 +1,4 @@ +/** + * Adapter handling the saving of entities in the database using jpa + */ +package it.chalmers.gamma.adapter.secondary.jpa; \ No newline at end of file diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsEntity.java new file mode 100644 index 000000000..f832bd636 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsEntity.java @@ -0,0 +1,46 @@ +package it.chalmers.gamma.adapter.secondary.jpa.settings; + +import it.chalmers.gamma.adapter.secondary.jpa.util.MutableEntity; +import it.chalmers.gamma.app.settings.domain.Settings; +import it.chalmers.gamma.app.settings.domain.SettingsId; +import jakarta.persistence.*; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "g_settings") +public class SettingsEntity extends MutableEntity { + + @Column(name = "updated_at") + protected Instant updatedAt; + @Column(name = "last_updated_user_agreement") + protected Instant lastUpdatedUserAgreement; + @OneToMany(fetch = FetchType.EAGER, mappedBy = "id.settings", cascade = CascadeType.ALL, orphanRemoval = true) + protected List infoSuperGroupTypeEntities; + @Id + @Column(name = "id", columnDefinition = "uuid") + private UUID id; + + protected SettingsEntity() { + this.id = UUID.randomUUID(); + this.infoSuperGroupTypeEntities = new ArrayList<>(); + } + + @Override + public SettingsId getId() { + return new SettingsId(this.id); + } + + public Settings toDomain() { + return new Settings( + this.lastUpdatedUserAgreement, + this.infoSuperGroupTypeEntities + .stream() + .map(SettingsInfoSuperGroupTypeEntity::getSuperGroupType) + .toList() + ); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsInfoSuperGroupTypeEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsInfoSuperGroupTypeEntity.java new file mode 100644 index 000000000..588928fb1 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsInfoSuperGroupTypeEntity.java @@ -0,0 +1,33 @@ +package it.chalmers.gamma.adapter.secondary.jpa.settings; + +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupTypeEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupType; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "g_settings_info_api_super_group_types") +public class SettingsInfoSuperGroupTypeEntity extends ImmutableEntity { + + @EmbeddedId + private SettingsInfoSuperGroupTypePK id; + + protected SettingsInfoSuperGroupTypeEntity() { + } + + protected SettingsInfoSuperGroupTypeEntity(SettingsEntity settingsEntity, SuperGroupTypeEntity superGroupTypeEntity) { + this.id = new SettingsInfoSuperGroupTypePK(settingsEntity, superGroupTypeEntity); + } + + @Override + public SettingsInfoSuperGroupTypePK getId() { + return this.id; + } + + public SuperGroupType getSuperGroupType() { + return this.id.getValue().superGroupType(); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsInfoSuperGroupTypePK.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsInfoSuperGroupTypePK.java new file mode 100644 index 000000000..9d59de92f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsInfoSuperGroupTypePK.java @@ -0,0 +1,40 @@ +package it.chalmers.gamma.adapter.secondary.jpa.settings; + +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupTypeEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.PKId; +import it.chalmers.gamma.app.settings.domain.SettingsId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupType; +import jakarta.persistence.*; + +@Embeddable +public class SettingsInfoSuperGroupTypePK extends PKId { + + @ManyToOne + @JoinColumn(name = "settings_id") + private SettingsEntity settings; + + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "super_group_type_name") + private SuperGroupTypeEntity superGroupType; + + protected SettingsInfoSuperGroupTypePK() { + } + + protected SettingsInfoSuperGroupTypePK(SettingsEntity settingsEntity, SuperGroupTypeEntity superGroupTypeEntity) { + this.settings = settingsEntity; + this.superGroupType = superGroupTypeEntity; + } + + @Override + public SettingsInfoSuperGroupTypePKDTO getValue() { + return new SettingsInfoSuperGroupTypePKDTO( + settings.getId(), + new SuperGroupType(superGroupType.getId()) + ); + } + + protected record SettingsInfoSuperGroupTypePKDTO(SettingsId settingsId, SuperGroupType superGroupType) { + } + + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsJpaRepository.java new file mode 100644 index 000000000..0a5903e7f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsJpaRepository.java @@ -0,0 +1,13 @@ +package it.chalmers.gamma.adapter.secondary.jpa.settings; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SettingsJpaRepository extends JpaRepository { + + Optional findTopByOrderByVersionDesc(); + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsRepositoryAdapter.java new file mode 100644 index 000000000..60d4f7dd7 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/settings/SettingsRepositoryAdapter.java @@ -0,0 +1,80 @@ +package it.chalmers.gamma.adapter.secondary.jpa.settings; + +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupTypeEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorHelper; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorState; +import it.chalmers.gamma.app.settings.domain.Settings; +import it.chalmers.gamma.app.settings.domain.SettingsRepository; +import org.springframework.stereotype.Service; + +import java.time.Instant; + +@Service +public class SettingsRepositoryAdapter implements SettingsRepository { + + private static final PersistenceErrorState superGroupTypeNotFound = new PersistenceErrorState( + null, + PersistenceErrorState.Type.FOREIGN_KEY_VIOLATION + ); + private final SettingsJpaRepository repository; + + public SettingsRepositoryAdapter(SettingsJpaRepository repository) { + this.repository = repository; + } + + @Override + public boolean hasSettings() { + return this.repository.findTopByOrderByVersionDesc().isPresent(); + } + + /** + * Assumes that there's always one settings entity available. + */ + @Override + public Settings getSettings() { + SettingsEntity settingsEntity = repository.findTopByOrderByVersionDesc() + .orElseThrow(IllegalStateException::new); + return settingsEntity.toDomain(); + } + + @Override + public void setSettings(UpdateSettings updateSettings) { + setSettings(updateSettings.updateSettings(getSettings())); + } + + @Override + public void setSettings(Settings settings) { + try { + this.repository.saveAndFlush(toEntity(settings)); + } catch (Exception e) { + PersistenceErrorState state = PersistenceErrorHelper.getState(e); + + if (state.equals(superGroupTypeNotFound)) { + throw new IllegalArgumentException(); + } + + throw e; + } + } + + private SettingsEntity toEntity(Settings settings) { + SettingsEntity settingsEntity = this.repository.findTopByOrderByVersionDesc() + .orElse(new SettingsEntity()); + + settingsEntity.updatedAt = Instant.now(); + settingsEntity.lastUpdatedUserAgreement = settings.lastUpdatedUserAgreement(); + settingsEntity.infoSuperGroupTypeEntities.clear(); + settingsEntity.infoSuperGroupTypeEntities.addAll( + settings.infoSuperGroupTypes() + .stream() + .map(superGroupType -> new SettingsInfoSuperGroupTypeEntity( + settingsEntity, + new SuperGroupTypeEntity(superGroupType) + )) + .toList() + ); + + return settingsEntity; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupEntity.java new file mode 100644 index 000000000..ec91b1eb7 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupEntity.java @@ -0,0 +1,43 @@ +package it.chalmers.gamma.adapter.secondary.jpa.supergroup; + +import it.chalmers.gamma.adapter.secondary.jpa.text.TextEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.MutableEntity; +import jakarta.persistence.*; + +import java.util.UUID; + + +@Entity +@Table(name = "g_super_group") +public class SuperGroupEntity extends MutableEntity { + + @Id + @Column(name = "super_group_id", columnDefinition = "uuid") + protected UUID id; + + @JoinColumn(name = "description") + @OneToOne(cascade = CascadeType.ALL) + protected TextEntity description; + + @Column(name = "e_name") + protected String name; + + @Column(name = "pretty_name") + protected String prettyName; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "super_group_type_name") + protected SuperGroupTypeEntity superGroupType; + + protected SuperGroupEntity() { + } + + protected SuperGroupEntity(UUID superGroupId) { + this.id = superGroupId; + } + + @Override + public UUID getId() { + return this.id; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupEntityConverter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupEntityConverter.java new file mode 100644 index 000000000..72667c405 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupEntityConverter.java @@ -0,0 +1,30 @@ +package it.chalmers.gamma.adapter.secondary.jpa.supergroup; + +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.supergroup.domain.SuperGroup; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupType; +import it.chalmers.gamma.app.user.domain.Name; +import org.springframework.stereotype.Service; + +@Service +public class SuperGroupEntityConverter { + + public final SuperGroupJpaRepository superGroupJpaRepository; + + public SuperGroupEntityConverter(SuperGroupJpaRepository superGroupJpaRepository) { + this.superGroupJpaRepository = superGroupJpaRepository; + } + + public SuperGroup toDomain(SuperGroupEntity entity) { + return new SuperGroup( + new SuperGroupId(entity.id), + entity.getVersion(), + new Name(entity.name), + new PrettyName(entity.prettyName), + new SuperGroupType(entity.superGroupType.getId()), + entity.description.toDomain() + ); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupJpaRepository.java new file mode 100644 index 000000000..d7285263f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupJpaRepository.java @@ -0,0 +1,14 @@ +package it.chalmers.gamma.adapter.secondary.jpa.supergroup; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface SuperGroupJpaRepository extends JpaRepository { + + List findAllBySuperGroupType_Name(String type); + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupRepositoryAdapter.java new file mode 100644 index 000000000..28c59a9c2 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupRepositoryAdapter.java @@ -0,0 +1,117 @@ +package it.chalmers.gamma.adapter.secondary.jpa.supergroup; + +import it.chalmers.gamma.adapter.secondary.jpa.text.TextEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorHelper; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorState; +import it.chalmers.gamma.app.supergroup.domain.SuperGroup; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupRepository; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupType; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class SuperGroupRepositoryAdapter implements SuperGroupRepository { + + private static final PersistenceErrorState typeNotFound = new PersistenceErrorState( + "fkit_super_group_super_group_type_name_fkey", + PersistenceErrorState.Type.FOREIGN_KEY_VIOLATION + ); + private static final PersistenceErrorState nameAlreadyExists = new PersistenceErrorState( + "fkit_super_group_e_name_key", + PersistenceErrorState.Type.NOT_UNIQUE + ); + private static final PersistenceErrorState superGroupIsUsed = new PersistenceErrorState( + "fkit_group_super_group_id_fkey", + PersistenceErrorState.Type.FOREIGN_KEY_VIOLATION + ); + private final SuperGroupJpaRepository repository; + private final SuperGroupTypeJpaRepository superGroupTypeJpaRepository; + private final SuperGroupEntityConverter superGroupEntityConverter; + + public SuperGroupRepositoryAdapter(SuperGroupJpaRepository repository, + SuperGroupTypeJpaRepository superGroupTypeJpaRepository, + SuperGroupEntityConverter superGroupEntityConverter) { + this.repository = repository; + this.superGroupTypeJpaRepository = superGroupTypeJpaRepository; + this.superGroupEntityConverter = superGroupEntityConverter; + } + + @Override + public void save(SuperGroup superGroup) { + try { + this.repository.saveAndFlush(toEntity(superGroup)); + } catch (DataIntegrityViolationException e) { + PersistenceErrorState state = PersistenceErrorHelper.getState(e); + + if (state.equals(typeNotFound)) { + throw new TypeNotFoundRuntimeException(); + } else if (state.equals(nameAlreadyExists)) { + throw new NameAlreadyExistsRuntimeException(); + } + + throw e; + } + } + + @Override + public void delete(SuperGroupId superGroupId) throws SuperGroupNotFoundException, SuperGroupIsUsedException { + try { + this.repository.deleteById(superGroupId.value()); + } catch (EmptyResultDataAccessException e) { + throw new SuperGroupNotFoundException(); + } catch (Exception e) { + PersistenceErrorState state = PersistenceErrorHelper.getState(e); + + if (state.equals(superGroupIsUsed)) { + throw new SuperGroupIsUsedException(); + } + + throw e; + } + } + + @Override + public List getAll() { + return this.repository.findAll().stream().map(this.superGroupEntityConverter::toDomain).toList(); + } + + @Override + public List getAllByType(SuperGroupType superGroupType) { + return this.repository.findAllBySuperGroupType_Name(superGroupType.value()) + .stream() + .map(this.superGroupEntityConverter::toDomain) + .toList(); + } + + @Override + public Optional get(SuperGroupId superGroupId) { + return this.repository.findById(superGroupId.value()).map(this.superGroupEntityConverter::toDomain); + } + + private SuperGroupEntity toEntity(SuperGroup superGroup) { + SuperGroupEntity superGroupEntity = this.repository.findById(superGroup.id().value()) + .orElse(new SuperGroupEntity()); + + superGroupEntity.increaseVersion(superGroup.version()); + + superGroupEntity.id = superGroup.id().value(); + superGroupEntity.superGroupType = this.superGroupTypeJpaRepository.getById(superGroup.type().value()); + superGroupEntity.name = superGroup.name().value(); + superGroupEntity.prettyName = superGroup.prettyName().value(); + + if (superGroupEntity.description == null) { + superGroupEntity.description = new TextEntity(); + } + + superGroupEntity.description.apply(superGroup.description()); + + return superGroupEntity; + } + + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupTypeEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupTypeEntity.java new file mode 100644 index 000000000..2882bb37c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupTypeEntity.java @@ -0,0 +1,29 @@ +package it.chalmers.gamma.adapter.secondary.jpa.supergroup; + +import it.chalmers.gamma.adapter.secondary.jpa.util.AbstractEntity; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "g_super_group_type") +public class SuperGroupTypeEntity extends AbstractEntity { + + @Id + @Column(name = "super_group_type_name") + private String name; + + protected SuperGroupTypeEntity() { + } + + public SuperGroupTypeEntity(SuperGroupType superGroupType) { + this.name = superGroupType.getValue(); + } + + @Override + public String getId() { + return this.name; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupTypeJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupTypeJpaRepository.java new file mode 100644 index 000000000..65b18772d --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupTypeJpaRepository.java @@ -0,0 +1,6 @@ +package it.chalmers.gamma.adapter.secondary.jpa.supergroup; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SuperGroupTypeJpaRepository extends JpaRepository { +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupTypeRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupTypeRepositoryAdapter.java new file mode 100644 index 000000000..238ca38b5 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/supergroup/SuperGroupTypeRepositoryAdapter.java @@ -0,0 +1,62 @@ +package it.chalmers.gamma.adapter.secondary.jpa.supergroup; + +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorHelper; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorState; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupType; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupTypeRepository; +import jakarta.transaction.Transactional; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class SuperGroupTypeRepositoryAdapter implements SuperGroupTypeRepository { + + private static final PersistenceErrorState typeIsUsed = new PersistenceErrorState( + "fkit_super_group_super_group_type_name_fkey", + PersistenceErrorState.Type.FOREIGN_KEY_VIOLATION + ); + private final SuperGroupTypeJpaRepository repository; + + public SuperGroupTypeRepositoryAdapter(SuperGroupTypeJpaRepository repository) { + this.repository = repository; + } + + @Transactional + @Override + public void add(SuperGroupType superGroupType) throws SuperGroupTypeAlreadyExistsException { + if (this.repository.existsById(superGroupType.value())) { + throw new SuperGroupTypeAlreadyExistsException(superGroupType.value()); + } + + this.repository.saveAndFlush(new SuperGroupTypeEntity(superGroupType)); + } + + @Override + public void delete(SuperGroupType superGroupType) throws SuperGroupTypeNotFoundException, SuperGroupTypeHasUsagesException { + try { + this.repository.deleteById(superGroupType.value()); + } catch (EmptyResultDataAccessException e) { + throw new SuperGroupTypeNotFoundException(); + } catch (DataIntegrityViolationException e) { + PersistenceErrorState state = PersistenceErrorHelper.getState(e); + + if (state.equals(typeIsUsed)) { + throw new SuperGroupTypeHasUsagesException(); + } + + throw e; + } + } + + @Override + public List getAll() { + return this.repository.findAll() + .stream() + .map(SuperGroupTypeEntity::getId) + .map(SuperGroupType::new) + .toList(); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/text/TextEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/text/TextEntity.java new file mode 100644 index 000000000..f0516458e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/text/TextEntity.java @@ -0,0 +1,57 @@ +package it.chalmers.gamma.adapter.secondary.jpa.text; + +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import it.chalmers.gamma.app.common.Text; +import it.chalmers.gamma.app.common.TextId; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.util.UUID; + +@Entity +@Table(name = "g_text") +public class TextEntity extends ImmutableEntity { + + @Id + @Column(name = "text_id", columnDefinition = "uuid") + private final UUID id; + + @Column(name = "sv") + private String sv; + + @Column(name = "en") + private String en; + + public TextEntity() { + this(TextId.generate().getValue(), null, null); + } + + public TextEntity(UUID id, String sv, String en) { + this.id = id; + this.sv = sv; + this.en = en; + } + + public TextEntity(Text text) { + this(TextId.generate().getValue(), text.sv().value(), text.en().value()); + } + + public Text toDomain() { + return new Text( + this.sv, + this.en + ); + } + + @Override + public TextId getId() { + return new TextId(this.id); + } + + public void apply(Text newText) { + this.sv = newText.sv().value(); + this.en = newText.en().value(); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/TrustedUserDetailsRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/TrustedUserDetailsRepository.java new file mode 100644 index 000000000..5ffb65f52 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/TrustedUserDetailsRepository.java @@ -0,0 +1,85 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.image.domain.ImageUri; +import it.chalmers.gamma.app.settings.domain.Settings; +import it.chalmers.gamma.app.settings.domain.SettingsRepository; +import it.chalmers.gamma.app.user.domain.*; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.time.Instant; +import java.util.Collections; +import java.util.Optional; +import java.util.UUID; + +public class TrustedUserDetailsRepository implements UserDetailsService { + + private final UserJpaRepository userJpaRepository; + private final SettingsRepository settingsRepository; + + public TrustedUserDetailsRepository(UserJpaRepository userJpaRepository, SettingsRepository settingsRepository) { + this.userJpaRepository = userJpaRepository; + this.settingsRepository = settingsRepository; + } + + public GammaUser getGammaUserByUser() { + User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + UserEntity userEntity = this.userJpaRepository.findById(UUID.fromString(user.getUsername())).orElseThrow(() -> new UsernameNotFoundException("User not found")); + + Settings settings = this.settingsRepository.getSettings(); + boolean acceptedUserAgreement = hasAcceptedLatestUserAgreement(userEntity.userAgreementAccepted, settings); + + return new GammaUser( + new UserId(UUID.fromString(user.getUsername())), + new Cid(userEntity.cid), + new Nick(userEntity.nick), + new FirstName(userEntity.firstName), + new LastName(userEntity.lastName), + new AcceptanceYear(userEntity.acceptanceYear), + userEntity.language, + new UserExtended( + new Email(userEntity.email), + userEntity.getVersion(), + acceptedUserAgreement, + userEntity.locked, + userEntity.userAvatar == null ? null : new ImageUri(userEntity.userAvatar.avatarUri) + ) + ); + } + + @Override + public UserDetails loadUserByUsername(String userIdentifier) throws UsernameNotFoundException { + UserEntity userEntity = getUserByUsernameOrEmail(userIdentifier) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + return new User( + userEntity.id.toString(), + userEntity.password, + true, + true, + true, + !userEntity.locked, + // Authorities will be loaded by UpdateUserPrincipalFilter + Collections.emptyList() + ); + } + + private Optional getUserByUsernameOrEmail(String userIdentifier) { + Optional userEntity = this.userJpaRepository.findByCid(userIdentifier); + if (userEntity.isEmpty()) { + userEntity = this.userJpaRepository.findByEmail(userIdentifier); + } + + return userEntity; + } + + private boolean hasAcceptedLatestUserAgreement(Instant acceptedUserAgreement, Settings settings) { + return settings.lastUpdatedUserAgreement().compareTo(acceptedUserAgreement) < 0; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationEntity.java new file mode 100644 index 000000000..fcf295d33 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationEntity.java @@ -0,0 +1,57 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import it.chalmers.gamma.app.user.activation.domain.UserActivation; +import it.chalmers.gamma.app.user.activation.domain.UserActivationToken; +import it.chalmers.gamma.app.user.domain.Cid; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.time.Instant; + +@Entity +@Table(name = "g_user_activation") +public class UserActivationEntity extends ImmutableEntity { + + @Id + @Column(name = "cid") + private String cid; + + @Column(name = "token") + private String token; + + @Column(name = "created_at") + private Instant createdAt; + + protected UserActivationEntity() { + } + + protected UserActivationEntity(Cid cid) { + this.createdAt = Instant.now(); + this.cid = cid.getValue(); + } + + public void setToken(UserActivationToken token) { + this.token = token.value(); + } + + public UserActivation toDomain() { + return new UserActivation( + Cid.valueOf(this.cid), + new UserActivationToken(this.token), + this.createdAt + ); + } + + @Override + public String getId() { + return this.cid; + } + + public Cid cid() { + return Cid.valueOf(this.cid); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationJpaRepository.java new file mode 100644 index 000000000..a22fab4b1 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationJpaRepository.java @@ -0,0 +1,13 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserActivationJpaRepository extends JpaRepository { + + Optional findByToken(String token); + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationRepositoryAdapter.java new file mode 100644 index 000000000..49d7f0b8f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationRepositoryAdapter.java @@ -0,0 +1,74 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorHelper; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorState; +import it.chalmers.gamma.app.user.activation.domain.UserActivation; +import it.chalmers.gamma.app.user.activation.domain.UserActivationRepository; +import it.chalmers.gamma.app.user.activation.domain.UserActivationToken; +import it.chalmers.gamma.app.user.domain.Cid; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class UserActivationRepositoryAdapter implements UserActivationRepository { + + private static final PersistenceErrorState cidNotAllowed = new PersistenceErrorState( + "user_activation_cid_fkey", + PersistenceErrorState.Type.FOREIGN_KEY_VIOLATION + ); + private final UserActivationJpaRepository userActivationJpaRepository; + + public UserActivationRepositoryAdapter(UserActivationJpaRepository userActivationJpaRepository) { + this.userActivationJpaRepository = userActivationJpaRepository; + } + + @Override + public UserActivationToken createActivationToken(Cid cid) throws CidNotAllowedException { + UserActivationEntity entity = this.userActivationJpaRepository.findById(cid.value()) + .orElse(new UserActivationEntity(cid)); + + UserActivationToken token = UserActivationToken.generate(); + entity.setToken(token); + + try { + this.userActivationJpaRepository.saveAndFlush(entity); + return token; + } catch (DataIntegrityViolationException e) { + PersistenceErrorState state = PersistenceErrorHelper.getState(e); + + if (state.equals(cidNotAllowed)) { + throw new CidNotAllowedException(); + } + + throw e; + } + } + + @Override + public Optional get(Cid cid) { + return this.userActivationJpaRepository.findById(cid.value()) + .map(UserActivationEntity::toDomain); + } + + @Override + public List getAll() { + return this.userActivationJpaRepository.findAll() + .stream() + .map(UserActivationEntity::toDomain) + .toList(); + } + + @Override + public Cid getByToken(UserActivationToken token) { + return this.userActivationJpaRepository.findByToken(token.value()) + .orElseThrow(TokenNotActivatedException::new).cid(); + } + + @Override + public void removeActivation(Cid cid) throws CidNotActivatedException { + this.userActivationJpaRepository.deleteById(cid.value()); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserApprovalEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserApprovalEntity.java new file mode 100644 index 000000000..63079b410 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserApprovalEntity.java @@ -0,0 +1,36 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "g_user_approval") +public class UserApprovalEntity extends ImmutableEntity { + + @EmbeddedId + private UserApprovalEntityPK id; + + public UserApprovalEntity() { + } + + public UserApprovalEntity(UserEntity user, ClientEntity client) { + this.id = new UserApprovalEntityPK(user, client); + } + + @Override + public UserApprovalEntityPK getId() { + return this.id; + } + + public UserEntity getUserEntity() { + return this.id.getUserEntity(); + } + + public ClientEntity getClientEntity() { + return this.id.getClientEntity(); + } +} + diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserApprovalEntityPK.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserApprovalEntityPK.java new file mode 100644 index 000000000..aeb87c56e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserApprovalEntityPK.java @@ -0,0 +1,49 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.PKId; +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.user.domain.UserId; +import jakarta.persistence.Embeddable; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +@Embeddable +public class UserApprovalEntityPK extends PKId { + + @ManyToOne + @JoinColumn(name = "user_id") + private UserEntity user; + @ManyToOne + @JoinColumn(name = "client_uid") + private ClientEntity client; + + protected UserApprovalEntityPK() { + } + + public UserApprovalEntityPK(UserEntity user, ClientEntity client) { + this.user = user; + this.client = client; + } + + public UserEntity getUserEntity() { + return this.user; + } + + public ClientEntity getClientEntity() { + return this.client; + } + + @Override + public UserApprovalPKDTO getValue() { + return new UserApprovalPKDTO( + new UserId(this.user.getId()), + new ClientUid(this.client.getId()) + ); + } + + protected record UserApprovalPKDTO(UserId userId, ClientUid clientUid) { + + + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserApprovalJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserApprovalJpaRepository.java new file mode 100644 index 000000000..baebf3eab --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserApprovalJpaRepository.java @@ -0,0 +1,17 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.UUID; + +public interface UserApprovalJpaRepository extends JpaRepository { + List findAllById_User_Id(UUID id); + + List findAllById_Client_ClientUid(UUID id); + List findAllById_Client_ClientId(String id); + + boolean existsById_Client_ClientUidAndId_User_Id(UUID clientUid, UUID userId); + + void deleteById_Client_ClientUidAndId_User_Id(UUID clientUid, UUID userId); +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserAvatarEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserAvatarEntity.java new file mode 100644 index 000000000..aa6693d1b --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserAvatarEntity.java @@ -0,0 +1,31 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import it.chalmers.gamma.adapter.secondary.jpa.util.MutableEntity; +import jakarta.persistence.*; + +import java.util.UUID; + +@Entity +@Table(name = "g_user_avatar_uri") +public class UserAvatarEntity extends MutableEntity { + + @Id + @Column(name = "user_id", columnDefinition = "uuid") + protected UUID userId; + + @OneToOne + @PrimaryKeyJoinColumn(name = "user_id") + protected UserEntity user; + + @Column(name = "avatar_uri") + protected String avatarUri; + + protected UserAvatarEntity() { + } + + @Override + public UUID getId() { + return this.userId; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserAvatarJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserAvatarJpaRepository.java new file mode 100644 index 000000000..8ad1a2ea1 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserAvatarJpaRepository.java @@ -0,0 +1,11 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import it.chalmers.gamma.adapter.secondary.jpa.user.UserAvatarEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface UserAvatarJpaRepository extends JpaRepository { +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserAvatarRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserAvatarRepositoryAdapter.java new file mode 100644 index 000000000..3d86afdb8 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserAvatarRepositoryAdapter.java @@ -0,0 +1,29 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import it.chalmers.gamma.app.image.domain.ImageUri; +import it.chalmers.gamma.app.image.domain.UserAvatarRepository; +import it.chalmers.gamma.app.user.domain.UserId; +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.UUID; + +@Service +public class UserAvatarRepositoryAdapter implements UserAvatarRepository { + + private final UserAvatarJpaRepository userAvatarJpaRepository; + + public UserAvatarRepositoryAdapter(UserAvatarJpaRepository userAvatarJpaRepository) { + this.userAvatarJpaRepository = userAvatarJpaRepository; + } + + @Override + public Optional getAvatarUri(UserId userId) { + return this.userAvatarJpaRepository.findById(userId.value()).map(userAvatarEntity -> new ImageUri(userAvatarEntity.avatarUri)); + } + + @Override + public void removeAvatarUri(UUID userId) { + this.userAvatarJpaRepository.deleteById(userId); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserEntity.java new file mode 100644 index 000000000..60af70ce9 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserEntity.java @@ -0,0 +1,65 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import it.chalmers.gamma.adapter.secondary.jpa.user.gdpr.GdprTrainedEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.MutableEntity; +import it.chalmers.gamma.app.user.domain.Language; +import jakarta.persistence.*; + +import java.time.Instant; +import java.util.UUID; + +@Entity +@Table(name = "g_user") +public class UserEntity extends MutableEntity { + + @Id + @Column(name = "user_id", columnDefinition = "uuid") + protected UUID id; + + @Column(name = "cid") + protected String cid; + + @Column(name = "password") + protected String password; + + @Column(name = "nick") + protected String nick; + + @Column(name = "first_name") + protected String firstName; + + @Column(name = "last_name") + protected String lastName; + + @Column(name = "email") + protected String email; + + @Column(name = "language") + @Enumerated(EnumType.STRING) + protected Language language; + + @Column(name = "user_agreement_accepted") + protected Instant userAgreementAccepted; + + @Column(name = "acceptance_year") + protected int acceptanceYear; + + @Column(name = "locked") + protected boolean locked; + + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + protected UserAvatarEntity userAvatar; + + protected UserEntity() { + } + + @Override + public UUID getId() { + return this.id; + } + + public void acceptUserAgreement() { + this.userAgreementAccepted = Instant.now(); + } + +} \ No newline at end of file diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserEntityConverter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserEntityConverter.java new file mode 100644 index 000000000..02fd47288 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserEntityConverter.java @@ -0,0 +1,63 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import it.chalmers.gamma.app.authentication.UserAccessGuard; +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.image.domain.ImageUri; +import it.chalmers.gamma.app.settings.domain.Settings; +import it.chalmers.gamma.app.settings.domain.SettingsRepository; +import it.chalmers.gamma.app.user.domain.*; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; + +import java.time.Instant; + +@Service +public class UserEntityConverter { + + private final UserAccessGuard userAccessGuard; + private final SettingsRepository settingsRepository; + + public UserEntityConverter(UserAccessGuard userAccessGuard, + SettingsRepository SettingsRepository) { + this.userAccessGuard = userAccessGuard; + this.settingsRepository = SettingsRepository; + } + + @Nullable + public GammaUser toDomain(UserEntity userEntity) { + Settings settings = this.settingsRepository.getSettings(); + UserId userId = new UserId(userEntity.id); + boolean acceptedUserAgreement = hasAcceptedLatestUserAgreement(userEntity.userAgreementAccepted, settings); + + if (!userAccessGuard.haveAccessToUser(userId, userEntity.locked, acceptedUserAgreement)) { + return null; + } + + UserExtended extended = null; + if (userAccessGuard.accessToExtended(userId)) { + extended = new UserExtended( + new Email(userEntity.email), + userEntity.getVersion(), + acceptedUserAgreement, + userEntity.locked, + userEntity.userAvatar == null ? null : new ImageUri(userEntity.userAvatar.avatarUri) + ); + } + + return new GammaUser( + userId, + new Cid(userEntity.cid), + new Nick(userEntity.nick), + new FirstName(userEntity.firstName), + new LastName(userEntity.lastName), + new AcceptanceYear(userEntity.acceptanceYear), + userEntity.language, + extended + ); + } + + private boolean hasAcceptedLatestUserAgreement(Instant acceptedUserAgreement, Settings settings) { + return settings.lastUpdatedUserAgreement().compareTo(acceptedUserAgreement) < 0; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserJpaRepository.java new file mode 100644 index 000000000..51c322ecf --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserJpaRepository.java @@ -0,0 +1,17 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +//TODO: Only UserRepositoryAdapter and UserPasswordRetrieverAdapter should be able to access this. +@Repository +public interface UserJpaRepository extends JpaRepository { + + Optional findByCid(String cid); + + Optional findByEmail(String email); + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordRetrieverAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordRetrieverAdapter.java new file mode 100644 index 000000000..af597b73e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordRetrieverAdapter.java @@ -0,0 +1,23 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import it.chalmers.gamma.app.user.domain.Password; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.security.user.UserPasswordRetriever; +import org.springframework.stereotype.Service; + +@Service +public class UserPasswordRetrieverAdapter implements UserPasswordRetriever { + + private final UserJpaRepository userJpaRepository; + + public UserPasswordRetrieverAdapter(UserJpaRepository userJpaRepository) { + this.userJpaRepository = userJpaRepository; + } + + @Override + public Password getPassword(UserId id) { + UserEntity userEntity = this.userJpaRepository.findById(id.value()) + .orElseThrow(UserNotFoundException::new); + return new Password(userEntity.password); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserRepositoryAdapter.java new file mode 100644 index 000000000..234a65d7c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserRepositoryAdapter.java @@ -0,0 +1,160 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user; + +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorHelper; +import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorState; +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.user.domain.*; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@Service +public class UserRepositoryAdapter implements UserRepository { + + private final UserJpaRepository repository; + private final UserEntityConverter converter; + private final UserAvatarJpaRepository userAvatarJpaRepository; + private final PasswordEncoder passwordEncoder; + + private final PersistenceErrorState cidNotUnique = new PersistenceErrorState( + "ituser_cid_key", + PersistenceErrorState.Type.NOT_UNIQUE + ); + + private final PersistenceErrorState emailNotUnique = new PersistenceErrorState( + "ituser_email_key", + PersistenceErrorState.Type.NOT_UNIQUE + ); + + public UserRepositoryAdapter(UserJpaRepository repository, + UserEntityConverter converter, + UserAvatarJpaRepository userAvatarJpaRepository, + PasswordEncoder passwordEncoder) { + this.repository = repository; + this.converter = converter; + this.userAvatarJpaRepository = userAvatarJpaRepository; + this.passwordEncoder = passwordEncoder; + } + + @Override + public void create(GammaUser user, UnencryptedPassword password) + throws CidAlreadyInUseException, EmailAlreadyInUseException { + try { + this.save(toEntity(user, password.value())); + } catch (DataIntegrityViolationException e) { + PersistenceErrorState state = PersistenceErrorHelper.getState(e); + + if (cidNotUnique.equals(state)) { + throw new CidAlreadyInUseException(); + } else if (emailNotUnique.equals(state)) { + throw new EmailAlreadyInUseException(); + } + + throw e; + } + } + + @Override + public void save(GammaUser user) { + this.save(toEntity(user, null)); + } + + private void save(UserEntity userEntity) { + this.repository.saveAndFlush(userEntity); + } + + @Override + public void delete(UserId userId) { + this.repository.deleteById(userId.value()); + } + + @Override + public List getAll() { + return this.repository.findAll().stream().map(this.converter::toDomain).filter(Objects::nonNull).toList(); + } + + @Override + public Optional get(UserId userId) { + return this.repository.findById(userId.getValue()).map(this.converter::toDomain); + } + + @Override + public Optional get(Cid cid) { + return this.repository.findByCid(cid.getValue()).map(this.converter::toDomain); + } + + @Override + public Optional get(Email email) { + return this.repository.findByEmail(email.value()).map(this.converter::toDomain); + } + + @Override + public boolean checkPassword(UserId userId, UnencryptedPassword password) throws UserNotFoundException { + UserEntity entity = this.repository.findById(userId.value()) + .orElseThrow(UserNotFoundException::new); + return passwordEncoder.matches(password.value(), entity.password); + } + + @Override + public void setPassword(UserId userId, UnencryptedPassword newPassword) throws UserNotFoundException { + UserEntity userEntity = this.repository.findById(userId.value()) + .orElseThrow(UserNotFoundException::new); + userEntity.password = passwordEncoder.encode(newPassword.value()); + this.save(userEntity); + } + + @Override + public void acceptUserAgreement(UserId userId) { + UserEntity userEntity = this.repository.findById(userId.value()) + .orElseThrow(UserNotFoundException::new); + userEntity.acceptUserAgreement(); + save(userEntity); + } + + private UserEntity toEntity(GammaUser d, String password) { + UserEntity e = this.repository.findById(d.id().value()) + .orElse(new UserEntity()); + UserExtended extended = d.extended(); + + e.increaseVersion(extended.version()); + + //If you want to update when the user agreement has been updated, use acceptUserAgreement() + if (e.userAgreementAccepted == null) { + if (d.extended().acceptedUserAgreement()) { + e.userAgreementAccepted = Instant.now(); + } else { + e.userAgreementAccepted = Instant.ofEpochSecond(0); + } + } + + e.id = d.id().value(); + e.cid = d.cid().value(); + e.acceptanceYear = d.acceptanceYear().value(); + e.email = extended.email().value(); + e.firstName = d.firstName().value(); + e.lastName = d.lastName().value(); + e.nick = d.nick().value(); + e.locked = d.extended().locked(); + e.language = d.language(); + + if (password != null) { + e.password = passwordEncoder.encode(password); + } + + if (d.extended().avatarUri() != null) { + e.userAvatar = this.userAvatarJpaRepository.findById(d.id().value()) + .orElse(new UserAvatarEntity()); + e.userAvatar.userId = e.id; + e.userAvatar.user = e; + e.userAvatar.avatarUri = d.extended().avatarUri().value(); + } + + return e; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/admin/AdminEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/admin/AdminEntity.java new file mode 100644 index 000000000..e9cdabc4d --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/admin/AdminEntity.java @@ -0,0 +1,29 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user.admin; + +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.util.UUID; + +@Entity +@Table(name = "g_admin_user") +public class AdminEntity extends ImmutableEntity { + + @Id + @Column(name = "user_id", columnDefinition = "uuid") + private UUID userId; + + protected AdminEntity() {} + + protected AdminEntity(UUID userId) { + this.userId = userId; + } + + @Override + public UUID getId() { + return this.userId; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/admin/AdminEntityJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/admin/AdminEntityJpaRepository.java new file mode 100644 index 000000000..55ce40cb3 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/admin/AdminEntityJpaRepository.java @@ -0,0 +1,10 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user.admin; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface AdminEntityJpaRepository extends JpaRepository { +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/admin/AdminEntityRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/admin/AdminEntityRepositoryAdapter.java new file mode 100644 index 000000000..3b86870c1 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/admin/AdminEntityRepositoryAdapter.java @@ -0,0 +1,48 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user.admin; + +import it.chalmers.gamma.app.admin.domain.AdminRepository; +import it.chalmers.gamma.app.user.domain.UserId; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public class AdminEntityRepositoryAdapter implements AdminRepository { + + private final AdminEntityJpaRepository repository; + + public AdminEntityRepositoryAdapter(AdminEntityJpaRepository repository) { + this.repository = repository; + } + + @Override + public boolean isAdmin(UserId userId) { + return this.repository.findById(userId.value()).isPresent(); + } + + @Override + public void setAdmin(UserId userId, boolean admin) { + Optional maybeAdminEntity = this.repository.findById(userId.value()); + + if (maybeAdminEntity.isPresent()) { + if(!admin) { + this.repository.deleteById(userId.value()); + } + } else { + if(admin) { + this.repository.save(new AdminEntity(userId.value())); + } + } + } + + @Override + public List getAll() { + return this.repository + .findAll() + .stream() + .map(adminEntity -> new UserId(adminEntity.getId())) + .toList(); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/gdpr/GdprTrainedEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/gdpr/GdprTrainedEntity.java new file mode 100644 index 000000000..858670e3a --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/gdpr/GdprTrainedEntity.java @@ -0,0 +1,32 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user.gdpr; + +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntity; +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import jakarta.persistence.*; + +import java.util.UUID; + +@Entity +@Table(name = "g_gdpr_trained") +public class GdprTrainedEntity extends ImmutableEntity { + + @Id + @Column(name = "user_id", columnDefinition = "uuid") + private UUID userId; + + @OneToOne + @PrimaryKeyJoinColumn(name = "user_id") + private UserEntity user; + + protected GdprTrainedEntity() {} + + public GdprTrainedEntity(UserEntity userEntity) { + this.userId = userEntity.getId(); + this.user = userEntity; + } + + @Override + public UUID getId() { + return this.userId; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/gdpr/GdprTrainedJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/gdpr/GdprTrainedJpaRepository.java new file mode 100644 index 000000000..b6960783d --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/gdpr/GdprTrainedJpaRepository.java @@ -0,0 +1,11 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user.gdpr; + + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface GdprTrainedJpaRepository extends JpaRepository { + +} + diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/gdpr/GdprTrainedRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/gdpr/GdprTrainedRepositoryAdapter.java new file mode 100644 index 000000000..a795d0029 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/gdpr/GdprTrainedRepositoryAdapter.java @@ -0,0 +1,39 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user.gdpr; + +import it.chalmers.gamma.adapter.secondary.jpa.user.UserJpaRepository; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.gdpr.GdprTrainedRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public class GdprTrainedRepositoryAdapter implements GdprTrainedRepository { + + private final GdprTrainedJpaRepository gdprTrainedJpaRepository; + private final UserJpaRepository userJpaRepository; + + public GdprTrainedRepositoryAdapter(GdprTrainedJpaRepository gdprTrainedJpaRepository, UserJpaRepository userJpaRepository) { + this.gdprTrainedJpaRepository = gdprTrainedJpaRepository; + this.userJpaRepository = userJpaRepository; + } + + @Override + public void setGdprTrainedStatus(UserId userId, boolean gdprTrained) { + Optional maybeGdprTrainedEntity = this.gdprTrainedJpaRepository.findById(userId.value()); + + if(gdprTrained && maybeGdprTrainedEntity.isEmpty()) { + this.gdprTrainedJpaRepository.save(new GdprTrainedEntity(userJpaRepository.findById(userId.value()).orElseThrow())); + } else if(!gdprTrained && maybeGdprTrainedEntity.isPresent()) { + this.gdprTrainedJpaRepository.deleteById(userId.value()); + } + } + + @Override + public List getAll() { + return this.gdprTrainedJpaRepository.findAll().stream().map(gdprTrainedEntity -> new UserId(gdprTrainedEntity.getId())).toList(); + } + + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetEntity.java new file mode 100644 index 000000000..2cb055113 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetEntity.java @@ -0,0 +1,54 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user.password; + +import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.passwordreset.domain.PasswordReset; +import it.chalmers.gamma.app.user.passwordreset.domain.PasswordResetToken; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.time.Instant; +import java.util.UUID; + +@Entity +@Table(name = "g_password_reset") +public class UserPasswordResetEntity extends ImmutableEntity { + + @Id + @Column(name = "user_id", columnDefinition = "uuid") + private UUID userId; + + @Column(name = "created_at") + private Instant createdAt; + + @Column(name = "token") + private String token; + + protected UserPasswordResetEntity() { + } + + public UserPasswordResetEntity(UUID userId, Instant createdAt, String token) { + this.userId = userId; + this.createdAt = createdAt; + this.token = token; + } + + @Override + public UserId getId() { + return new UserId(this.userId); + } + + public PasswordReset toDomain() { + return new PasswordReset( + this.getId(), + new PasswordResetToken(this.token), + this.createdAt + ); + } + + public String getToken() { + return this.token; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetJpaRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetJpaRepository.java new file mode 100644 index 000000000..0429ff8a6 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetJpaRepository.java @@ -0,0 +1,14 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user.password; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface UserPasswordResetJpaRepository extends JpaRepository { + Optional findByUserId(UUID userId); + + void deleteByToken(String token); +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetRepositoryAdapter.java new file mode 100644 index 000000000..1947ae476 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetRepositoryAdapter.java @@ -0,0 +1,63 @@ +package it.chalmers.gamma.adapter.secondary.jpa.user.password; + +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntity; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserJpaRepository; +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.user.domain.Cid; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserIdentifier; +import it.chalmers.gamma.app.user.passwordreset.domain.PasswordReset; +import it.chalmers.gamma.app.user.passwordreset.domain.PasswordResetRepository; +import it.chalmers.gamma.app.user.passwordreset.domain.PasswordResetToken; +import jakarta.transaction.Transactional; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.Optional; + +@Service("PasswordResetRepository") +@Transactional +public class UserPasswordResetRepositoryAdapter implements PasswordResetRepository { + + private final UserJpaRepository userJpaRepository; + private final UserPasswordResetJpaRepository userPasswordResetJpaRepository; + + public UserPasswordResetRepositoryAdapter(UserJpaRepository userJpaRepository, + UserPasswordResetJpaRepository userPasswordResetJpaRepository) { + this.userJpaRepository = userJpaRepository; + this.userPasswordResetJpaRepository = userPasswordResetJpaRepository; + } + + @Override + public PasswordResetToken createNewToken(Email email) throws UserNotFoundException { + Optional maybeUserEntity = this.userJpaRepository.findByEmail(email.value()); + + if(maybeUserEntity.isEmpty()) { + throw new UserNotFoundException(); + } + + UserEntity userEntity = maybeUserEntity.get(); + PasswordResetToken token = PasswordResetToken.generate(); + + this.userPasswordResetJpaRepository.save( + new UserPasswordResetEntity( + userEntity.getId(), + Instant.now(), + token.value() + ) + ); + + return token; + } + + @Override + public Optional getToken(UserId id) { + return userPasswordResetJpaRepository.findByUserId(id.value()) + .map(userPasswordResetEntity -> new PasswordResetToken(userPasswordResetEntity.getToken())); + } + + @Override + public void removeToken(PasswordResetToken token) { + this.userPasswordResetJpaRepository.deleteByToken(token.value()); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/AbstractEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/AbstractEntity.java new file mode 100644 index 000000000..636db52ae --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/AbstractEntity.java @@ -0,0 +1,47 @@ +package it.chalmers.gamma.adapter.secondary.jpa.util; + +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PostLoad; +import jakarta.persistence.PostPersist; +import jakarta.persistence.Transient; +import org.springframework.data.domain.Persistable; + +import java.util.Objects; + +@MappedSuperclass +public abstract class AbstractEntity implements Persistable { + + @Transient + private boolean persisted = false; + + @PostPersist + @PostLoad + void setPersisted() { + persisted = true; + } + + @Override + public boolean isNew() { + return !persisted; + } + + @Override + public final int hashCode() { + assert (getId() != null); + + return Objects.hash(getId().hashCode()); + } + + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + return Objects.equals(this.getId(), ((AbstractEntity) o).getId()); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/ImmutableEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/ImmutableEntity.java new file mode 100644 index 000000000..f65fd5a18 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/ImmutableEntity.java @@ -0,0 +1,8 @@ +package it.chalmers.gamma.adapter.secondary.jpa.util; + +import jakarta.persistence.MappedSuperclass; + +@MappedSuperclass +public abstract class ImmutableEntity extends AbstractEntity { + +} \ No newline at end of file diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/MutableEntity.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/MutableEntity.java new file mode 100644 index 000000000..b90e29f89 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/MutableEntity.java @@ -0,0 +1,52 @@ +package it.chalmers.gamma.adapter.secondary.jpa.util; + + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; + +@MappedSuperclass +public abstract class MutableEntity extends AbstractEntity { + + /** + * It is the responsibility of each entity converter to manage the version. + * Not using @jakarta.persistence.Version since it doesn't handle foreign keys + * such as members for a group. + */ + @Column(name = "version") + private int version; + + protected MutableEntity() { + this.version = 0; + } + + /** + * If not the correct version is provided, then the data + * that is being tried to be converted is outdated. + */ + public void increaseVersion(int currentVersion) { + /* + * If id is null, then currentVersion must be 0. This to indicate that the incoming entity is new. + * If not, then something is trying to save an entity that has been deleted. + */ + if (this.getId() == null && currentVersion != 0) { + throw new IllegalEntityStateException(); + } + // Version has to match the current version. + else if (this.version != currentVersion) { + throw new StaleDomainObjectException(); + } + + // Checks passed, updating the version. + this.version++; + } + + public int getVersion() { + return this.version; + } + + public static class IllegalEntityStateException extends RuntimeException { + } + + public static class StaleDomainObjectException extends RuntimeException { + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/PKId.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/PKId.java new file mode 100644 index 000000000..b31de7e12 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/PKId.java @@ -0,0 +1,21 @@ +package it.chalmers.gamma.adapter.secondary.jpa.util; + +import it.chalmers.gamma.app.common.Id; + +public abstract class PKId implements Id { + + @Override + public boolean equals(Object obj) { + if (obj instanceof Id otherId) { + return getValue().equals(otherId.getValue()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return getValue().hashCode(); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/PersistenceErrorHelper.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/PersistenceErrorHelper.java new file mode 100644 index 000000000..46eee1c40 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/PersistenceErrorHelper.java @@ -0,0 +1,60 @@ +package it.chalmers.gamma.adapter.secondary.jpa.util; + +import jakarta.persistence.EntityExistsException; +import jakarta.persistence.EntityNotFoundException; +import org.postgresql.util.PSQLException; +import org.postgresql.util.ServerErrorMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +import static it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorState.Type.FOREIGN_KEY_VIOLATION; +import static it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorState.Type.NOT_UNIQUE; + +public final class PersistenceErrorHelper { + + private static final Logger LOGGER = LoggerFactory.getLogger(PersistenceErrorHelper.class); + + private PersistenceErrorHelper() { + } + + public static PersistenceErrorState getState(Exception e) { + List states = new ArrayList<>(); + + for (Throwable t = e.getCause(); t != null; t = t.getCause()) { + + //If there's a specific PQLException such as a UNIQUE violation + if (t instanceof PSQLException postgresException) { + ServerErrorMessage serverErrorMessage = postgresException.getServerErrorMessage(); + if (serverErrorMessage != null) { + for (PersistenceErrorState.Type type : PersistenceErrorState.Type.values()) { + if (type.ERROR_CODE.equals(serverErrorMessage.getSQLState())) { + return new PersistenceErrorState(serverErrorMessage.getConstraint(), type); + } + } + } + } + + if (t instanceof EntityExistsException) { + return new PersistenceErrorState(null, NOT_UNIQUE); + } + + if (t instanceof EntityNotFoundException) { + return new PersistenceErrorState(null, FOREIGN_KEY_VIOLATION); + } + + } + + e.printStackTrace(); + + throw new UnknownDataIntegrityViolationException(); + } + + public static class UnknownDataIntegrityViolationException extends RuntimeException { + + } + + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/PersistenceErrorState.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/PersistenceErrorState.java new file mode 100644 index 000000000..3b6aa0a1c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/util/PersistenceErrorState.java @@ -0,0 +1,17 @@ +package it.chalmers.gamma.adapter.secondary.jpa.util; + + +public record PersistenceErrorState(String constraint, Type type) { + + public enum Type { + NOT_UNIQUE("23505"), FOREIGN_KEY_VIOLATION("23503"), IS_NULL("23502"); + + public final String ERROR_CODE; + + Type(String errorCode) { + this.ERROR_CODE = errorCode; + } + + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/mail/GotifyMailService.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/mail/GotifyMailService.java new file mode 100644 index 000000000..79f785fac --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/mail/GotifyMailService.java @@ -0,0 +1,61 @@ +package it.chalmers.gamma.adapter.secondary.mail; + +import it.chalmers.gamma.app.mail.domain.MailService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class GotifyMailService implements MailService { + + private static final Logger LOGGER = LoggerFactory.getLogger(GotifyMailService.class); + private final String gotifyApiKey; + private final String gotifyURL; + private final boolean production; + + public GotifyMailService(@Value("${application.gotify.key}") String gotifyApiKey, + @Value("${application.gotify.url}") String gotifyURL, + @Value("${application.production}") boolean production) { + this.gotifyApiKey = gotifyApiKey; + this.gotifyURL = gotifyURL; + this.production = production; + } + + public void sendMail(String email, String subject, String body) { + if (this.production) { + sendMailViaGotify(email, subject, body); + } else { + LOGGER.warn("Not in production environment, printing mail: \n " + + "to: " + email + "\n" + + "subject: " + subject + "\n" + + "body: " + body); + } + } + + /** + * Sends mail using Gotify Rest API, see https://github.com/cthit/gotify + * + * @return true if message was successfully sent false if not + */ + private void sendMailViaGotify(String email, String subject, String body) { + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.AUTHORIZATION, "pre-shared: " + this.gotifyApiKey); + headers.setContentType(MediaType.APPLICATION_JSON); + + record Request (String to, String from, String subject, String body) {} + + Request request = new Request(email, "no-reply@chalmers.it", subject, body); + + HttpEntity entity = new HttpEntity<>(request, headers); + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity response = restTemplate.postForEntity(this.gotifyURL, entity, String.class); + LOGGER.info("Gotify responded with " + response.getHeaders() + response.getBody()); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationRedisRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationRedisRepository.java new file mode 100644 index 000000000..70999f384 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationRedisRepository.java @@ -0,0 +1,9 @@ +package it.chalmers.gamma.adapter.secondary.redis.oauth2; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AuthorizationRedisRepository extends CrudRepository { + +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationRepositoryAdapter.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationRepositoryAdapter.java new file mode 100644 index 000000000..b42d1e7fc --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationRepositoryAdapter.java @@ -0,0 +1,73 @@ +package it.chalmers.gamma.adapter.secondary.redis.oauth2; + +import it.chalmers.gamma.app.oauth2.domain.GammaAuthorizationRepository; +import it.chalmers.gamma.app.oauth2.domain.GammaAuthorizationToken; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +public class AuthorizationRepositoryAdapter implements GammaAuthorizationRepository { + + private final AuthorizationRedisRepository authorizationRedisRepository; + private final AuthorizationTokenMapperRedisRepository authorizationTokenMapperRedisRepository; + + public AuthorizationRepositoryAdapter(AuthorizationRedisRepository authorizationRedisRepository, AuthorizationTokenMapperRedisRepository authorizationTokenMapperRedisRepository) { + this.authorizationRedisRepository = authorizationRedisRepository; + this.authorizationTokenMapperRedisRepository = authorizationTokenMapperRedisRepository; + } + + //HMM den sparar bara gamma-authorization och gamma-authorization-token? + //Också, ta bort GammaAuthorization och spara OAuth2Authorization direkt bara. + + @Override + public void save(OAuth2Authorization authorization) { + this.authorizationRedisRepository.save(new AuthorizationValue(authorization)); + + var code = authorization.getToken(OAuth2AuthorizationCode.class); + if (code != null) { + this.authorizationTokenMapperRedisRepository.save(new AuthorizationTokenMapperValue(authorization.getId(), "code", code.getToken().getTokenValue())); + } + + var accessToken = authorization.getToken(OAuth2AccessToken.class); + if (accessToken != null) { + this.authorizationTokenMapperRedisRepository.save(new AuthorizationTokenMapperValue(authorization.getId(), "access_token", accessToken.getToken().getTokenValue())); + } + + var state = authorization.getAttribute("state"); + if (state != null) { + this.authorizationTokenMapperRedisRepository.save(new AuthorizationTokenMapperValue(authorization.getId(), "state", state.toString())); + } + + var oidcToken = authorization.getToken(OidcIdToken.class); + if (oidcToken != null) { + this.authorizationTokenMapperRedisRepository.save(new AuthorizationTokenMapperValue(authorization.getId(), "oidc", oidcToken.getToken().getTokenValue())); + } + } + + @Override + public void remove(OAuth2Authorization authorization) { + this.authorizationRedisRepository.save(new AuthorizationValue(authorization)); + } + + @Override + public Optional findById(String id) { + return this.authorizationRedisRepository.findById(id).map(AuthorizationValue::toOAuth2Authorization); + } + + @Override + public Optional findByToken(GammaAuthorizationToken gammaAuthorizationToken) { + String token = gammaAuthorizationToken.type().name().toLowerCase() + ":" + gammaAuthorizationToken.value(); + Optional maybeTokenMapper = this.authorizationTokenMapperRedisRepository.findById(token); + if (maybeTokenMapper.isPresent()) { + Optional maybeAuthorizationValue = this.authorizationRedisRepository.findById(maybeTokenMapper.get().authorizationKey); + return maybeAuthorizationValue.map(AuthorizationValue::toOAuth2Authorization); + } + + return Optional.empty(); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationTokenMapperRedisRepository.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationTokenMapperRedisRepository.java new file mode 100644 index 000000000..f1ba78fe9 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationTokenMapperRedisRepository.java @@ -0,0 +1,8 @@ +package it.chalmers.gamma.adapter.secondary.redis.oauth2; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AuthorizationTokenMapperRedisRepository extends CrudRepository { +} diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationTokenMapperValue.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationTokenMapperValue.java new file mode 100644 index 000000000..b0d8209d1 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationTokenMapperValue.java @@ -0,0 +1,23 @@ +package it.chalmers.gamma.adapter.secondary.redis.oauth2; + +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +@RedisHash(value = "token", timeToLive = 3600) +public class AuthorizationTokenMapperValue { + + @Id + String tokenKey; + + String authorizationKey; + + public AuthorizationTokenMapperValue() { + } + + public AuthorizationTokenMapperValue(String authorizationKey, String type, String token) { + this.tokenKey = type + ":" + token; + this.authorizationKey = authorizationKey; + } + +} + diff --git a/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationValue.java b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationValue.java new file mode 100644 index 000000000..f43b74e74 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/adapter/secondary/redis/oauth2/AuthorizationValue.java @@ -0,0 +1,37 @@ +package it.chalmers.gamma.adapter.secondary.redis.oauth2; + + +import it.chalmers.gamma.util.Serializer; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; + +import java.io.IOException; + +@RedisHash(value = "gamma-authorization", timeToLive = 3600) +public class AuthorizationValue { + + @Id + String id; + String authorization; + + public AuthorizationValue() { + } + + public AuthorizationValue(OAuth2Authorization authorization) { + this.id = authorization.getId(); + try { + this.authorization = Serializer.toString(authorization); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public OAuth2Authorization toOAuth2Authorization() { + try { + return (OAuth2Authorization) Serializer.fromString(this.authorization); + } catch (IOException | ClassNotFoundException e) { + throw new RuntimeException(e); + } + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/Facade.java b/backend/src/main/java/it/chalmers/gamma/app/Facade.java new file mode 100644 index 000000000..122e57886 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/Facade.java @@ -0,0 +1,13 @@ +package it.chalmers.gamma.app; + +import it.chalmers.gamma.app.authentication.AccessGuard; + +public abstract class Facade { + + protected AccessGuard accessGuard; + + public Facade(AccessGuard accessGuard) { + this.accessGuard = accessGuard; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/UnexpectedRuntimeException.java b/backend/src/main/java/it/chalmers/gamma/app/UnexpectedRuntimeException.java new file mode 100644 index 000000000..f71788bb4 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/UnexpectedRuntimeException.java @@ -0,0 +1,5 @@ +package it.chalmers.gamma.app; + +public class UnexpectedRuntimeException extends RuntimeException { + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/admin/AdminFacade.java b/backend/src/main/java/it/chalmers/gamma/app/admin/AdminFacade.java new file mode 100644 index 000000000..8ae7c50af --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/admin/AdminFacade.java @@ -0,0 +1,40 @@ +package it.chalmers.gamma.app.admin; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.admin.domain.AdminRepository; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.user.domain.UserId; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.UUID; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; + +@Component +public class AdminFacade extends Facade { + + private final AdminRepository adminRepository; + + public AdminFacade(AccessGuard accessGuard, AdminRepository adminRepository) { + super(accessGuard); + this.adminRepository = adminRepository; + } + + public void setAdmin(UUID userId, boolean admin) { + accessGuard.require(isAdmin()); + + this.adminRepository.setAdmin(new UserId(userId), admin); + } + + public List getAllAdmins() { + accessGuard.require(isAdmin()); + + return this.adminRepository + .getAll() + .stream() + .map(UserId::value) + .toList(); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/admin/domain/AdminRepository.java b/backend/src/main/java/it/chalmers/gamma/app/admin/domain/AdminRepository.java new file mode 100644 index 000000000..4b98fcbfc --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/admin/domain/AdminRepository.java @@ -0,0 +1,14 @@ +package it.chalmers.gamma.app.admin.domain; + +import it.chalmers.gamma.app.user.domain.UserId; + +import java.util.List; + +public interface AdminRepository { + + boolean isAdmin(UserId userId); + void setAdmin(UserId userId, boolean admin); + + List getAll(); + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/apikey/ApiKeyFacade.java b/backend/src/main/java/it/chalmers/gamma/app/apikey/ApiKeyFacade.java new file mode 100644 index 000000000..5b25ad81b --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/apikey/ApiKeyFacade.java @@ -0,0 +1,120 @@ +package it.chalmers.gamma.app.apikey; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.apikey.domain.*; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; + +@Component +public class ApiKeyFacade extends Facade { + + private final ApiKeyRepository apiKeyRepository; + + public ApiKeyFacade(AccessGuard accessGuard, ApiKeyRepository apiKeyRepository) { + super(accessGuard); + this.apiKeyRepository = apiKeyRepository; + } + + public String[] getApiKeyTypes() { + List types = Arrays.stream(ApiKeyType.values()).filter(apiKeyType -> apiKeyType != ApiKeyType.CLIENT).toList(); + String[] s = new String[types.size()]; + for (int i = 0; i < s.length; i++) { + s[i] = types.get(i).name(); + } + return s; + } + + public String create(NewApiKey newApiKey) { + this.accessGuard.require(isAdmin()); + + ApiKeyType type = ApiKeyType.valueOf(newApiKey.keyType); + + if(type == ApiKeyType.CLIENT) { + throw new IllegalArgumentException("Cannot create api key with type client without creating a client at the same time"); + } + + ApiKeyToken apiKeyToken = ApiKeyToken.generate(); + apiKeyRepository.create( + new ApiKey( + ApiKeyId.generate(), + new PrettyName(newApiKey.prettyName), + new Text(newApiKey.svDescription, newApiKey.enDescription), + type, + apiKeyToken + ) + ); + return apiKeyToken.value(); + } + + public void delete(UUID apiKeyId) throws ApiKeyNotFoundException { + this.accessGuard.require(isAdmin()); + + try { + apiKeyRepository.delete(new ApiKeyId(apiKeyId)); + } catch (ApiKeyRepository.ApiKeyNotFoundException e) { + throw new ApiKeyNotFoundException(); + } + } + + public Optional getById(UUID apiKeyId) { + this.accessGuard.require(isAdmin()); + + return this.apiKeyRepository.getById(new ApiKeyId(apiKeyId)).map(ApiKeyDTO::new); + } + + public List getAll() { + this.accessGuard.require(isAdmin()); + + return this.apiKeyRepository.getAll() + .stream() + .map(ApiKeyDTO::new) + .toList(); + } + + public String resetApiKeyToken(UUID apiKeyId) throws ApiKeyNotFoundException { + this.accessGuard.require(isAdmin()); + + ApiKeyToken token; + try { + token = this.apiKeyRepository.resetApiKeyToken(new ApiKeyId(apiKeyId)); + } catch (ApiKeyRepository.ApiKeyNotFoundException e) { + throw new ApiKeyNotFoundException(); + } + return token.value(); + } + + public record NewApiKey( + String prettyName, + String svDescription, + String enDescription, + String keyType) { + } + + public record ApiKeyDTO(UUID id, + String prettyName, + String svDescription, + String enDescription, + String keyType) { + public ApiKeyDTO(ApiKey apiKey) { + this(apiKey.id().value(), + apiKey.prettyName().value(), + apiKey.description().sv().value(), + apiKey.description().en().value(), + apiKey.keyType().name() + ); + } + } + + public static class ApiKeyNotFoundException extends Exception { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKey.java b/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKey.java new file mode 100644 index 000000000..0d161f7a2 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKey.java @@ -0,0 +1,26 @@ +package it.chalmers.gamma.app.apikey.domain; + +import io.soabase.recordbuilder.core.RecordBuilder; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; + +import java.util.Objects; + +/** + * id and apiKeyToken must both be unique from all other api key tokens. + */ +@RecordBuilder +public record ApiKey(ApiKeyId id, + PrettyName prettyName, + Text description, + ApiKeyType keyType, + ApiKeyToken apiKeyToken) implements ApiKeyBuilder.With { + public ApiKey { + Objects.requireNonNull(id); + Objects.requireNonNull(prettyName); + Objects.requireNonNull(description); + Objects.requireNonNull(keyType); + Objects.requireNonNull(apiKeyToken); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyId.java b/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyId.java new file mode 100644 index 000000000..4ba34017e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyId.java @@ -0,0 +1,27 @@ +package it.chalmers.gamma.app.apikey.domain; + +import it.chalmers.gamma.app.common.Id; + +import java.util.Objects; +import java.util.UUID; + +public record ApiKeyId(UUID value) implements Id { + + public ApiKeyId { + Objects.requireNonNull(value); + } + + public ApiKeyId(String value) { + this(UUID.fromString(value)); + } + + public static ApiKeyId generate() { + return new ApiKeyId(UUID.randomUUID()); + } + + @Override + public UUID getValue() { + return this.value; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyRepository.java b/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyRepository.java new file mode 100644 index 000000000..fd6b43d9b --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyRepository.java @@ -0,0 +1,29 @@ +package it.chalmers.gamma.app.apikey.domain; + +import java.util.List; +import java.util.Optional; + +public interface ApiKeyRepository { + + void create(ApiKey apiKey) throws ApiKeyAlreadyExistRuntimeException; + + void delete(ApiKeyId apiKeyId) throws ApiKeyNotFoundException; + + ApiKeyToken resetApiKeyToken(ApiKeyId apiKeyId) throws ApiKeyNotFoundException; + + List getAll(); + + Optional getById(ApiKeyId apiKeyId); + + Optional getByToken(ApiKeyToken apiKeyToken); + + class ApiKeyNotFoundException extends Exception { + } + + /** + * Either the api key id or token already exists. Runtime exception since id and token is generated. + */ + class ApiKeyAlreadyExistRuntimeException extends RuntimeException { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyToken.java b/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyToken.java new file mode 100644 index 000000000..ebf0a0382 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyToken.java @@ -0,0 +1,27 @@ +package it.chalmers.gamma.app.apikey.domain; + +import it.chalmers.gamma.util.TokenUtils; + +import java.util.Objects; + +public record ApiKeyToken(String value) { + + public ApiKeyToken { + Objects.requireNonNull(value); + + //TODO: Use the following validation +// if (value.length() < 150) { +// throw new IllegalArgumentException(); +// } + } + + public static ApiKeyToken generate() { + String value = TokenUtils.generateToken( + 150, + TokenUtils.CharacterTypes.LOWERCASE, + TokenUtils.CharacterTypes.UPPERCASE, + TokenUtils.CharacterTypes.NUMBERS + ); + return new ApiKeyToken(value); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyType.java b/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyType.java new file mode 100644 index 000000000..af8575eff --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyType.java @@ -0,0 +1,19 @@ +package it.chalmers.gamma.app.apikey.domain; + +import it.chalmers.gamma.adapter.primary.external.client.ClientApiV1Controller; +import it.chalmers.gamma.adapter.primary.external.goldapps.GoldappsV1ApiController; +import it.chalmers.gamma.adapter.primary.external.info.InfoV1ApiController; +import it.chalmers.gamma.adapter.primary.external.allowlist.AllowListV1ApiController; + +public enum ApiKeyType { + CLIENT(ClientApiV1Controller.URI), + GOLDAPPS(GoldappsV1ApiController.URI), + INFO(InfoV1ApiController.URI), + ALLOW_LIST(AllowListV1ApiController.URI); + + public final String URI; + + ApiKeyType(String uri) { + this.URI = uri; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/authentication/AccessGuard.java b/backend/src/main/java/it/chalmers/gamma/app/authentication/AccessGuard.java new file mode 100644 index 000000000..49ed09b95 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/authentication/AccessGuard.java @@ -0,0 +1,157 @@ +package it.chalmers.gamma.app.authentication; + +import it.chalmers.gamma.app.apikey.domain.ApiKeyType; +import it.chalmers.gamma.app.client.domain.ClientRepository; +import it.chalmers.gamma.app.client.domain.Client; +import it.chalmers.gamma.app.group.domain.Group; +import it.chalmers.gamma.app.user.domain.GammaUser; +import it.chalmers.gamma.app.user.domain.UnencryptedPassword; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserRepository; +import it.chalmers.gamma.security.authentication.ApiAuthentication; +import it.chalmers.gamma.security.authentication.AuthenticationExtractor; +import it.chalmers.gamma.security.authentication.LocalRunnerAuthentication; +import it.chalmers.gamma.security.authentication.UserAuthentication; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class AccessGuard { + + private static final Logger LOGGER = LoggerFactory.getLogger(AccessGuard.class); + + private final UserRepository userRepository; + private final ClientRepository clientRepository; + + public AccessGuard(UserRepository userRepository, + ClientRepository clientRepository) { + this.userRepository = userRepository; + this.clientRepository = clientRepository; + } + + public static AccessChecker isAdmin() { + return (clientRepository, userRepository) -> { + if (AuthenticationExtractor.getAuthentication() instanceof UserAuthentication userAuthenticated) { + return userAuthenticated.isAdmin(); + } + + return false; + }; + } + + public static AccessChecker passwordCheck(String password) { + return (clientRepository, userRepository) -> { + if (AuthenticationExtractor.getAuthentication() instanceof UserAuthentication userAuthenticated) { + GammaUser user = userAuthenticated.get(); + return userRepository.checkPassword(user.id(), new UnencryptedPassword(password)); + } + + return false; + }; + } + + public static AccessChecker isApi(ApiKeyType apiKeyType) { + return (clientRepository, userRepository) -> { + if (AuthenticationExtractor.getAuthentication() instanceof ApiAuthentication apiPrincipal) { + return apiPrincipal.get().keyType() == apiKeyType; + } + + return false; + }; + } + + public static AccessChecker isClientApi() { + return (clientRepository, userRepository) -> { + if (AuthenticationExtractor.getAuthentication() instanceof ApiAuthentication apiPrincipal) { + return apiPrincipal.get().keyType() == ApiKeyType.CLIENT; + } + + return false; + }; + } + + public static AccessChecker isSignedInUserMemberOfGroup(Group group) { + return (clientRepository, userRepository) -> { + if (AuthenticationExtractor.getAuthentication() instanceof UserAuthentication userPrincipal) { + GammaUser user = userPrincipal.get(); + return group.groupMembers().stream().anyMatch(groupMember -> groupMember.user().equals(user)); + } + + return false; + }; + } + + public static AccessChecker isSignedIn() { + return (clientRepository, userRepository) + -> AuthenticationExtractor.getAuthentication() instanceof UserAuthentication; + } + + public static AccessChecker isNotSignedIn() { + return (clientRepository, userRepository) -> + AuthenticationExtractor.getAuthentication() == null; + } + + public static AccessChecker userHasAcceptedClient(UserId id) { + return (clientRepository, userRepository) -> { + if (AuthenticationExtractor.getAuthentication() instanceof ApiAuthentication apiPrincipal) { + if (apiPrincipal.getClient().isPresent()) { + Client client = apiPrincipal.getClient().get(); + return clientRepository.isApprovedByUser(id, client.clientUid()); + } + } + + return false; + }; + } + + /** + * Such as Bootstrap + */ + public static AccessChecker isLocalRunner() { + return (clientRepository, userRepository) -> + AuthenticationExtractor.getAuthentication() instanceof LocalRunnerAuthentication; + } + + public void require(AccessChecker check) { + if (!validate(check)) { + throw new AccessDeniedException(); + } + } + + public void requireEither(AccessChecker... checks) { + for (AccessChecker check : checks) { + if (validate(check)) { + return; + } + } + + //None of the check went through thus access denied + throw new AccessDeniedException(); + } + + public void requireAll(AccessChecker... checks) { + for (AccessChecker check : checks) { + if (!validate(check)) { + //If any of the checks fails, then access denied + throw new AccessDeniedException(); + } + } + } + + private boolean validate(AccessChecker check) { + return check.validate(clientRepository, userRepository); + } + + public interface AccessChecker { + boolean validate(ClientRepository clientRepository, UserRepository userRepository); + } + + public static class AccessDeniedException extends RuntimeException { + public AccessDeniedException() { + LOGGER.error("Access was denied."); + } + } + +} + diff --git a/backend/src/main/java/it/chalmers/gamma/app/authentication/UserAccessGuard.java b/backend/src/main/java/it/chalmers/gamma/app/authentication/UserAccessGuard.java new file mode 100644 index 000000000..0268ac176 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/authentication/UserAccessGuard.java @@ -0,0 +1,140 @@ +package it.chalmers.gamma.app.authentication; + +import it.chalmers.gamma.app.apikey.domain.ApiKeyType; +import it.chalmers.gamma.app.client.domain.ClientRepository; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.bootstrap.BootstrapAuthenticated; +import it.chalmers.gamma.security.authentication.ApiAuthentication; +import it.chalmers.gamma.security.authentication.AuthenticationExtractor; +import it.chalmers.gamma.security.authentication.UserAuthentication; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Service; + +/* + * One could think that using AuthenticatedService would be a great idea. + * The problem there is that there will be a circular dependency. + */ +@Service +public class UserAccessGuard { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserAccessGuard.class); + + private final ClientRepository clientRepository; + + public UserAccessGuard(ClientRepository clientRepository) { + this.clientRepository = clientRepository; + } + + public boolean accessToExtended(UserId userId) { + return isMe(userId) || isAdmin() || isLocalRunnerAuthenticated(); + } + + public boolean isMe(UserId userId) { + if (SecurityContextHolder.getContext().getAuthentication() instanceof UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) { + return UserId.valueOf(usernamePasswordAuthenticationToken.getName()).equals(userId); + } + + if (SecurityContextHolder.getContext().getAuthentication() instanceof JwtAuthenticationToken jwtAuthenticationToken) { + return UserId.valueOf(jwtAuthenticationToken.getName()).equals(userId); + } + + return false; + } + + public boolean isAdmin() { + if (AuthenticationExtractor.getAuthentication() instanceof UserAuthentication authenticationDetails) { + return authenticationDetails.isAdmin(); + } + + return false; + } + + public boolean haveAccessToUser(UserId userId, boolean userLocked, boolean acceptedUserAgreement) { + if (SecurityContextHolder.getContext().getAuthentication() == null + || !SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) { + return false; + } + + //Always access to yourself + if (isMe(userId)) { + return true; + } + + /* + * If the user is locked or has not accepted the latest user agreement then nothing should be returned + * unless if and only if the signed-in user is an admin. + */ + if (userLocked || !acceptedUserAgreement) { + return isAdmin(); + } + + // If one user is trying to access another user, then approve + if (isInternalAuthenticated()) { + return true; + } + + // If a client is trying to access a user that have approved the client, then approve + if (haveAcceptedClient(userId)) { + return true; + } + + if (apiKeyWithAccess()) { + return true; + } + + // If it's a local runner, then approve + if (isLocalRunnerAuthenticated()) { + return true; + } + + LOGGER.error("tried to access the user: " + userId + "; "); + + //Return false by default + return true; + } + + private boolean isInternalAuthenticated() { + return AuthenticationExtractor.getAuthentication() instanceof UserAuthentication; + } + + private boolean isLocalRunnerAuthenticated() { + return SecurityContextHolder.getContext().getAuthentication() instanceof BootstrapAuthenticated; + } + + //If the client tries to access a user that have not accepted the client, then return null. + private boolean haveAcceptedClient(UserId userId) { + if (AuthenticationExtractor.getAuthentication() instanceof ApiAuthentication apiAuthenticationPrincipal) { + ApiKeyType apiKeyType = apiAuthenticationPrincipal.get().keyType(); + if (apiKeyType.equals(ApiKeyType.CLIENT)) { + if (apiAuthenticationPrincipal.getClient().isEmpty()) { + throw new IllegalStateException( + "An api key that is of type CLIENT must have a client connected to them; " + + apiAuthenticationPrincipal.get() + ); + } + + + return clientRepository.isApprovedByUser(userId, apiAuthenticationPrincipal.getClient().get().clientUid()); + } + } + + return false; + } + + /** + * Api Key with type INFO or GOLDAPPS have access to user information. + */ + private boolean apiKeyWithAccess() { + if (AuthenticationExtractor.getAuthentication() instanceof ApiAuthentication apiAuthenticationPrincipal) { + ApiKeyType apiKeyType = apiAuthenticationPrincipal.get().keyType(); + return apiKeyType.equals(ApiKeyType.INFO) || apiKeyType.equals(ApiKeyType.GOLDAPPS); + } + + return false; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/ClientFacade.java b/backend/src/main/java/it/chalmers/gamma/app/client/ClientFacade.java new file mode 100644 index 000000000..287b45f6e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/ClientFacade.java @@ -0,0 +1,210 @@ +package it.chalmers.gamma.app.client; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.apikey.domain.ApiKey; +import it.chalmers.gamma.app.apikey.domain.ApiKeyId; +import it.chalmers.gamma.app.apikey.domain.ApiKeyToken; +import it.chalmers.gamma.app.apikey.domain.ApiKeyType; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.client.domain.*; +import it.chalmers.gamma.app.client.domain.restriction.ClientRestriction; +import it.chalmers.gamma.app.client.domain.restriction.ClientRestrictionId; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; +import it.chalmers.gamma.app.post.domain.PostId; +import it.chalmers.gamma.app.post.domain.PostRepository; +import it.chalmers.gamma.app.supergroup.SuperGroupFacade; +import it.chalmers.gamma.app.supergroup.domain.SuperGroup; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupRepository; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserRepository; +import jakarta.transaction.Transactional; +import org.springframework.stereotype.Service; + +import java.util.*; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; +import static it.chalmers.gamma.app.authentication.AccessGuard.isSignedIn; + +@Service +public class ClientFacade extends Facade { + + private final ClientRepository clientRepository; + private final UserRepository userRepository; + private final SuperGroupRepository superGroupRepository; + private final PostRepository postRepository; + + public ClientFacade(AccessGuard accessGuard, + ClientRepository clientRepository, + UserRepository userRepository, + SuperGroupRepository superGroupRepository, + PostRepository postRepository) { + super(accessGuard); + this.clientRepository = clientRepository; + this.userRepository = userRepository; + this.superGroupRepository = superGroupRepository; + this.postRepository = postRepository; + } + + /** + * @return The client secret for the client + */ + @Transactional + public ClientAndApiKeySecrets create(NewClient newClient) { + this.accessGuard.require(isAdmin()); + + ClientSecret clientSecret = ClientSecret.generate(); + ApiKey apiKey = null; + ApiKeyToken apiKeyToken = null; + + if (newClient.generateApiKey) { + apiKeyToken = ApiKeyToken.generate(); + + apiKey = new ApiKey( + ApiKeyId.generate(), + new PrettyName(newClient.prettyName), + new Text( + "Api nyckel för klienten: " + newClient.prettyName, + "Api key for client: " + newClient.prettyName + ), + ApiKeyType.CLIENT, + apiKeyToken + ); + } + + List scopes = new ArrayList<>(); + scopes.add(Scope.PROFILE); + if (newClient.emailScope) { + scopes.add(Scope.EMAIL); + } + + ClientUid clientUid = ClientUid.generate(); + ClientId clientId = ClientId.generate(); + + Client client = new Client( + clientUid, + clientId, + clientSecret, + new ClientRedirectUrl(newClient.redirectUrl), + new PrettyName(newClient.prettyName), + new Text( + newClient.svDescription, + newClient.enDescription + ), + scopes, + apiKey, + new ClientOwnerOfficial(), + new ClientRestriction( + ClientRestrictionId.generate(), + newClient.restrictions + .superGroups + .stream() + .map(superGroupId -> this.superGroupRepository.get(new SuperGroupId(superGroupId)).orElseThrow()) + .toList() + ) + ); + + this.clientRepository.save(client); + + return new ClientAndApiKeySecrets( + clientUid.value(), + clientId.value(), + clientSecret.value(), + apiKeyToken == null ? null : apiKeyToken.value() + ); + } + + @Transactional + public ClientAndApiKeySecrets createDev(NewClient newClient) { + accessGuard.require(isSignedIn()); + + throw new UnsupportedOperationException(); + } + + public void delete(String clientUid) throws ClientFacade.ClientNotFoundException { + this.accessGuard.require(isAdmin()); + + try { + this.clientRepository.delete(ClientUid.valueOf(clientUid)); + } catch (ClientRepository.ClientNotFoundException e) { + throw new ClientFacade.ClientNotFoundException(); + } + } + + public Optional get(String clientUid) { + return this.clientRepository.get(ClientUid.valueOf(clientUid)).map(ClientDTO::new); + } + + public List getAll() { + this.accessGuard.require(isAdmin()); + + return this.clientRepository.getAll() + .stream() + .map(ClientDTO::new) + .toList(); + } + + public String resetClientSecret(String clientUid) throws ClientNotFoundException { + this.accessGuard.require(isAdmin()); + + Client client = this.clientRepository.get(ClientUid.valueOf(clientUid)) + .orElseThrow(ClientNotFoundException::new); + ClientSecret newSecret = ClientSecret.generate(); + + Client newClient = client.withClientSecret(newSecret); + + this.clientRepository.save(newClient); + + return newSecret.value(); + } + + public record NewClientRestrictions(List superGroups) {} + + public record NewClient(String redirectUrl, + String prettyName, + String svDescription, + String enDescription, + boolean generateApiKey, + boolean emailScope, + NewClientRestrictions restrictions) { + } + + public record ClientAndApiKeySecrets( + UUID clientUid, + String clientId, + String clientSecret, + String apiKeyToken + ) { + } + + public record ClientDTO(UUID clientUid, + String clientId, + String webServerRedirectUrl, + String prettyName, + String svDescription, + String enDescription, + boolean hasApiKey, + ClientRestrictionDTO restriction) { + + public ClientDTO(Client client) { + this(client.clientUid().value(), + client.clientId().value(), + client.clientRedirectUrl().value(), + client.prettyName().value(), + client.description().sv().value(), + client.description().en().value(), + client.clientApiKey().isPresent(), + client.restrictions().map(clientRestriction -> new ClientRestrictionDTO( + clientRestriction.superGroups().stream().map(SuperGroupFacade.SuperGroupDTO::new).toList() + )).orElse(null) + ); + } + + public record ClientRestrictionDTO(List superGroups) { } + } + + public static class ClientNotFoundException extends Exception { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/Client.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/Client.java new file mode 100644 index 000000000..20f4391fd --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/Client.java @@ -0,0 +1,160 @@ +package it.chalmers.gamma.app.client.domain; + +import it.chalmers.gamma.app.apikey.domain.ApiKey; +import it.chalmers.gamma.app.apikey.domain.ApiKeyType; +import it.chalmers.gamma.app.client.domain.restriction.ClientRestriction; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public final class Client { + private final ClientUid clientUid; + private final ClientId clientId; + private final ClientSecret clientSecret; + private final ClientRedirectUrl clientRedirectUrl; + private final PrettyName prettyName; + private final Text description; + private final List scopes; + private final ApiKey clientApiKey; + private final ClientOwner owner; + private final ClientRestriction restriction; + + public Client(ClientUid clientUid, + ClientId clientId, + ClientSecret clientSecret, + ClientRedirectUrl clientRedirectUrl, + PrettyName prettyName, + Text description, + List scopes, + ApiKey clientApiKey, + ClientOwner owner, + ClientRestriction restriction) { + Objects.requireNonNull(clientId); + Objects.requireNonNull(clientRedirectUrl); + Objects.requireNonNull(prettyName); + Objects.requireNonNull(description); + Objects.requireNonNull(scopes); + Objects.requireNonNull(owner); + + if (clientApiKey != null && clientApiKey.keyType() != ApiKeyType.CLIENT) { + throw new IllegalArgumentException("If a client has a ApiKey, then the type must be ApiKeyType.CLIENT"); + } + + this.clientUid = clientUid; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.clientRedirectUrl = clientRedirectUrl; + this.prettyName = prettyName; + this.description = description; + this.scopes = scopes; + this.clientApiKey = clientApiKey; + this.owner = owner; + this.restriction = restriction; + } + + public ClientUid clientUid() { + return clientUid; + } + + public ClientId clientId() { + return clientId; + } + + public ClientSecret clientSecret() { + return clientSecret; + } + + public ClientRedirectUrl clientRedirectUrl() { + return clientRedirectUrl; + } + + public PrettyName prettyName() { + return prettyName; + } + + public Text description() { + return description; + } + + public List scopes() { + return scopes; + } + + public Optional clientApiKey() { + return Optional.ofNullable(this.clientApiKey); + } + + public Optional restrictions() { + return Optional.ofNullable(this.restriction); + } + + public ClientOwner access() { + return owner; + } + + public Client withClientSecret(ClientSecret clientSecret) { + return new Client( + this.clientUid, + this.clientId, + clientSecret, + this.clientRedirectUrl, + this.prettyName, + this.description, + this.scopes, + this.clientApiKey, + this.owner, + this.restriction + ); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Client client = (Client) o; + return clientUid.equals(client.clientUid) + && clientId.equals(client.clientId) + && clientSecret.equals(client.clientSecret) + && clientRedirectUrl.equals(client.clientRedirectUrl) + && prettyName.equals(client.prettyName) + && description.equals(client.description) + && scopes.equals(client.scopes) + && Objects.equals(clientApiKey, client.clientApiKey) + && owner == client.owner + && restriction == client.restriction; + } + + @Override + public int hashCode() { + return Objects.hash(clientUid, + clientId, + clientSecret, + clientRedirectUrl, + prettyName, + description, + scopes, + clientApiKey, + owner, + restriction + ); + } + + @Override + public String toString() { + return "Client{" + + "clientUid=" + clientUid + + ", clientId=" + clientId + + ", clientRedirectUrl=" + clientRedirectUrl + + ", prettyName=" + prettyName + + ", description=" + description + + ", scopes=" + scopes + + ", clientApiKey=" + clientApiKey + + ", access=" + owner + + ", restriction=" + restriction + + '}'; + } +} + diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientAuthorityFacade.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientAuthorityFacade.java new file mode 100644 index 000000000..1c468a64c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientAuthorityFacade.java @@ -0,0 +1,227 @@ +package it.chalmers.gamma.app.client.domain; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.client.domain.authority.Authority; +import it.chalmers.gamma.app.client.domain.authority.AuthorityName; +import it.chalmers.gamma.app.client.domain.authority.ClientAuthorityRepository; +import it.chalmers.gamma.app.post.PostFacade; +import it.chalmers.gamma.app.post.domain.PostId; +import it.chalmers.gamma.app.post.domain.PostRepository; +import it.chalmers.gamma.app.supergroup.SuperGroupFacade; +import it.chalmers.gamma.app.supergroup.domain.SuperGroup; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupRepository; +import it.chalmers.gamma.app.user.UserFacade; +import it.chalmers.gamma.app.user.domain.GammaUser; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserRepository; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; +import static it.chalmers.gamma.app.authentication.AccessGuard.isLocalRunner; + +@Service +public class ClientAuthorityFacade extends Facade { + + private static final Logger LOGGER = LoggerFactory.getLogger(ClientAuthorityFacade.class); + + private final ClientAuthorityRepository clientAuthorityRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + private final SuperGroupRepository superGroupRepository; + + public ClientAuthorityFacade(AccessGuard accessGuard, + ClientAuthorityRepository clientAuthorityRepository, + UserRepository userRepository, + PostRepository postRepository, + SuperGroupRepository superGroupRepository) { + super(accessGuard); + this.clientAuthorityRepository = clientAuthorityRepository; + this.userRepository = userRepository; + this.postRepository = postRepository; + this.superGroupRepository = superGroupRepository; + } + + public void create(UUID clientUid, String name) throws ClientAuthorityRepository.ClientAuthorityAlreadyExistsException { + this.accessGuard.require(isAdmin()); + + this.clientAuthorityRepository.create(new ClientUid(clientUid), new AuthorityName(name)); + } + + public void delete(UUID clientUid, String name) throws ClientAuthorityNotFoundException { + this.accessGuard.require(isAdmin()); + + try { + this.clientAuthorityRepository.delete(new ClientUid(clientUid), new AuthorityName(name)); + } catch (ClientAuthorityRepository.ClientAuthorityNotFoundException e) { + throw new ClientAuthorityNotFoundException(); + } + } + + public Optional get(UUID clientUid, String name) { + this.accessGuard.require(isAdmin()); + + return this.clientAuthorityRepository.get(new ClientUid(clientUid), new AuthorityName(name)) + .map(ClientAuthorityDTO::new); + } + + @Transactional + public void addSuperGroupToClientAuthority(UUID clientUid, String name, UUID superGroupId) + throws ClientAuthorityNotFoundException, SuperGroupNotFoundException { + this.accessGuard.require(isAdmin()); + + Authority authority = this.clientAuthorityRepository.get(new ClientUid(clientUid), new AuthorityName(name)) + .orElseThrow(ClientAuthorityNotFoundException::new); + + List superGroups = new ArrayList<>(authority.superGroups()); + superGroups.add(this.superGroupRepository.get(new SuperGroupId(superGroupId)) + .orElseThrow(SuperGroupNotFoundException::new)); + + this.clientAuthorityRepository.save(authority.withSuperGroups(superGroups)); + } + + @Transactional + public void addSuperGroupPostToClientAuthority(UUID clientUid, String name, UUID superGroupId, UUID postId) + throws ClientAuthorityNotFoundException, SuperGroupNotFoundException, PostNotFoundException { + this.accessGuard.require(isAdmin()); + + Authority authority = this.clientAuthorityRepository.get(new ClientUid(clientUid), new AuthorityName(name)) + .orElseThrow(ClientAuthorityNotFoundException::new); + + List posts = new ArrayList<>(authority.posts()); + posts.add(new Authority.SuperGroupPost( + this.superGroupRepository.get(new SuperGroupId(superGroupId)) + .orElseThrow(SuperGroupNotFoundException::new), + this.postRepository.get(new PostId(postId)) + .orElseThrow(PostNotFoundException::new) + )); + + this.clientAuthorityRepository.save(authority.withPosts(posts)); + } + + @Transactional + public void addUserToClientAuthority(UUID clientUid, String name, UUID userId) throws ClientAuthorityRepository.ClientAuthorityNotFoundRuntimeException, ClientAuthorityNotFoundException, UserNotFoundException { + this.accessGuard.requireEither( + isAdmin(), + isLocalRunner() + ); + + Authority authority = this.clientAuthorityRepository.get(new ClientUid(clientUid), new AuthorityName(name)) + .orElseThrow(ClientAuthorityNotFoundException::new); + + List newUsersList = new ArrayList<>(authority.users()); + GammaUser newUser = this.userRepository.get(new UserId(userId)) + .orElseThrow(UserNotFoundException::new); + newUsersList.add(newUser); + + this.clientAuthorityRepository.save(authority.withUsers(newUsersList)); + } + + @Transactional + public void removeSuperGroupFromClientAuthority(UUID clientUid, String name, UUID superGroupId) + throws ClientAuthorityNotFoundException { + this.accessGuard.require(isAdmin()); + + Authority authority = this.clientAuthorityRepository.get(new ClientUid(clientUid), new AuthorityName(name)) + .orElseThrow(ClientAuthorityNotFoundException::new); + + List newSuperGroups = new ArrayList<>(authority.superGroups()); + for (int i = 0; i < newSuperGroups.size(); i++) { + if (newSuperGroups.get(i).id().value().equals(superGroupId)) { + newSuperGroups.remove(i); + break; + } + } + + this.clientAuthorityRepository.save(authority.withSuperGroups(newSuperGroups)); + } + + @Transactional + public void removeSuperGroupPostFromClientAuthority(UUID clientUid, String name, UUID superGroupId, UUID postId) + throws ClientAuthorityNotFoundException { + this.accessGuard.require(isAdmin()); + + Authority authority = this.clientAuthorityRepository.get(new ClientUid(clientUid), new AuthorityName(name)) + .orElseThrow(ClientAuthorityNotFoundException::new); + + List newPosts = new ArrayList<>(authority.posts()); + for (int i = 0; i < newPosts.size(); i++) { + Authority.SuperGroupPost superGroupPost = newPosts.get(i); + if (superGroupPost.post().id().value().equals(postId) + && superGroupPost.superGroup().id().value().equals(superGroupId)) { + newPosts.remove(i); + break; + } + } + + this.clientAuthorityRepository.save(authority.withPosts(newPosts)); + } + + public List getAll(UUID clientUid) { + this.accessGuard.require(isAdmin()); + + var authorities = this.clientAuthorityRepository.getAllByClient(new ClientUid(clientUid)); + + return authorities.stream().map(ClientAuthorityDTO::new).toList(); + } + + @Transactional + public void removeUserFromClientAuthority(UUID clientUid, String name, UUID userId) throws ClientAuthorityNotFoundException { + this.accessGuard.require(isAdmin()); + + Authority authority = this.clientAuthorityRepository.get(new ClientUid(clientUid), new AuthorityName(name)) + .orElseThrow(ClientAuthorityNotFoundException::new); + + List newUsers = new ArrayList<>(authority.users()); + for (int i = 0; i < newUsers.size(); i++) { + if (newUsers.get(i).id().value().equals(userId)) { + newUsers.remove(i); + break; + } + } + + this.clientAuthorityRepository.save(authority.withUsers(newUsers)); + } + public record SuperGroupPostDTO(SuperGroupFacade.SuperGroupDTO superGroup, PostFacade.PostDTO post) { + public SuperGroupPostDTO(Authority.SuperGroupPost post) { + this(new SuperGroupFacade.SuperGroupDTO(post.superGroup()), new PostFacade.PostDTO(post.post())); + } + } + public record ClientAuthorityDTO( + UUID clientUid, + String authorityName, + List superGroups, + List users, + List posts) { + + public ClientAuthorityDTO(Authority authority) { + this(authority.client().clientUid().value(), + authority.name().value(), + authority.superGroups().stream().map(SuperGroupFacade.SuperGroupDTO::new).toList(), + authority.users().stream().map(UserFacade.UserDTO::new).toList(), + authority.posts().stream().map(SuperGroupPostDTO::new).toList()); + } + } + + public static class ClientAuthorityNotFoundException extends Exception { + } + + public static class SuperGroupNotFoundException extends Exception { + } + + public static class UserNotFoundException extends Exception { + } + + public static class PostNotFoundException extends Exception { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientId.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientId.java new file mode 100644 index 000000000..bd859129e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientId.java @@ -0,0 +1,24 @@ +package it.chalmers.gamma.app.client.domain; + +import it.chalmers.gamma.util.TokenUtils; + +import java.util.Objects; + +/** + * Id for the client that is used in the OAuth2 flow. + */ +public record ClientId(String value) { + + public ClientId { + Objects.requireNonNull(value); + } + + public static ClientId generate() { + String id = TokenUtils.generateToken(75, TokenUtils.CharacterTypes.LOWERCASE, + TokenUtils.CharacterTypes.UPPERCASE, + TokenUtils.CharacterTypes.NUMBERS + ); + return new ClientId(id); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientOwner.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientOwner.java new file mode 100644 index 000000000..aa91e633c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientOwner.java @@ -0,0 +1,4 @@ +package it.chalmers.gamma.app.client.domain; + +public interface ClientOwner { +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientOwnerOfficial.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientOwnerOfficial.java new file mode 100644 index 000000000..d068836c8 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientOwnerOfficial.java @@ -0,0 +1,5 @@ +package it.chalmers.gamma.app.client.domain; + +public class ClientOwnerOfficial implements ClientOwner { + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientRedirectUrl.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientRedirectUrl.java new file mode 100644 index 000000000..98ca6413e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientRedirectUrl.java @@ -0,0 +1,13 @@ +package it.chalmers.gamma.app.client.domain; + +public record ClientRedirectUrl(String value) { + + public ClientRedirectUrl { + if (value == null) { + throw new NullPointerException(); + } + + //TODO: add more validation + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientRepository.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientRepository.java new file mode 100644 index 000000000..9c371ce99 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientRepository.java @@ -0,0 +1,45 @@ +package it.chalmers.gamma.app.client.domain; + +import it.chalmers.gamma.app.apikey.domain.ApiKeyToken; +import it.chalmers.gamma.app.client.domain.restriction.ClientRestriction; +import it.chalmers.gamma.app.user.domain.UserId; + +import java.util.List; +import java.util.Optional; + +public interface ClientRepository { + + void save(Client client) + throws AuthorityNotFoundRuntimeException, UserNotFoundRuntimeException, ClientIdAlreadyExistsRuntimeException; + + void delete(ClientUid clientId) throws ClientNotFoundException; + + List getAll(); + + Optional get(ClientUid clientUid); + + Optional get(ClientId clientId); + + void addClientApproval(UserId userId, ClientUid clientUid); + + boolean isApprovedByUser(UserId userId, ClientUid clientUid); + + List getClientsByUserApproved(UserId id); + + void deleteUserApproval(ClientUid clientUid, UserId userId); + + Optional getByApiKey(ApiKeyToken apiKeyToken); + + class ClientNotFoundException extends Exception { + } + + class ClientIdAlreadyExistsRuntimeException extends RuntimeException { + } + + class AuthorityNotFoundRuntimeException extends RuntimeException { + } + + class UserNotFoundRuntimeException extends RuntimeException { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientSecret.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientSecret.java new file mode 100644 index 000000000..b79850b73 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientSecret.java @@ -0,0 +1,28 @@ +package it.chalmers.gamma.app.client.domain; + +import it.chalmers.gamma.util.TokenUtils; + +import java.util.Objects; + +public record ClientSecret(String value) { + + public ClientSecret { + Objects.requireNonNull(value); + //Should I not encode this? + if (!value.startsWith("{noop}")) { + throw new IllegalArgumentException(); + } + } + + public static ClientSecret generate() { + //TODO Should I really have a {noop} here? that's spring specific. + String value = "{noop}" + TokenUtils.generateToken( + 75, + TokenUtils.CharacterTypes.LOWERCASE, + TokenUtils.CharacterTypes.UPPERCASE, + TokenUtils.CharacterTypes.NUMBERS + ); + return new ClientSecret(value); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientUid.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientUid.java new file mode 100644 index 000000000..0eb63ffd7 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientUid.java @@ -0,0 +1,29 @@ +package it.chalmers.gamma.app.client.domain; + +import it.chalmers.gamma.app.common.Id; + +import java.util.Objects; +import java.util.UUID; + +/** + * UUID value for Client. + */ +public record ClientUid(UUID value) implements Id { + + public ClientUid { + Objects.requireNonNull(value); + } + + public static ClientUid generate() { + return new ClientUid(UUID.randomUUID()); + } + + public static ClientUid valueOf(String uid) { + return new ClientUid(UUID.fromString(uid)); + } + + @Override + public String getValue() { + return value.toString(); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientUserOwner.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientUserOwner.java new file mode 100644 index 000000000..d24627d2c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/ClientUserOwner.java @@ -0,0 +1,6 @@ +package it.chalmers.gamma.app.client.domain; + +import it.chalmers.gamma.app.user.domain.UserId; + +public record ClientUserOwner(UserId userId) implements ClientOwner { +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/Scope.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/Scope.java new file mode 100644 index 000000000..6a9100cdf --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/Scope.java @@ -0,0 +1,5 @@ +package it.chalmers.gamma.app.client.domain; + +public enum Scope { + PROFILE, EMAIL +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/approval/ClientApprovals.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/approval/ClientApprovals.java new file mode 100644 index 000000000..6f4e165c4 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/approval/ClientApprovals.java @@ -0,0 +1,16 @@ +package it.chalmers.gamma.app.client.domain.approval; + +import it.chalmers.gamma.app.client.domain.Client; +import it.chalmers.gamma.app.user.domain.GammaUser; + +import java.util.List; +import java.util.Objects; + +public record ClientApprovals(Client client, List approvals) { + + public ClientApprovals { + Objects.requireNonNull(client); + Objects.requireNonNull(approvals); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/approval/ClientApprovalsRepository.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/approval/ClientApprovalsRepository.java new file mode 100644 index 000000000..d9b10cd03 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/approval/ClientApprovalsRepository.java @@ -0,0 +1,14 @@ +package it.chalmers.gamma.app.client.domain.approval; + +import it.chalmers.gamma.app.client.domain.ClientId; +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.user.domain.GammaUser; + +import java.util.List; + +public interface ClientApprovalsRepository { + + List getAllByClientId(ClientId clientId); + List getAllByClientUid(ClientUid clientUid); + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/authority/Authority.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/authority/Authority.java new file mode 100644 index 000000000..680e010aa --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/authority/Authority.java @@ -0,0 +1,30 @@ +package it.chalmers.gamma.app.client.domain.authority; + + +import io.soabase.recordbuilder.core.RecordBuilder; +import it.chalmers.gamma.app.client.domain.Client; +import it.chalmers.gamma.app.post.domain.Post; +import it.chalmers.gamma.app.supergroup.domain.SuperGroup; +import it.chalmers.gamma.app.user.domain.GammaUser; + +import java.util.List; +import java.util.Objects; + +@RecordBuilder +public record Authority(Client client, + AuthorityName name, + List posts, + List superGroups, + List users) implements AuthorityBuilder.With { + + public Authority { + Objects.requireNonNull(client); + Objects.requireNonNull(name); + Objects.requireNonNull(posts); + Objects.requireNonNull(superGroups); + Objects.requireNonNull(users); + } + + public record SuperGroupPost(SuperGroup superGroup, Post post) { + } +} \ No newline at end of file diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/authority/AuthorityName.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/authority/AuthorityName.java new file mode 100644 index 000000000..5b0ced92c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/authority/AuthorityName.java @@ -0,0 +1,23 @@ +package it.chalmers.gamma.app.client.domain.authority; + +import it.chalmers.gamma.app.common.Id; + +public record AuthorityName(String value) implements Id { + + public AuthorityName { + if (value == null) { + throw new NullPointerException("Authority name cannot be null"); + } else if (!value.matches("^([0-9a-z]{2,30})$")) { + throw new IllegalArgumentException("Input: " + value + "; Authority nane must have letter ranging a - z, and be between size 5 and 30 to be valid"); + } + } + + public static AuthorityName valueOf(String name) { + return new AuthorityName(name); + } + + @Override + public String getValue() { + return this.value; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/authority/AuthorityType.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/authority/AuthorityType.java new file mode 100644 index 000000000..6526fc4f0 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/authority/AuthorityType.java @@ -0,0 +1,5 @@ +package it.chalmers.gamma.app.client.domain.authority; + +public enum AuthorityType { + AUTHORITY, SUPERGROUP, GROUP +} \ No newline at end of file diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/authority/ClientAuthorityRepository.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/authority/ClientAuthorityRepository.java new file mode 100644 index 000000000..cf05637f3 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/authority/ClientAuthorityRepository.java @@ -0,0 +1,41 @@ +package it.chalmers.gamma.app.client.domain.authority; + +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.user.domain.UserId; + +import java.util.List; +import java.util.Optional; + +public interface ClientAuthorityRepository { + + void create(ClientUid clientUid, AuthorityName authorityName) throws ClientAuthorityAlreadyExistsException; + + void delete(ClientUid clientUid, AuthorityName authorityName) throws ClientAuthorityNotFoundException; + + void save(Authority authority) + throws ClientAuthorityNotFoundRuntimeException, NotCompleteClientAuthorityException; + + List getAllByClient(ClientUid clientUid); + List getAllByUser(ClientUid clientUid, UserId userId); + Optional get(ClientUid clientUid, AuthorityName authorityName); + + class ClientAuthorityAlreadyExistsException extends Exception { + public ClientAuthorityAlreadyExistsException(String value) { + super("Authority level: " + value + " already exists"); + } + } + + class ClientAuthorityNotFoundException extends Exception { + } + + class ClientAuthorityNotFoundRuntimeException extends RuntimeException { + } + + /** + * Can be avoided if you check that supergroups, posts, and users actually exists. + * It happens when linking an authority level with one of the above, and it is not found in database. + */ + class NotCompleteClientAuthorityException extends RuntimeException { } + + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/restriction/ClientRestriction.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/restriction/ClientRestriction.java new file mode 100644 index 000000000..468386d37 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/restriction/ClientRestriction.java @@ -0,0 +1,14 @@ +package it.chalmers.gamma.app.client.domain.restriction; + +import io.soabase.recordbuilder.core.RecordBuilder; +import it.chalmers.gamma.app.client.domain.authority.Authority; +import it.chalmers.gamma.app.post.domain.Post; +import it.chalmers.gamma.app.supergroup.domain.SuperGroup; +import it.chalmers.gamma.app.user.domain.GammaUser; + +import java.util.List; + +@RecordBuilder +public record ClientRestriction(ClientRestrictionId id, List superGroups) { + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/client/domain/restriction/ClientRestrictionId.java b/backend/src/main/java/it/chalmers/gamma/app/client/domain/restriction/ClientRestrictionId.java new file mode 100644 index 000000000..73d4c1a4d --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/client/domain/restriction/ClientRestrictionId.java @@ -0,0 +1,13 @@ +package it.chalmers.gamma.app.client.domain.restriction; + +import it.chalmers.gamma.app.client.domain.ClientUid; + +import java.util.UUID; + +public record ClientRestrictionId(UUID value) { + + public static ClientRestrictionId generate() { + return new ClientRestrictionId(UUID.randomUUID()); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/common/Email.java b/backend/src/main/java/it/chalmers/gamma/app/common/Email.java new file mode 100644 index 000000000..2456d89e9 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/common/Email.java @@ -0,0 +1,24 @@ +package it.chalmers.gamma.app.common; + +import it.chalmers.gamma.app.user.domain.UserIdentifier; + +import java.io.Serializable; +import java.util.regex.Pattern; + +public record Email(String value) implements UserIdentifier, Serializable { + + private static final Pattern emailPattern = Pattern.compile("^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$"); + + public Email { + if (value == null) { + throw new NullPointerException("Email cannot be null"); + } else if (!emailPattern.matcher(value).matches()) { + throw new IllegalArgumentException("Email: [" + value + "] does not look valid"); + } + } + + public static boolean isValidEmail(String possibleEmail) { + return emailPattern.matcher(possibleEmail).matches(); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/common/Id.java b/backend/src/main/java/it/chalmers/gamma/app/common/Id.java new file mode 100644 index 000000000..83cd8efc2 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/common/Id.java @@ -0,0 +1,9 @@ +package it.chalmers.gamma.app.common; + +import java.io.Serializable; + +public interface Id extends Serializable { + + S getValue(); + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/common/PrettyName.java b/backend/src/main/java/it/chalmers/gamma/app/common/PrettyName.java new file mode 100644 index 000000000..7dc59dcf9 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/common/PrettyName.java @@ -0,0 +1,15 @@ +package it.chalmers.gamma.app.common; + +import java.io.Serializable; + +public record PrettyName(String value) implements Serializable { + + public PrettyName { + if (value == null) { + throw new NullPointerException("Pretty name cannot be null"); + } else if (value.length() < 2 || value.length() > 50) { + throw new IllegalArgumentException("Pretty name must be between 3 and 50 in length"); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/common/Text.java b/backend/src/main/java/it/chalmers/gamma/app/common/Text.java new file mode 100644 index 000000000..cc5f68f0a --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/common/Text.java @@ -0,0 +1,20 @@ +package it.chalmers.gamma.app.common; + +import java.util.Objects; + +public record Text(TextValue sv, + TextValue en) { + + public Text { + Objects.requireNonNull(sv); + Objects.requireNonNull(en); + } + + public Text(String sv, String en) { + this(TextValue.valueOf(sv), TextValue.valueOf(en)); + } + + public Text() { + this(TextValue.empty(), TextValue.empty()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/it/chalmers/gamma/app/common/TextId.java b/backend/src/main/java/it/chalmers/gamma/app/common/TextId.java new file mode 100644 index 000000000..3c577ff6d --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/common/TextId.java @@ -0,0 +1,20 @@ +package it.chalmers.gamma.app.common; + +import java.util.Objects; +import java.util.UUID; + +public record TextId(UUID value) implements Id { + + public TextId { + Objects.requireNonNull(value); + } + + public static TextId generate() { + return new TextId(UUID.randomUUID()); + } + + @Override + public UUID getValue() { + return this.value; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/common/TextValue.java b/backend/src/main/java/it/chalmers/gamma/app/common/TextValue.java new file mode 100644 index 000000000..10157b773 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/common/TextValue.java @@ -0,0 +1,21 @@ +package it.chalmers.gamma.app.common; + +public record TextValue(String value) { + + public TextValue { + if (value == null) { + throw new NullPointerException("Text value cannot be null"); + } else if (value.length() > 2048) { + throw new IllegalArgumentException("Text value max length is 2048"); + } + } + + public static TextValue empty() { + return new TextValue(""); + } + + public static TextValue valueOf(String value) { + return new TextValue(value); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/goldapps/GoldappsFacade.java b/backend/src/main/java/it/chalmers/gamma/app/goldapps/GoldappsFacade.java new file mode 100644 index 000000000..916064232 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/goldapps/GoldappsFacade.java @@ -0,0 +1,164 @@ +package it.chalmers.gamma.app.goldapps; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.apikey.domain.ApiKeyType; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.group.domain.GroupMember; +import it.chalmers.gamma.app.group.domain.GroupRepository; +import it.chalmers.gamma.app.post.domain.Post; +import it.chalmers.gamma.app.supergroup.domain.SuperGroup; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.user.domain.GammaUser; +import org.springframework.stereotype.Service; + +import java.util.*; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isApi; + +@Service +public class GoldappsFacade extends Facade { + + private final GroupRepository groupRepository; + + public GoldappsFacade(AccessGuard accessGuard, + GroupRepository groupRepository) { + super(accessGuard); + this.groupRepository = groupRepository; + } + + public List getSuperGroupsByTypes(List superGroupTypes) { + this.accessGuard.require(isApi(ApiKeyType.GOLDAPPS)); + + // grupper + // grupper som är activa + + + return null; + } + + /** + * Get all super groups that have the provided types + * and members that are a part of groups that has each supergroup + */ + public List getActiveSuperGroups(List superGroupTypes) { + this.accessGuard.require(isApi(ApiKeyType.GOLDAPPS)); + + Map superGroupMap = new HashMap<>(); + + this.groupRepository.getAll() + .stream() + .filter(group -> superGroupTypes.contains(group.superGroup().type().value())) + .forEach(group -> { + List activeGroupMember = group.groupMembers() + .stream() + .filter(groupMember -> !groupMember.user().extended().locked()) + //TODO: +// .filter(groupMember -> groupMember.user().extended().gdprTrained()) + .map(GoldappsUserPostDTO::new) + .toList(); + + SuperGroupId superGroupId = group.superGroup().id(); + if (!superGroupMap.containsKey(superGroupId)) { + superGroupMap.put( + superGroupId, + new SuperGroupWithMembers( + group.superGroup(), + new HashSet<>(activeGroupMember) + )); + } else { + superGroupMap.get(superGroupId).members.addAll(activeGroupMember); + } + }); + + return superGroupMap + .values() + .stream() + .map(superGroupWithMembers -> new GoldappsSuperGroupDTO( + superGroupWithMembers.superGroup, + new ArrayList<>(superGroupWithMembers.members) + )) + .toList(); + } + + /** + * Returns the users that are active right now. + * Takes in a list of super group types to help determine + * what kinds of groups that are deemed active. + * User must also be not locked, and have participated in gdpr training. + */ + public List getActiveUsers(List superGroupTypes) { + this.accessGuard.require(isApi(ApiKeyType.GOLDAPPS)); + + return this.groupRepository.getAll() + .stream() + .filter(group -> superGroupTypes.contains(group.superGroup().type().value())) + .flatMap(group -> group.groupMembers().stream()) + .map(GroupMember::user) + .distinct() + .filter(user -> !user.extended().locked()) + //TODO: +// .filter(user -> user.extended().gdprTrained()) + .map(GoldappsUserDTO::new) + .toList(); + } + + public record GoldappsPostDTO(UUID postId, + String svText, + String enText, + String emailPrefix) { + public GoldappsPostDTO(Post post) { + this(post.id().value(), + post.name().sv().value(), + post.name().en().value(), + post.emailPrefix().value()); + } + } + + public record GoldappsUserPostDTO(GoldappsPostDTO post, + GoldappsUserDTO user) { + public GoldappsUserPostDTO(GroupMember groupMember) { + this(new GoldappsPostDTO(groupMember.post()), + new GoldappsUserDTO(groupMember.user())); + } + } + + public record GoldappsUserDTO(String email, + String cid, + String firstName, + String lastName, + String nick) { + public GoldappsUserDTO(GammaUser user) { + this(user.extended().email().value(), + user.cid().value(), + user.firstName().value(), + user.lastName().value(), + user.nick().value()); + } + } + + public record GoldappsSuperGroupDTO(String name, + String prettyName, + String type, + List members) { + public GoldappsSuperGroupDTO(SuperGroup superGroup, List members) { + this(superGroup.name().value(), + superGroup.prettyName().value(), + superGroup.type().value(), + members + ); + } + + } + + private static class SuperGroupWithMembers { + private final SuperGroup superGroup; + private final Set members; + + private SuperGroupWithMembers(SuperGroup superGroup, Set members) { + this.superGroup = superGroup; + this.members = members; + } + } + + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/group/GroupFacade.java b/backend/src/main/java/it/chalmers/gamma/app/group/GroupFacade.java new file mode 100644 index 000000000..284596555 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/group/GroupFacade.java @@ -0,0 +1,234 @@ +package it.chalmers.gamma.app.group; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.UnexpectedRuntimeException; +import it.chalmers.gamma.app.apikey.domain.ApiKeyType; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.group.domain.*; +import it.chalmers.gamma.app.post.PostFacade; +import it.chalmers.gamma.app.post.domain.PostId; +import it.chalmers.gamma.app.post.domain.PostRepository; +import it.chalmers.gamma.app.settings.domain.SettingsRepository; +import it.chalmers.gamma.app.supergroup.SuperGroupFacade; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupRepository; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupType; +import it.chalmers.gamma.app.user.UserFacade; +import it.chalmers.gamma.app.user.domain.Name; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserRepository; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static it.chalmers.gamma.app.authentication.AccessGuard.*; + +@Service +public class GroupFacade extends Facade { + + private static final Logger LOGGER = LoggerFactory.getLogger(GroupFacade.class); + + private final GroupRepository groupRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + private final SuperGroupRepository superGroupRepository; + private final SettingsRepository settingsRepository; + + public GroupFacade(AccessGuard accessGuard, + GroupRepository groupRepository, + UserRepository userRepository, + PostRepository postRepository, + SuperGroupRepository superGroupRepository, SettingsRepository settingsRepository) { + super(accessGuard); + this.groupRepository = groupRepository; + this.userRepository = userRepository; + this.postRepository = postRepository; + this.superGroupRepository = superGroupRepository; + this.settingsRepository = settingsRepository; + } + + @Transactional + public void create(NewGroup newGroup) throws GroupAlreadyExistsException { + accessGuard.require(isAdmin()); + + Group group = new Group( + GroupId.generate(), + 0, + new Name(newGroup.name), + new PrettyName(newGroup.prettyName), + this.superGroupRepository.get(new SuperGroupId(newGroup.superGroup)) + .orElseThrow(SuperGroupNotFoundRuntimeException::new), + new ArrayList<>(), + Optional.empty(), + Optional.empty() + ); + + try { + this.groupRepository.save(group); + } catch (GroupRepository.GroupNameAlreadyExistsException e) { + throw new GroupAlreadyExistsException(); + } + } + + @Transactional + public void update(UpdateGroup updateGroup) throws GroupAlreadyExistsException { + accessGuard.require(isAdmin()); + + GroupId groupId = new GroupId(updateGroup.id); + Group oldGroup = this.groupRepository.get(groupId) + .orElseThrow(GroupNotFoundRuntimeException::new); + Group newGroup = oldGroup.with() + .version(updateGroup.version) + .name(new Name(updateGroup.name)) + .prettyName(new PrettyName(updateGroup.prettyName)) + .superGroup(this.superGroupRepository.get(new SuperGroupId(updateGroup.superGroup)) + .orElseThrow(SuperGroupNotFoundRuntimeException::new)) + .build(); + + try { + this.groupRepository.save(newGroup); + } catch (GroupRepository.GroupNameAlreadyExistsException e) { + throw new GroupAlreadyExistsException(); + } + } + + @Transactional + public void setMembers(UUID groupId, List newMembers) throws GroupNotFoundRuntimeException { + accessGuard.require(isAdmin()); + + Group oldGroup = this.groupRepository.get(new GroupId(groupId)) + .orElseThrow(GroupNotFoundRuntimeException::new); + + List newGroupMembers = new ArrayList<>(); + + for (ShallowMember shallowMember : newMembers) { + newGroupMembers.add(new GroupMember( + this.postRepository.get(new PostId(shallowMember.postId)) + .orElseThrow(PostNotFoundRuntimeException::new), + new UnofficialPostName(shallowMember.unofficialPostName == null ? "" : shallowMember.unofficialPostName), + this.userRepository.get(new UserId(shallowMember.userId)) + .orElseThrow(UserNotFoundRuntimeException::new) + ) + ); + } + + try { + this.groupRepository.save(oldGroup.withGroupMembers(newGroupMembers)); + } catch (GroupRepository.GroupNameAlreadyExistsException e) { + //Unexpected error since it can only be thrown if id or name is the same as any other group. + LOGGER.error("GroupAlreadyExistsException when just trying to update withGroupMembers", e); + throw new UnexpectedRuntimeException(); + } + } + + @Transactional + public void delete(UUID id) throws GroupNotFoundRuntimeException { + accessGuard.require(isAdmin()); + + try { + this.groupRepository.delete(new GroupId(id)); + } catch (GroupRepository.GroupNotFoundException e) { + throw new GroupNotFoundRuntimeException(); + } + } + + public Optional getWithMembers(UUID groupId) { + accessGuard.require(isSignedIn()); + + return this.groupRepository.get(new GroupId(groupId)).map(GroupWithMembersDTO::new); + } + + public List getAll() { + accessGuard.requireEither(isSignedIn(), isClientApi()); + + return this.groupRepository.getAll().stream().map(GroupDTO::new).toList(); + } + + public List getAllForInfoApi() { + accessGuard.require(isApi(ApiKeyType.INFO)); + + List allowedSuperGroupType = this.settingsRepository.getSettings().infoSuperGroupTypes(); + + return this.groupRepository.getAll() + .stream() + .filter(group -> allowedSuperGroupType.contains(group.superGroup().type())) + .map(GroupWithMembersDTO::new) + .toList(); + } + + public List getAllBySuperGroup(UUID superGroupId) { + accessGuard.require(isSignedIn()); + + return this.groupRepository.getAllBySuperGroup(new SuperGroupId(superGroupId)) + .stream() + .map(GroupWithMembersDTO::new) + .toList(); + } + + public record NewGroup(String name, + String prettyName, + UUID superGroup) { + } + + public record UpdateGroup(UUID id, + int version, + String name, + String prettyName, + UUID superGroup) { + } + + public record ShallowMember(UUID userId, UUID postId, String unofficialPostName) { + } + + public record GroupMemberDTO(UserFacade.UserDTO user, PostFacade.PostDTO post, String unofficialPostName) { + public GroupMemberDTO(GroupMember groupMember) { + this(new UserFacade.UserDTO(groupMember.user()), new PostFacade.PostDTO(groupMember.post()), groupMember.unofficialPostName().value()); + } + } + + public record GroupDTO(UUID id, String name, String prettyName, SuperGroupFacade.SuperGroupDTO superGroup) { + public GroupDTO(Group group) { + this(group.id().value(), + group.name().value(), + group.prettyName().value(), + new SuperGroupFacade.SuperGroupDTO(group.superGroup()) + ); + } + } + + public record GroupWithMembersDTO(UUID id, int version, String name, String prettyName, + List groupMembers, SuperGroupFacade.SuperGroupDTO superGroup) { + public GroupWithMembersDTO(Group group) { + this(group.id().value(), + group.version(), + group.name().value(), + group.prettyName().value(), + group.groupMembers().stream().map(GroupMemberDTO::new).toList(), + new SuperGroupFacade.SuperGroupDTO(group.superGroup()) + ); + } + } + + public static class GroupAlreadyExistsException extends Exception { + } + + public static class UserNotFoundRuntimeException extends RuntimeException { + } + + public static class PostNotFoundRuntimeException extends RuntimeException { + } + + public static class GroupNotFoundRuntimeException extends RuntimeException { + } + + public static class SuperGroupNotFoundRuntimeException extends RuntimeException { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/group/domain/EmailPrefix.java b/backend/src/main/java/it/chalmers/gamma/app/group/domain/EmailPrefix.java new file mode 100644 index 000000000..9b2a3400e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/group/domain/EmailPrefix.java @@ -0,0 +1,14 @@ +package it.chalmers.gamma.app.group.domain; + +public record EmailPrefix(String value) { + + public EmailPrefix { + if (value != null && !value.matches("^$|^(?:\\w+|\\w+\\.\\w+)+$")) { + throw new IllegalArgumentException("Email prefix most be letters of a - z, and each word must be seperated by a dot"); + } + } + + public static EmailPrefix none() { + return new EmailPrefix(""); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/group/domain/Group.java b/backend/src/main/java/it/chalmers/gamma/app/group/domain/Group.java new file mode 100644 index 000000000..a1de5d6a9 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/group/domain/Group.java @@ -0,0 +1,31 @@ +package it.chalmers.gamma.app.group.domain; + +import io.soabase.recordbuilder.core.RecordBuilder; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.image.domain.ImageUri; +import it.chalmers.gamma.app.supergroup.domain.SuperGroup; +import it.chalmers.gamma.app.user.domain.Name; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@RecordBuilder +public record Group(GroupId id, + int version, + Name name, + PrettyName prettyName, + SuperGroup superGroup, + List groupMembers, + Optional avatarUri, + Optional bannerUri) implements GroupBuilder.With { + + public Group { + Objects.requireNonNull(id); + Objects.requireNonNull(prettyName); + Objects.requireNonNull(superGroup); + Objects.requireNonNull(avatarUri); + Objects.requireNonNull(bannerUri); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/group/domain/GroupId.java b/backend/src/main/java/it/chalmers/gamma/app/group/domain/GroupId.java new file mode 100644 index 000000000..b1d2c65b2 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/group/domain/GroupId.java @@ -0,0 +1,26 @@ +package it.chalmers.gamma.app.group.domain; + +import it.chalmers.gamma.app.common.Id; + +import java.util.Objects; +import java.util.UUID; + +public record GroupId(UUID value) implements Id { + + public GroupId { + Objects.requireNonNull(value); + } + + public static GroupId generate() { + return new GroupId(UUID.randomUUID()); + } + + public static GroupId valueOf(String value) { + return new GroupId(UUID.fromString(value)); + } + + @Override + public UUID getValue() { + return this.value; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/group/domain/GroupMember.java b/backend/src/main/java/it/chalmers/gamma/app/group/domain/GroupMember.java new file mode 100644 index 000000000..36ab93ef8 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/group/domain/GroupMember.java @@ -0,0 +1,19 @@ +package it.chalmers.gamma.app.group.domain; + +import io.soabase.recordbuilder.core.RecordBuilder; +import it.chalmers.gamma.app.post.domain.Post; +import it.chalmers.gamma.app.user.domain.GammaUser; + +import java.util.Objects; + +@RecordBuilder +public record GroupMember(Post post, + UnofficialPostName unofficialPostName, + GammaUser user) implements GroupMemberBuilder.With { + + public GroupMember { + Objects.requireNonNull(post); + Objects.requireNonNull(unofficialPostName); + Objects.requireNonNull(user); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/group/domain/GroupRepository.java b/backend/src/main/java/it/chalmers/gamma/app/group/domain/GroupRepository.java new file mode 100644 index 000000000..0969d5cba --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/group/domain/GroupRepository.java @@ -0,0 +1,44 @@ +package it.chalmers.gamma.app.group.domain; + +import it.chalmers.gamma.app.post.domain.PostId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserMembership; + +import java.util.List; +import java.util.Optional; + +public interface GroupRepository { + + void save(Group group) + throws GroupNameAlreadyExistsException, SuperGroupNotFoundRuntimeException, + UserNotFoundRuntimeException, PostNotFoundRuntimeException; + + void delete(GroupId groupId) throws GroupNotFoundException; + + List getAll(); + + List getAllBySuperGroup(SuperGroupId superGroupId); + + List getAllByPost(PostId postId); + + List getAllByUser(UserId userId); + + Optional get(GroupId groupId); + + class GroupNotFoundException extends Exception { + } + + class GroupNameAlreadyExistsException extends Exception { + } + + class SuperGroupNotFoundRuntimeException extends RuntimeException { + } + + class UserNotFoundRuntimeException extends RuntimeException { + } + + class PostNotFoundRuntimeException extends RuntimeException { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/group/domain/UnofficialPostName.java b/backend/src/main/java/it/chalmers/gamma/app/group/domain/UnofficialPostName.java new file mode 100644 index 000000000..79c77c34f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/group/domain/UnofficialPostName.java @@ -0,0 +1,15 @@ +package it.chalmers.gamma.app.group.domain; + +public record UnofficialPostName(String value) { + + public UnofficialPostName { + if ("".equals(value)) { + value = null; + } + } + + public static UnofficialPostName none() { + return new UnofficialPostName(null); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/image/ImageFacade.java b/backend/src/main/java/it/chalmers/gamma/app/image/ImageFacade.java new file mode 100644 index 000000000..22ee8feaa --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/image/ImageFacade.java @@ -0,0 +1,146 @@ +package it.chalmers.gamma.app.image; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.group.domain.Group; +import it.chalmers.gamma.app.group.domain.GroupId; +import it.chalmers.gamma.app.group.domain.GroupRepository; +import it.chalmers.gamma.app.image.domain.Image; +import it.chalmers.gamma.app.image.domain.ImageService; +import it.chalmers.gamma.app.image.domain.ImageUri; +import it.chalmers.gamma.app.image.domain.UserAvatarRepository; +import it.chalmers.gamma.app.user.domain.GammaUser; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserRepository; +import it.chalmers.gamma.security.authentication.AuthenticationExtractor; +import it.chalmers.gamma.security.authentication.GammaAuthentication; +import it.chalmers.gamma.security.authentication.UserAuthentication; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.UUID; + +import static it.chalmers.gamma.app.authentication.AccessGuard.*; + +@Service +public class ImageFacade extends Facade { + + private static final Logger LOGGER = LoggerFactory.getLogger(ImageFacade.class); + + private final ImageService imageService; + private final UserRepository userRepository; + private final GroupRepository groupRepository; + private final UserAvatarRepository userAvatarRepository; + + public ImageFacade(AccessGuard accessGuard, + ImageService imageService, + UserRepository userRepository, + GroupRepository groupRepository, + UserAvatarRepository userAvatarRepository) { + super(accessGuard); + this.imageService = imageService; + this.userRepository = userRepository; + this.groupRepository = groupRepository; + this.userAvatarRepository = userAvatarRepository; + } + + public void setGroupBanner(UUID groupId, Image image) throws ImageService.ImageCouldNotBeSavedException { + Group group = this.groupRepository.get(new GroupId(groupId)).orElseThrow(); + accessGuard.requireEither(isAdmin(), isSignedInUserMemberOfGroup(group)); + + ImageUri imageUri = this.imageService.saveImage(image); + try { + this.groupRepository.save(group.withBannerUri(Optional.of(imageUri))); + } catch (GroupRepository.GroupNameAlreadyExistsException e) { + e.printStackTrace(); + } + } + + public ImageDetails getGroupBanner(UUID groupId) { + Group group = this.groupRepository.get(new GroupId(groupId)).orElseThrow(); + return new ImageDetails( + this.imageService.getImage( + group.bannerUri().orElse(ImageUri.defaultGroupBanner()) + ) + ); + } + + public void removeGroupBanner(UUID groupId) { + Group group = this.groupRepository.get(new GroupId(groupId)).orElseThrow(); + accessGuard.requireEither(isAdmin(), isSignedInUserMemberOfGroup(group)); + + ImageUri imageUri = group.bannerUri().orElseThrow(); + + try { + this.groupRepository.save(group.withBannerUri(Optional.empty())); + this.imageService.removeImage(imageUri); + } catch (ImageService.ImageCouldNotBeRemovedException | GroupRepository.GroupNameAlreadyExistsException e) { + throw new RuntimeException(e); + } + } + + public void setGroupAvatar(UUID groupId, Image image) throws ImageService.ImageCouldNotBeSavedException { + Group group = this.groupRepository.get(new GroupId(groupId)).orElseThrow(); + accessGuard.requireEither(isAdmin(), isSignedInUserMemberOfGroup(group)); + + ImageUri imageUri = this.imageService.saveImage(image); + try { + this.groupRepository.save(group.withAvatarUri(Optional.of(imageUri))); + } catch (GroupRepository.GroupNameAlreadyExistsException e) { + e.printStackTrace(); + } + } + + public ImageDetails getGroupAvatar(UUID groupId) { + Group group = this.groupRepository.get(new GroupId(groupId)).orElseThrow(); + return new ImageDetails( + this.imageService.getImage( + group.avatarUri().orElse(ImageUri.defaultGroupAvatar()) + ) + ); + } + + public void removeGroupAvatar(UUID groupId) { + Group group = this.groupRepository.get(new GroupId(groupId)).orElseThrow(); + accessGuard.requireEither(isAdmin(), isSignedInUserMemberOfGroup(group)); + + ImageUri imageUri = group.avatarUri().orElseThrow(); + + try { + this.groupRepository.save(group.withAvatarUri(Optional.empty())); + this.imageService.removeImage(imageUri); + } catch (ImageService.ImageCouldNotBeRemovedException | GroupRepository.GroupNameAlreadyExistsException e) { + throw new RuntimeException(e); + } + } + + public ImageDetails getAvatar(UUID userId) { + ImageUri avatarUri = this.userAvatarRepository.getAvatarUri(new UserId(userId)) + .orElse(ImageUri.defaultUserAvatar()); + return new ImageDetails(this.imageService.getImage(avatarUri)); + } + + public void removeUserAvatar(UUID userId) { + this.accessGuard.require(isAdmin()); + ImageUri avatarUri = this.userAvatarRepository.getAvatarUri(new UserId(userId)) + .orElseThrow(); + + try { + this.userAvatarRepository.removeAvatarUri(userId); + this.imageService.removeImage(avatarUri); + } catch (ImageService.ImageCouldNotBeRemovedException e) { + throw new RuntimeException(e); + } + } + + public record ImageDetails(byte[] data, + String imageType) { + public ImageDetails(ImageService.ImageDetails image) { + this(image.data(), image.type()); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/image/domain/Image.java b/backend/src/main/java/it/chalmers/gamma/app/image/domain/Image.java new file mode 100644 index 000000000..2c74ffec8 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/image/domain/Image.java @@ -0,0 +1,4 @@ +package it.chalmers.gamma.app.image.domain; + +public interface Image { +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/image/domain/ImageService.java b/backend/src/main/java/it/chalmers/gamma/app/image/domain/ImageService.java new file mode 100644 index 000000000..ef8515802 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/image/domain/ImageService.java @@ -0,0 +1,27 @@ +package it.chalmers.gamma.app.image.domain; + +public interface ImageService { + + ImageUri saveImage(Image image) throws ImageCouldNotBeSavedException; + + void removeImage(ImageUri imageUri) throws ImageCouldNotBeRemovedException; + + ImageDetails getImage(ImageUri imageUri); + + record ImageDetails(byte[] data, String type) { + } + + class ImageCouldNotBeRemovedException extends Exception { + } + + class ImageCouldNotBeSavedException extends Exception { + public ImageCouldNotBeSavedException(String message) { + super(message); + } + + public ImageCouldNotBeSavedException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/image/domain/ImageUri.java b/backend/src/main/java/it/chalmers/gamma/app/image/domain/ImageUri.java new file mode 100644 index 000000000..e4e4d3b48 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/image/domain/ImageUri.java @@ -0,0 +1,30 @@ +package it.chalmers.gamma.app.image.domain; + +import java.io.Serializable; + +public record ImageUri(String value) implements Serializable { + + public ImageUri { + if (value == null) { + throw new NullPointerException("Image Uri cannot be null"); + } else if (!(value.endsWith(".png") + || value.endsWith(".jpg") + || value.endsWith(".gif") + || value.endsWith(".jpeg"))) { + throw new IllegalArgumentException("Image uri must end with .png, .jpg, .jpeg or .gif"); + } + } + + public static ImageUri defaultGroupBanner() { + return new ImageUri("default_group_banner.jpg"); + } + + public static ImageUri defaultGroupAvatar() { + return new ImageUri("default_group_avatar.jpg"); + } + + public static ImageUri defaultUserAvatar() { + return new ImageUri("default_user_avatar.jpg"); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/image/domain/UserAvatarRepository.java b/backend/src/main/java/it/chalmers/gamma/app/image/domain/UserAvatarRepository.java new file mode 100644 index 000000000..6bd406a5a --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/image/domain/UserAvatarRepository.java @@ -0,0 +1,13 @@ +package it.chalmers.gamma.app.image.domain; + +import it.chalmers.gamma.app.user.domain.UserId; + +import java.util.Optional; +import java.util.UUID; + +public interface UserAvatarRepository { + + Optional getAvatarUri(UserId userId); + + void removeAvatarUri(UUID userId); +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/mail/domain/MailService.java b/backend/src/main/java/it/chalmers/gamma/app/mail/domain/MailService.java new file mode 100644 index 000000000..9cfcb735f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/mail/domain/MailService.java @@ -0,0 +1,7 @@ +package it.chalmers.gamma.app.mail.domain; + +public interface MailService { + + void sendMail(String email, String subject, String body); + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/oauth2/GammaAuthorizationConsentService.java b/backend/src/main/java/it/chalmers/gamma/app/oauth2/GammaAuthorizationConsentService.java new file mode 100644 index 000000000..517f0b742 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/oauth2/GammaAuthorizationConsentService.java @@ -0,0 +1,76 @@ +package it.chalmers.gamma.app.oauth2; + +import it.chalmers.gamma.app.client.domain.Client; +import it.chalmers.gamma.app.client.domain.ClientRepository; +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.user.domain.UserId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Component +public class GammaAuthorizationConsentService implements OAuth2AuthorizationConsentService { + + private final Logger LOGGER = LoggerFactory.getLogger(GammaAuthorizationConsentService.class); + private final ClientRepository clientRepository; + + public GammaAuthorizationConsentService(ClientRepository clientRepository) { + this.clientRepository = clientRepository; + } + + @Override + public void save(OAuth2AuthorizationConsent authorizationConsent) { + Client client = this.clientRepository.get(ClientUid.valueOf(authorizationConsent.getRegisteredClientId())) + .orElseThrow(); + + List consentedScopes = authorizationConsent.getScopes().stream().map(String::toLowerCase).sorted().toList(); + + List clientScopes = new ArrayList<>(client.scopes().stream().map(scope -> scope.name().toLowerCase()).toList()); + clientScopes.add("openid"); + clientScopes.sort(String::compareTo); + + if(consentedScopes.size() != clientScopes.size() || !consentedScopes.equals(clientScopes)) { + throw new IllegalStateException( + "Must have the same scopes for the authorize request and what is on the client." + + "Consent scopes: " + consentedScopes + + "Client scopes: " + clientScopes); + } + + this.clientRepository.addClientApproval(UserId.valueOf(authorizationConsent.getPrincipalName()), ClientUid.valueOf(authorizationConsent.getRegisteredClientId())); + } + + @Override + public void remove(OAuth2AuthorizationConsent authorizationConsent) { + // Use instead MeFacade.deleteUserApproval + throw new UnsupportedOperationException(); + } + + @Override + public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) { + if (this.clientRepository.isApprovedByUser(UserId.valueOf(principalName), ClientUid.valueOf(registeredClientId))) { + Optional maybeClient = this.clientRepository.get(ClientUid.valueOf(registeredClientId)); + + if (maybeClient.isEmpty()) { + return null; + } + + Client client = maybeClient.get(); + OAuth2AuthorizationConsent.Builder consentBuilder = OAuth2AuthorizationConsent.withId(registeredClientId, principalName); + client.scopes().forEach(scope -> consentBuilder.scope(scope.name().toLowerCase())); + consentBuilder.scope("openid"); + + return consentBuilder.build(); + } + + return null; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/oauth2/GammaAuthorizationService.java b/backend/src/main/java/it/chalmers/gamma/app/oauth2/GammaAuthorizationService.java new file mode 100644 index 000000000..532db5064 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/oauth2/GammaAuthorizationService.java @@ -0,0 +1,102 @@ +package it.chalmers.gamma.app.oauth2; + +import it.chalmers.gamma.app.client.domain.authority.AuthorityName; +import it.chalmers.gamma.app.client.domain.authority.ClientAuthorityRepository; +import it.chalmers.gamma.app.client.domain.Client; +import it.chalmers.gamma.app.client.domain.ClientRepository; +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.client.domain.restriction.ClientRestriction; +import it.chalmers.gamma.app.group.domain.Group; +import it.chalmers.gamma.app.group.domain.GroupRepository; +import it.chalmers.gamma.app.oauth2.domain.GammaAuthorizationRepository; +import it.chalmers.gamma.app.oauth2.domain.GammaAuthorizationToken; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserMembership; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class GammaAuthorizationService implements OAuth2AuthorizationService { + + private final Logger LOGGER = LoggerFactory.getLogger(GammaAuthorizationService.class); + private final GammaAuthorizationRepository gammaAuthorizationRepository; + private final ClientRepository clientRepository; + private final GroupRepository groupRepository; + + + public GammaAuthorizationService(GammaAuthorizationRepository gammaAuthorizationRepository, + ClientRepository clientRepository, + GroupRepository groupRepository) { + this.gammaAuthorizationRepository = gammaAuthorizationRepository; + this.clientRepository = clientRepository; + this.groupRepository = groupRepository; + } + + @Override + public void save(OAuth2Authorization authorization) { + UsernamePasswordAuthenticationToken authenticationToken = + authorization.getAttribute("java.security.Principal"); + + // The first oauth2 request has no tokens, so lets stop that + // if the signed-in user does not have the proper authority. + if(hasNoTokens(authorization) && authenticationToken.getPrincipal() instanceof User user) { + Client client = this.clientRepository.get(ClientUid.valueOf(authorization.getRegisteredClientId())) + .orElseThrow(); + + // If the client has no restrictions, then any user can sign in. + if(client.restrictions().isPresent() && userPassesRestriction(client.restrictions().get(), user)) { + throw new AccessDeniedException("User does not have the necessary authority"); + } + } + + gammaAuthorizationRepository.save(authorization); + } + + private boolean userPassesRestriction(ClientRestriction restriction, User user) { + UserId userId = UserId.valueOf(user.getUsername()); + + List memberships = this.groupRepository.getAllByUser(userId); + List userSuperGroups = memberships.stream().map(UserMembership::group).map(group -> group.superGroup().id()).distinct().toList(); + + return restriction.superGroups().stream().anyMatch(superGroup -> userSuperGroups.contains(superGroup.id())); + } + + private boolean hasNoTokens(OAuth2Authorization authorization) { + return authorization.getToken(OAuth2AuthorizationCode.class) == null && authorization.getToken(OAuth2AccessToken.class) == null && authorization.getToken(OidcIdToken.class) == null; + } + + //TODO: Tokens are not removed? + @Override + public void remove(OAuth2Authorization authorization) { + LOGGER.info("Remove: " + authorization.toString()); + gammaAuthorizationRepository.remove(authorization); + } + + @Override + public OAuth2Authorization findById(String id) { + return gammaAuthorizationRepository + .findById(id) + .orElseThrow(); + } + + @Override + public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) { + return gammaAuthorizationRepository.findByToken(GammaAuthorizationToken.valueOf(token, tokenType)).orElseThrow(); + } + + public static class UserIsNotAuthorizedException extends RuntimeException {} + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/oauth2/GammaRegisteredClientRepository.java b/backend/src/main/java/it/chalmers/gamma/app/oauth2/GammaRegisteredClientRepository.java new file mode 100644 index 000000000..e9c2f766c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/oauth2/GammaRegisteredClientRepository.java @@ -0,0 +1,64 @@ +package it.chalmers.gamma.app.oauth2; + +import it.chalmers.gamma.app.client.domain.*; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.stereotype.Component; + +@Component +public class GammaRegisteredClientRepository implements RegisteredClientRepository { + + private final ClientRepository clientRepository; + + public GammaRegisteredClientRepository(ClientRepository clientRepository) { + this.clientRepository = clientRepository; + } + + @Override + public void save(RegisteredClient registeredClient) { + throw new UnsupportedOperationException("Use ClientFacade instead."); + } + + @Override + public RegisteredClient findById(String id) { + Client client = this.clientRepository.get(ClientUid.valueOf(id)) + .orElseThrow(NullPointerException::new); + + return toRegisteredClient(client); + } + + @Override + public RegisteredClient findByClientId(String clientId) { + Client client = this.clientRepository.get(new ClientId(clientId)) + .orElseThrow(NullPointerException::new); + + return toRegisteredClient(client); + } + + private RegisteredClient toRegisteredClient(Client client) { + RegisteredClient.Builder builder = RegisteredClient + .withId(client.clientUid().getValue()) + .clientId(client.clientId().value()) + .clientSecret(client.clientSecret().value()) + .redirectUri(client.clientRedirectUrl().value()) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .clientName(client.prettyName().value()) + .clientSettings( + ClientSettings + .builder() + .requireAuthorizationConsent(true) + .build() + ); + + builder.scope("openid"); + + for (Scope scope : client.scopes()) { + builder.scope(scope.name().toLowerCase()); + } + + return builder.build(); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/oauth2/OAuth2AuthorizationServerSecurityConfig.java b/backend/src/main/java/it/chalmers/gamma/app/oauth2/OAuth2AuthorizationServerSecurityConfig.java new file mode 100644 index 000000000..35a6022ac --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/oauth2/OAuth2AuthorizationServerSecurityConfig.java @@ -0,0 +1,54 @@ +package it.chalmers.gamma.app.oauth2; + +import it.chalmers.gamma.app.user.MeFacade; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; + +@Configuration +public class OAuth2AuthorizationServerSecurityConfig { + + private final UserInfoMapper userInfoMapper; + + public OAuth2AuthorizationServerSecurityConfig(UserInfoMapper userInfoMapper) { + this.userInfoMapper = userInfoMapper; + } + + /** + * This SecurityFilterChain setups the security for the endpoints that is used for OAuth 2.1. + */ + @Order(Ordered.HIGHEST_PRECEDENCE) + @Bean + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, MeFacade meFacade) throws Exception { + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) + .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint + .consentPage("/oauth2/consent") + ) + .oidc(oidcConfigurer -> oidcConfigurer + .userInfoEndpoint(userInfo -> userInfo.userInfoMapper(userInfoMapper)) + ); + http + .exceptionHandling((exceptions) -> exceptions + .authenticationEntryPoint( + new LoginUrlAuthenticationEntryPoint("/login?authorizing")) + ) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); + + return http.build(); + } + + @Bean + public AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().build(); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/oauth2/UserInfoMapper.java b/backend/src/main/java/it/chalmers/gamma/app/oauth2/UserInfoMapper.java new file mode 100644 index 000000000..cf6015309 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/oauth2/UserInfoMapper.java @@ -0,0 +1,90 @@ +package it.chalmers.gamma.app.oauth2; + +import it.chalmers.gamma.app.client.domain.authority.ClientAuthorityRepository; +import it.chalmers.gamma.app.user.domain.GammaUser; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext; +import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Component +public class UserInfoMapper implements Function { + + private final ClientAuthorityRepository clientAuthorityRepository; + private final UserRepository userRepository; + private final String baseUrl; + private final String contextPath; + + public UserInfoMapper(ClientAuthorityRepository clientAuthorityRepository, UserRepository userRepository, + @Value("${application.base-uri}") String baseUrl, + @Value("${server.servlet.context-path}") String contextPath) { + this.clientAuthorityRepository = clientAuthorityRepository; + this.userRepository = userRepository; + this.baseUrl = baseUrl; + this.contextPath = contextPath; + } + + public OidcUserInfo apply(OidcUserInfoAuthenticationContext context) { + OidcUserInfoAuthenticationToken authentication = context.getAuthentication(); + JwtAuthenticationToken principal = (JwtAuthenticationToken) authentication.getPrincipal(); + GammaUser me = this.userRepository.get(UserId.valueOf(principal.getName())) + .orElseThrow(); + + /* + * Available scopes are profile, email. + * The prefix that spring-authorization-server adds in SCOPE_ + */ + final String PROFILE_SCOPE = "SCOPE_profile"; + final String EMAIL_SCOPE = "SCOPE_email"; + + Map claims = new HashMap<>(principal.getToken().getClaims()); + Collection scopes = principal.getAuthorities(); + + for (GrantedAuthority scope : scopes) { + if (scope.getAuthority().equals(PROFILE_SCOPE)) { + //https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + claims.put("name", me.firstName().value() + " '" + me.nick().value() + "' " + me.lastName().value()); + claims.put("given_name", me.firstName().value()); + claims.put("family_name", me.lastName().value()); + claims.put("nickname", me.nick().value()); + claims.put("locale", me.language().toString().toLowerCase()); + + claims.put("picture", this.baseUrl + + this.contextPath + + "/images/user/avatar/" + + me.id().value() + ); + + // Non-standard claims. + claims.put("cid", me.cid().value()); + + // Separate record here to guarantee that the props doesn't change + record UserInfoAuthority(String authority, String type) { + + } + +// claims.put( +// "authorities", +// this.clientAuthorityRepository.getByUser(me.id()) +// .stream() +// .map(userAuthority -> new UserInfoAuthority(userAuthority.authorityName().value(), userAuthority.authorityType().name())) +// .toList() +// ); + } else if (scope.getAuthority().equals(EMAIL_SCOPE)) { + claims.put("email", me.extended().email().value()); + } + } + + return new OidcUserInfo(claims); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/oauth2/domain/GammaAuthorizationRepository.java b/backend/src/main/java/it/chalmers/gamma/app/oauth2/domain/GammaAuthorizationRepository.java new file mode 100644 index 000000000..08fd73aba --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/oauth2/domain/GammaAuthorizationRepository.java @@ -0,0 +1,17 @@ +package it.chalmers.gamma.app.oauth2.domain; + +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; + +import java.util.Optional; + +public interface GammaAuthorizationRepository { + + void save(OAuth2Authorization authorization); + + void remove(OAuth2Authorization authorization); + + Optional findById(String id); + + Optional findByToken(GammaAuthorizationToken token); + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/oauth2/domain/GammaAuthorizationToken.java b/backend/src/main/java/it/chalmers/gamma/app/oauth2/domain/GammaAuthorizationToken.java new file mode 100644 index 000000000..7042acc73 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/oauth2/domain/GammaAuthorizationToken.java @@ -0,0 +1,20 @@ +package it.chalmers.gamma.app.oauth2.domain; + +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; + +public record GammaAuthorizationToken(String value, Type type) { + + public static GammaAuthorizationToken valueOf(String value, OAuth2TokenType auth2TokenType) { + return switch (auth2TokenType.getValue()) { + case "access_token" -> new GammaAuthorizationToken(value, Type.ACCESS_TOKEN); + case "code" -> new GammaAuthorizationToken(value, Type.CODE); + case "state" -> new GammaAuthorizationToken(value, Type.STATE); + case "oidc" -> new GammaAuthorizationToken(value, Type.OIDC); + default -> throw new IllegalArgumentException("Invalid token type: " + auth2TokenType.getValue()); + }; + } + + public enum Type { + CODE, ACCESS_TOKEN, STATE, OIDC + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/post/PostFacade.java b/backend/src/main/java/it/chalmers/gamma/app/post/PostFacade.java new file mode 100644 index 000000000..e0805632c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/post/PostFacade.java @@ -0,0 +1,109 @@ +package it.chalmers.gamma.app.post; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.common.Text; +import it.chalmers.gamma.app.group.GroupFacade; +import it.chalmers.gamma.app.group.domain.EmailPrefix; +import it.chalmers.gamma.app.group.domain.GroupRepository; +import it.chalmers.gamma.app.post.domain.Post; +import it.chalmers.gamma.app.post.domain.PostId; +import it.chalmers.gamma.app.post.domain.PostRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; +import static it.chalmers.gamma.app.authentication.AccessGuard.isSignedIn; + +@Service +public class PostFacade extends Facade { + + private final PostRepository postRepository; + private final GroupRepository groupRepository; + + public PostFacade(AccessGuard accessGuard, + PostRepository postRepository, + GroupRepository groupRepository) { + super(accessGuard); + this.postRepository = postRepository; + this.groupRepository = groupRepository; + } + + public void create(NewPost newPost) { + this.postRepository.save( + new Post( + PostId.generate(), + 0, + new Text(newPost.svText, newPost.enText), + new EmailPrefix(newPost.emailPrefix) + ) + ); + } + + public void update(UpdatePost updatePost) throws PostRepository.PostNotFoundException { + Post oldPost = this.postRepository.get(new PostId(updatePost.postId)) + .orElseThrow(); + Post newPost = oldPost.with() + .version(updatePost.version) + .name(new Text( + updatePost.svText, + updatePost.enText + )) + .emailPrefix(new EmailPrefix(updatePost.emailPrefix)) + .build(); + + this.postRepository.save(newPost); + } + + public void delete(UUID postId) throws PostRepository.PostNotFoundException { + this.postRepository.delete(new PostId(postId)); + } + + public Optional get(UUID postId) { + this.accessGuard.require(isSignedIn()); + + return this.postRepository.get(new PostId(postId)).map(PostDTO::new); + } + + public List getAll() { + this.accessGuard.require(isSignedIn()); + + return this.postRepository.getAll() + .stream() + .map(PostDTO::new) + .toList(); + } + + public List getPostUsages(UUID postId) { + this.accessGuard.require(isAdmin()); + + return this.groupRepository.getAllByPost(new PostId(postId)) + .stream() + .map(GroupFacade.GroupWithMembersDTO::new) + .toList(); + } + + public record NewPost(String svText, String enText, String emailPrefix) { + } + + public record UpdatePost(UUID postId, int version, String svText, String enText, String emailPrefix) { + } + + public record PostDTO(UUID id, + int version, + String svName, + String enName, + String emailPrefix) { + public PostDTO(Post post) { + this(post.id().value(), + post.version(), + post.name().sv().value(), + post.name().en().value(), + post.emailPrefix().value()); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/post/domain/Post.java b/backend/src/main/java/it/chalmers/gamma/app/post/domain/Post.java new file mode 100644 index 000000000..34207a81a --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/post/domain/Post.java @@ -0,0 +1,23 @@ +package it.chalmers.gamma.app.post.domain; + +import io.soabase.recordbuilder.core.RecordBuilder; +import it.chalmers.gamma.app.common.Text; +import it.chalmers.gamma.app.group.domain.EmailPrefix; + +import java.util.Objects; + +@RecordBuilder +public record Post(PostId id, + int version, + Text name, + EmailPrefix emailPrefix +) implements PostBuilder.With { + + public Post { + Objects.requireNonNull(id); + Objects.requireNonNull(name); + Objects.requireNonNull(emailPrefix); + } + +} + diff --git a/backend/src/main/java/it/chalmers/gamma/app/post/domain/PostId.java b/backend/src/main/java/it/chalmers/gamma/app/post/domain/PostId.java new file mode 100644 index 000000000..3084dd589 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/post/domain/PostId.java @@ -0,0 +1,26 @@ +package it.chalmers.gamma.app.post.domain; + +import it.chalmers.gamma.app.common.Id; + +import java.util.Objects; +import java.util.UUID; + +public record PostId(UUID value) implements Id { + + public PostId { + Objects.requireNonNull(value); + } + + public static PostId generate() { + return new PostId(UUID.randomUUID()); + } + + public static PostId valueOf(String value) { + return new PostId(UUID.fromString(value)); + } + + @Override + public UUID getValue() { + return this.value; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/post/domain/PostRepository.java b/backend/src/main/java/it/chalmers/gamma/app/post/domain/PostRepository.java new file mode 100644 index 000000000..bc887b83e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/post/domain/PostRepository.java @@ -0,0 +1,19 @@ +package it.chalmers.gamma.app.post.domain; + +import java.util.List; +import java.util.Optional; + +public interface PostRepository { + + void save(Post post); + + void delete(PostId postId) throws PostNotFoundException; + + List getAll(); + + Optional get(PostId postId); + + class PostNotFoundException extends Exception { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/settings/SettingsFacade.java b/backend/src/main/java/it/chalmers/gamma/app/settings/SettingsFacade.java new file mode 100644 index 000000000..593f2c985 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/settings/SettingsFacade.java @@ -0,0 +1,60 @@ +package it.chalmers.gamma.app.settings; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.settings.domain.SettingsRepository; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupType; +import jakarta.transaction.Transactional; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.List; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; +import static it.chalmers.gamma.app.authentication.AccessGuard.passwordCheck; + +@Service +public class SettingsFacade extends Facade { + + private final SettingsRepository settingsRepository; + + public SettingsFacade(AccessGuard accessGuard, SettingsRepository settingsRepository) { + super(accessGuard); + this.settingsRepository = settingsRepository; + } + + public void resetUserAgreement(String password) { + this.accessGuard.requireAll( + isAdmin(), + passwordCheck(password) + ); + + this.settingsRepository.setSettings( + settings -> settings.withLastUpdatedUserAgreement(Instant.now()) + ); + } + + @Transactional + public void setInfoSuperGroupTypes(List superGroupTypes) { + this.accessGuard.require(isAdmin()); + + this.settingsRepository.setSettings( + settings -> settings.withInfoSuperGroupTypes( + superGroupTypes + .stream() + .map(SuperGroupType::new) + .toList() + ) + ); + } + + public List getInfoApiSuperGroupTypes() { + this.accessGuard.require(isAdmin()); + + return this.settingsRepository.getSettings() + .infoSuperGroupTypes() + .stream() + .map(SuperGroupType::value) + .toList(); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/settings/domain/Settings.java b/backend/src/main/java/it/chalmers/gamma/app/settings/domain/Settings.java new file mode 100644 index 000000000..122919ec8 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/settings/domain/Settings.java @@ -0,0 +1,18 @@ +package it.chalmers.gamma.app.settings.domain; + +import io.soabase.recordbuilder.core.RecordBuilder; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupType; + +import java.time.Instant; +import java.util.List; +import java.util.Objects; + +@RecordBuilder +public record Settings(Instant lastUpdatedUserAgreement, + //The group types that are going to be used by InfoApiController + List infoSuperGroupTypes) implements SettingsBuilder.With { + public Settings { + Objects.requireNonNull(lastUpdatedUserAgreement); + Objects.requireNonNull(infoSuperGroupTypes); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/settings/domain/SettingsId.java b/backend/src/main/java/it/chalmers/gamma/app/settings/domain/SettingsId.java new file mode 100644 index 000000000..7d245f58b --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/settings/domain/SettingsId.java @@ -0,0 +1,26 @@ +package it.chalmers.gamma.app.settings.domain; + +import it.chalmers.gamma.app.common.Id; + +import java.util.Objects; +import java.util.UUID; + +public record SettingsId(UUID value) implements Id { + + public SettingsId { + Objects.requireNonNull(value); + } + + public static SettingsId generate() { + return new SettingsId(UUID.randomUUID()); + } + + public static SettingsId valueOf(String value) { + return new SettingsId(UUID.fromString(value)); + } + + @Override + public UUID getValue() { + return this.value; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/settings/domain/SettingsRepository.java b/backend/src/main/java/it/chalmers/gamma/app/settings/domain/SettingsRepository.java new file mode 100644 index 000000000..1e5636528 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/settings/domain/SettingsRepository.java @@ -0,0 +1,17 @@ +package it.chalmers.gamma.app.settings.domain; + +public interface SettingsRepository { + + boolean hasSettings(); + + Settings getSettings(); + + void setSettings(UpdateSettings updateSettings); + + void setSettings(Settings settings); + + interface UpdateSettings { + Settings updateSettings(Settings settings); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/supergroup/SuperGroupFacade.java b/backend/src/main/java/it/chalmers/gamma/app/supergroup/SuperGroupFacade.java new file mode 100644 index 000000000..76eae1539 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/supergroup/SuperGroupFacade.java @@ -0,0 +1,168 @@ +package it.chalmers.gamma.app.supergroup; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; +import it.chalmers.gamma.app.supergroup.domain.*; +import it.chalmers.gamma.app.user.domain.Name; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; +import static it.chalmers.gamma.app.authentication.AccessGuard.isSignedIn; + +@Service +public class SuperGroupFacade extends Facade { + + private final SuperGroupRepository superGroupRepository; + private final SuperGroupTypeRepository superGroupTypeRepository; + + public SuperGroupFacade(AccessGuard accessGuard, + SuperGroupRepository superGroupRepository, + SuperGroupTypeRepository superGroupTypeRepository) { + super(accessGuard); + this.superGroupRepository = superGroupRepository; + this.superGroupTypeRepository = superGroupTypeRepository; + } + + public void addType(String type) throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + accessGuard.require(isAdmin()); + + this.superGroupTypeRepository.add(new SuperGroupType(type)); + } + + public void removeType(String type) throws SuperGroupTypeRepository.SuperGroupTypeNotFoundException, SuperGroupTypeRepository.SuperGroupTypeHasUsagesException { + accessGuard.require(isAdmin()); + + this.superGroupTypeRepository.delete(new SuperGroupType(type)); + } + + public List getAllTypes() { + accessGuard.requireEither(isAdmin(), isSignedIn()); + + return this.superGroupTypeRepository.getAll() + .stream() + .map(SuperGroupType::value) + .toList(); + } + + public void createSuperGroup(NewSuperGroup newSuperGroup) throws SuperGroupRepository.SuperGroupAlreadyExistsException { + accessGuard.require(isAdmin()); + + this.superGroupRepository.save( + new SuperGroup( + SuperGroupId.generate(), + 0, + new Name(newSuperGroup.name), + new PrettyName(newSuperGroup.prettyName), + new SuperGroupType(newSuperGroup.superGroupType), + new Text( + newSuperGroup.svDescription, + newSuperGroup.enDescription + ) + ) + ); + } + + public void updateSuperGroup(UpdateSuperGroup updateSuperGroup) throws SuperGroupRepository.SuperGroupNotFoundException { + accessGuard.require(isAdmin()); + + + System.out.println(updateSuperGroup); + + SuperGroup oldSuperGroup = this.superGroupRepository.get(new SuperGroupId(updateSuperGroup.id)).orElseThrow(); + SuperGroup newSuperGroup = SuperGroupBuilder + .builder(oldSuperGroup) + .name(new Name(updateSuperGroup.name)) + .prettyName(new PrettyName(updateSuperGroup.prettyName)) + .type(new SuperGroupType(updateSuperGroup.type)) + .description( + new Text( + updateSuperGroup.svDescription, + updateSuperGroup.enDescription + ) + ).build(); + + this.superGroupRepository.save(newSuperGroup); + } + + public void deleteSuperGroup(SuperGroupId superGroupId) throws SuperGroupIsUsedException, SuperGroupNotFoundException { + accessGuard.require(isAdmin()); + + try { + this.superGroupRepository.delete(superGroupId); + } catch (SuperGroupRepository.SuperGroupNotFoundException e) { + throw new SuperGroupNotFoundException(); + } catch (SuperGroupRepository.SuperGroupIsUsedException e) { + throw new SuperGroupIsUsedException(); + } + } + + public List getAll() { + accessGuard.requireEither(isAdmin(), isSignedIn()); + + return this.superGroupRepository.getAll() + .stream() + .map(SuperGroupDTO::new) + .toList(); + } + + public List getAllSuperGroupsByType(String superGroupType) { + accessGuard.require(isAdmin()); + + return this.superGroupRepository.getAllByType(new SuperGroupType(superGroupType)) + .stream() + .map(SuperGroupDTO::new) + .toList(); + } + + public Optional get(UUID superGroupId) { + return this.superGroupRepository.get(new SuperGroupId(superGroupId)).map(SuperGroupDTO::new); + } + + public record NewSuperGroup(String name, + String prettyName, + String superGroupType, + String svDescription, + String enDescription) { + } + + public record UpdateSuperGroup(UUID id, + int version, + String name, + String prettyName, + String type, + String svDescription, + String enDescription) { + } + + public record SuperGroupDTO(UUID id, + int version, + String name, + String prettyName, + String type, + String svDescription, + String enDescription) { + public SuperGroupDTO(SuperGroup superGroup) { + this(superGroup.id().value(), + superGroup.version(), + superGroup.name().value(), + superGroup.prettyName().value(), + superGroup.type().value(), + superGroup.description().sv().value(), + superGroup.description().en().value() + ); + } + } + + public static class SuperGroupNotFoundException extends Exception { + } + + public static class SuperGroupIsUsedException extends Exception { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroup.java b/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroup.java new file mode 100644 index 000000000..001e1901e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroup.java @@ -0,0 +1,26 @@ +package it.chalmers.gamma.app.supergroup.domain; + +import io.soabase.recordbuilder.core.RecordBuilder; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; +import it.chalmers.gamma.app.user.domain.Name; + +import java.util.Objects; + +@RecordBuilder +public record SuperGroup(SuperGroupId id, + int version, + Name name, + PrettyName prettyName, + SuperGroupType type, + Text description) implements SuperGroupBuilder.With { + + public SuperGroup { + Objects.requireNonNull(id); + Objects.requireNonNull(name); + Objects.requireNonNull(prettyName); + Objects.requireNonNull(type); + Objects.requireNonNull(description); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroupId.java b/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroupId.java new file mode 100644 index 000000000..34963b36a --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroupId.java @@ -0,0 +1,26 @@ +package it.chalmers.gamma.app.supergroup.domain; + +import it.chalmers.gamma.app.common.Id; + +import java.util.Objects; +import java.util.UUID; + +public record SuperGroupId(UUID value) implements Id { + + public SuperGroupId { + Objects.requireNonNull(value); + } + + public static SuperGroupId generate() { + return new SuperGroupId(UUID.randomUUID()); + } + + public static SuperGroupId valueOf(String value) { + return new SuperGroupId(UUID.fromString(value)); + } + + @Override + public UUID getValue() { + return this.value; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroupRepository.java b/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroupRepository.java new file mode 100644 index 000000000..ecc189af0 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroupRepository.java @@ -0,0 +1,33 @@ +package it.chalmers.gamma.app.supergroup.domain; + +import java.util.List; +import java.util.Optional; + +public interface SuperGroupRepository { + + void save(SuperGroup superGroup) throws NameAlreadyExistsRuntimeException, TypeNotFoundRuntimeException; + + void delete(SuperGroupId superGroupId) throws SuperGroupNotFoundException, SuperGroupIsUsedException; + + List getAll(); + + List getAllByType(SuperGroupType superGroupType); + + Optional get(SuperGroupId superGroupId); + + class SuperGroupAlreadyExistsException extends Exception { + } + + class SuperGroupNotFoundException extends Exception { + } + + class SuperGroupIsUsedException extends Exception { + } + + class NameAlreadyExistsRuntimeException extends RuntimeException { + } + + class TypeNotFoundRuntimeException extends RuntimeException { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroupType.java b/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroupType.java new file mode 100644 index 000000000..b848e7218 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroupType.java @@ -0,0 +1,30 @@ +package it.chalmers.gamma.app.supergroup.domain; + +import it.chalmers.gamma.app.common.Id; + +import java.util.Locale; + +public record SuperGroupType(String value) implements Id { + + public SuperGroupType { + if (value == null) { + throw new NullPointerException("Super group type cannot be null"); + } + + value = value.toLowerCase(Locale.ROOT); + + if (!value.matches("^([a-z]{3,30})$")) { + throw new IllegalArgumentException("Super group type: [" + value + "] must be made using letters with length between 5 - 30"); + } + + } + + public static SuperGroupType valueOf(String name) { + return new SuperGroupType(name); + } + + @Override + public String getValue() { + return this.value; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroupTypeRepository.java b/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroupTypeRepository.java new file mode 100644 index 000000000..43040bb4f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/supergroup/domain/SuperGroupTypeRepository.java @@ -0,0 +1,25 @@ +package it.chalmers.gamma.app.supergroup.domain; + +import java.util.List; + +public interface SuperGroupTypeRepository { + + void add(SuperGroupType superGroupType) throws SuperGroupTypeAlreadyExistsException; + + void delete(SuperGroupType superGroupType) throws SuperGroupTypeNotFoundException, SuperGroupTypeHasUsagesException; + + List getAll(); + + class SuperGroupTypeAlreadyExistsException extends Exception { + public SuperGroupTypeAlreadyExistsException(String value) { + super("Super group type: " + value + " already exists"); + } + } + + class SuperGroupTypeNotFoundException extends Exception { + } + + class SuperGroupTypeHasUsagesException extends Exception { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/MeFacade.java b/backend/src/main/java/it/chalmers/gamma/app/user/MeFacade.java new file mode 100644 index 000000000..f43cb1291 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/MeFacade.java @@ -0,0 +1,230 @@ +package it.chalmers.gamma.app.user; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.admin.domain.AdminRepository; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.client.domain.Client; +import it.chalmers.gamma.app.client.domain.ClientRepository; +import it.chalmers.gamma.app.client.domain.ClientUid; +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.group.GroupFacade; +import it.chalmers.gamma.app.group.domain.GroupRepository; +import it.chalmers.gamma.app.image.domain.Image; +import it.chalmers.gamma.app.image.domain.ImageService; +import it.chalmers.gamma.app.image.domain.ImageUri; +import it.chalmers.gamma.app.post.PostFacade; +import it.chalmers.gamma.app.user.domain.*; +import it.chalmers.gamma.security.authentication.AuthenticationExtractor; +import it.chalmers.gamma.security.authentication.GammaAuthentication; +import it.chalmers.gamma.security.authentication.UserAuthentication; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isSignedIn; + +@Service +public class MeFacade extends Facade { + + private static final Logger LOGGER = LoggerFactory.getLogger(MeFacade.class); + + private final UserRepository userRepository; + private final ClientRepository clientRepository; + private final GroupRepository groupRepository; + private final ImageService imageService; + + public MeFacade(AccessGuard accessGuard, + UserRepository userRepository, + ClientRepository clientRepository, + GroupRepository groupRepository, + ImageService imageService) { + super(accessGuard); + this.userRepository = userRepository; + this.clientRepository = clientRepository; + this.groupRepository = groupRepository; + this.imageService = imageService; + } + + public List getSignedInUserApprovals() { + this.accessGuard.require(isSignedIn()); + + if (AuthenticationExtractor.getAuthentication() instanceof UserAuthentication userAuthentication) { + GammaUser user = userAuthentication.get(); + return this.clientRepository.getClientsByUserApproved(user.id()) + .stream() + .map(UserApprovedClientDTO::new) + .toList(); + } else { + return null; + } + } + + public void deleteUserApproval(UUID clientUid) { + this.accessGuard.require(isSignedIn()); + + if (AuthenticationExtractor.getAuthentication() instanceof UserAuthentication userAuthentication) { + this.clientRepository.deleteUserApproval(new ClientUid(clientUid), userAuthentication.get().id()); + } + } + + public MeDTO getMe() { + GammaAuthentication authenticated = AuthenticationExtractor.getAuthentication(); + GammaUser user = null; + boolean isAdmin = false; + + if (authenticated instanceof UserAuthentication userAuthentication) { + user = userAuthentication.get(); + isAdmin = userAuthentication.isAdmin(); + } + + if (user == null) { + throw new IllegalCallerException("Can only be called by signed in sessions"); + } + + List groups = this.groupRepository + .getAllByUser(user.id()) + .stream() + .map(MyMembership::new) + .toList(); + + return new MeDTO(user, groups, isAdmin); + } + + public void updateMe(UpdateMe updateMe) { + GammaAuthentication authenticated = AuthenticationExtractor.getAuthentication(); + if (authenticated instanceof UserAuthentication userAuthentication) { + GammaUser oldMe = userAuthentication.get(); + GammaUser newMe = oldMe.with() + .nick(new Nick(updateMe.nick)) + .firstName(new FirstName(updateMe.firstName)) + .lastName(new LastName(updateMe.lastName)) + .language(Language.valueOf(updateMe.language)) + .extended(oldMe.extended().with() + .email(new Email(updateMe.email)) + .build() + ) + .build(); + + this.userRepository.save(newMe); + } + } + + public void updatePassword(UpdatePassword updatePassword) { + GammaAuthentication authenticated = AuthenticationExtractor.getAuthentication(); + if (authenticated instanceof UserAuthentication userAuthentication) { + GammaUser me = userAuthentication.get(); + if (this.userRepository.checkPassword(me.id(), new UnencryptedPassword(updatePassword.oldPassword))) { + this.userRepository.setPassword(me.id(), new UnencryptedPassword(updatePassword.newPassword)); + } + } + } + + public void deleteMe(String password) { + GammaAuthentication authenticated = AuthenticationExtractor.getAuthentication(); + if (authenticated instanceof UserAuthentication userAuthentication) { + GammaUser me = userAuthentication.get(); + if (this.userRepository.checkPassword(me.id(), new UnencryptedPassword(password))) { + try { + this.userRepository.delete(me.id()); + } catch (UserRepository.UserNotFoundException e) { + e.printStackTrace(); + } + } + } + } + + public void acceptUserAgreement() { + GammaAuthentication authenticated = AuthenticationExtractor.getAuthentication(); + if (authenticated instanceof UserAuthentication userAuthentication + && !userAuthentication.get().extended().acceptedUserAgreement()) { + try { + this.userRepository.acceptUserAgreement(userAuthentication.get().id()); + } catch (UserRepository.UserNotFoundException e) { + throw new IllegalStateException(); + } + } + } + + public void setAvatar(Image image) throws ImageService.ImageCouldNotBeSavedException { + GammaAuthentication authenticated = AuthenticationExtractor.getAuthentication(); + if (authenticated instanceof UserAuthentication userAuthentication) { + GammaUser user = userAuthentication.get(); + LOGGER.info("Image has been attempted to be uploaded by the user " + user.id().value()); + ImageUri imageUri = this.imageService.saveImage(image); + this.userRepository.save(user.withExtended(user.extended().withAvatarUri(imageUri))); + LOGGER.info("Image was successfully uploaded with the id: " + imageUri.value()); + } else { + throw new ImageService.ImageCouldNotBeSavedException("Could not find the authenticated user to upload the image"); + } + } + + public void deleteAvatar() throws ImageService.ImageCouldNotBeRemovedException { + GammaAuthentication authenticated = AuthenticationExtractor.getAuthentication(); + if (authenticated instanceof UserAuthentication userAuthentication) { + GammaUser user = userAuthentication.get(); + this.userRepository.save(user.withExtended(user.extended().withAvatarUri(null))); + this.imageService.removeImage(user.extended().avatarUri()); + } + } + + public record UserApprovedClientDTO(UUID clientUid, + String name, + String svDescription, + String enDescription, + List scopes) { + public UserApprovedClientDTO(Client client) { + this(client.clientUid().value(), + client.prettyName().value(), + client.description().sv().value(), + client.description().en().value(), + client.scopes().stream().map(Enum::name).toList()); + } + } + + public record MyMembership(PostFacade.PostDTO post, GroupFacade.GroupDTO group, String unofficialPostName) { + public MyMembership(UserMembership userMembership) { + this(new PostFacade.PostDTO(userMembership.post()), new GroupFacade.GroupDTO(userMembership.group()), userMembership.unofficialPostName().value()); + } + } + + public record MeDTO(String nick, + String firstName, + String lastName, + String cid, + String email, + UUID id, + int acceptanceYear, + boolean userAgreement, + List groups, + String language, + boolean isAdmin) { + public MeDTO(GammaUser user, List groups, boolean isAdmin) { + this(user.nick().value(), + user.firstName().value(), + user.lastName().value(), + user.cid().value(), + user.extended().email().value(), + user.id().value(), + user.acceptanceYear().value(), + user.extended().acceptedUserAgreement(), + groups, + user.language().name(), + isAdmin + ); + } + } + + public record UpdateMe(String nick, + String firstName, + String lastName, + String email, + String language) { + } + + public record UpdatePassword(String oldPassword, String newPassword) { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/UserCreationFacade.java b/backend/src/main/java/it/chalmers/gamma/app/user/UserCreationFacade.java new file mode 100644 index 000000000..ee8f5b9aa --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/UserCreationFacade.java @@ -0,0 +1,140 @@ +package it.chalmers.gamma.app.user; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.mail.domain.MailService; +import it.chalmers.gamma.app.user.activation.domain.UserActivationRepository; +import it.chalmers.gamma.app.user.activation.domain.UserActivationToken; +import it.chalmers.gamma.app.user.domain.*; +import it.chalmers.gamma.app.user.allowlist.AllowListRepository; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; +import static it.chalmers.gamma.app.authentication.AccessGuard.isNotSignedIn; + +@Service +public class UserCreationFacade extends Facade { + + private static final String MAIL_POSTFIX = "student.chalmers.se"; + private static final Logger LOGGER = LoggerFactory.getLogger(UserCreationFacade.class); + private final MailService mailService; + private final AllowListRepository allowListRepository; + private final UserActivationRepository userActivationRepository; + private final UserRepository userRepository; + + public UserCreationFacade(AccessGuard accessGuard, + MailService mailService, + AllowListRepository allowListRepository, + UserActivationRepository userActivationRepository, + UserRepository userRepository) { + super(accessGuard); + this.mailService = mailService; + this.allowListRepository = allowListRepository; + this.userActivationRepository = userActivationRepository; + this.userRepository = userRepository; + } + + public void tryToActivateUser(String cidRaw) { + accessGuard.require(isNotSignedIn()); + + Cid cid = new Cid(cidRaw); + try { + UserActivationToken userActivationToken = this.userActivationRepository.createActivationToken(cid); + sendEmail(cid, userActivationToken); + LOGGER.info("Cid " + cid + " has been activated"); + } catch (UserActivationRepository.CidNotAllowedException e) { + LOGGER.info("Someone tried to activate the cid: " + cid); + } + } + + public void createUser(NewUser newUser) throws SomePropertyNotUniqueException { + this.accessGuard.require(isAdmin()); + + try { + this.userRepository.create( + new GammaUser( + UserId.generate(), + new Cid(newUser.cid), + new Nick(newUser.nick), + new FirstName(newUser.firstName), + new LastName(newUser.lastName), + new AcceptanceYear(newUser.acceptanceYear), + Language.valueOf(newUser.language), + new UserExtended( + new Email(newUser.email), + 0, + false, + false, + null + )), + new UnencryptedPassword(newUser.password) + ); + } catch (UserRepository.CidAlreadyInUseException | UserRepository.EmailAlreadyInUseException e) { + throw new SomePropertyNotUniqueException(); + } + } + + @Transactional + public void createUserWithCode(NewUser data, String token) throws SomePropertyNotUniqueException { + this.accessGuard.require(isNotSignedIn()); + + Cid tokenCid = this.userActivationRepository.getByToken(new UserActivationToken(token)); + + //TODO: Check if email is not student@chalmers.se + + if (tokenCid.value().equals(data.cid)) { + Cid cid = new Cid(data.cid); + + try { + this.userRepository.create( + new GammaUser( + UserId.generate(), + cid, + new Nick(data.nick), + new FirstName(data.firstName), + new LastName(data.lastName), + new AcceptanceYear(data.acceptanceYear), + Language.valueOf(data.language), + new UserExtended( + new Email(data.email), + 0, + false, + false, + null + ) + ), + new UnencryptedPassword(data.password) + ); + } catch (UserRepository.CidAlreadyInUseException | UserRepository.EmailAlreadyInUseException e) { + throw new SomePropertyNotUniqueException(); + } + + this.userActivationRepository.removeActivation(cid); + } + } + + private void sendEmail(Cid cid, UserActivationToken userActivationToken) { + String to = cid.getValue() + "@" + MAIL_POSTFIX; + String code = userActivationToken.value(); + String message = "Your code to Gamma is: " + code; + this.mailService.sendMail(to, "Gamma activation code", message); + } + + public record NewUser(String password, + String nick, + String firstName, + String email, + String lastName, + int acceptanceYear, + String cid, + String language) { + } + + public class SomePropertyNotUniqueException extends Exception { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/UserFacade.java b/backend/src/main/java/it/chalmers/gamma/app/user/UserFacade.java new file mode 100644 index 000000000..329453d9f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/UserFacade.java @@ -0,0 +1,203 @@ +package it.chalmers.gamma.app.user; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.apikey.domain.ApiKeyType; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.client.domain.Client; +import it.chalmers.gamma.app.client.domain.approval.ClientApprovalsRepository; +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.group.GroupFacade; +import it.chalmers.gamma.app.group.domain.GroupRepository; +import it.chalmers.gamma.app.post.PostFacade; +import it.chalmers.gamma.app.settings.domain.Settings; +import it.chalmers.gamma.app.settings.domain.SettingsRepository; +import it.chalmers.gamma.app.user.domain.*; +import it.chalmers.gamma.security.authentication.ApiAuthentication; +import it.chalmers.gamma.security.authentication.AuthenticationExtractor; +import org.springframework.stereotype.Component; + +import java.util.*; + +import static it.chalmers.gamma.app.authentication.AccessGuard.*; + +@Component +public class UserFacade extends Facade { + + private final UserRepository userRepository; + private final GroupRepository groupRepository; + private final SettingsRepository settingsRepository; + private final ClientApprovalsRepository clientApprovalsRepository; + + public UserFacade(AccessGuard accessGuard, + UserRepository userRepository, + GroupRepository groupRepository, + SettingsRepository settingsRepository, + ClientApprovalsRepository clientApprovalsRepository) { + super(accessGuard); + this.userRepository = userRepository; + this.groupRepository = groupRepository; + this.settingsRepository = settingsRepository; + this.clientApprovalsRepository = clientApprovalsRepository; + } + + public Optional get(UUID id) { + UserId userId = new UserId(id); + accessGuard.requireEither( + isSignedIn(), + userHasAcceptedClient(userId), + isApi(ApiKeyType.INFO) + ); + + Optional maybeUser = this.userRepository.get(userId).map(UserDTO::new); + return maybeUser.map(userDTO -> new UserWithGroupsDTO( + userDTO, + getUserGroups(userId) + )); + } + + private List getUserGroups(UserId userId) { + return this.groupRepository.getAllByUser(userId) + .stream() + .map(UserGroupDTO::new) + .toList(); + } + + public List getAll() { + accessGuard.require(isSignedIn()); + + return this.userRepository.getAll().stream().map(UserDTO::new).toList(); + } + + public List getAllByClientAccepting() { + this.accessGuard.require(isClientApi()); + + Settings settings = settingsRepository.getSettings(); + + if (AuthenticationExtractor.getAuthentication() instanceof ApiAuthentication apiAuthentication) { + Client client = apiAuthentication.getClient().orElseThrow(); + return clientApprovalsRepository.getAllByClientUid(client.clientUid()) + .stream() + .map(UserDTO::new) + .toList(); + } + + return Collections.emptyList(); + } + + public void setUserPassword(UUID id, String newPassword) { + accessGuard.require(isAdmin()); + this.userRepository.setPassword(new UserId(id), new UnencryptedPassword(newPassword)); + } + + public void deleteUser(UUID id) { + accessGuard.require(isAdmin()); + + try { + this.userRepository.delete(new UserId(id)); + } catch (UserRepository.UserNotFoundException e) { + e.printStackTrace(); + } + } + + public Optional getAsAdmin(UUID id) { + accessGuard.require(isAdmin()); + + UserId userId = new UserId(id); + + Optional maybeUser = this.userRepository.get(userId).map(UserExtendedDTO::new); + return maybeUser.map(userExtendedDTO -> new UserExtendedWithGroupsDTO( + userExtendedDTO, + getUserGroups(userId) + )); + + } + + public void updateUser(UpdateUser updateUser) { + accessGuard.require(isAdmin()); + + GammaUser oldUser = this.userRepository.get(new UserId(updateUser.id)).orElseThrow(); + this.userRepository.save( + oldUser.with() + .nick(new Nick(updateUser.nick)) + .firstName(new FirstName(updateUser.firstName)) + .lastName(new LastName(updateUser.lastName)) + .language(Language.valueOf(updateUser.language)) + .acceptanceYear(new AcceptanceYear(updateUser.acceptanceYear)) + .extended(oldUser.extended().with() + .email(new Email(updateUser.email)) + .build() + ) + .build() + ); + } + + public record UserDTO(String cid, + String nick, + String firstName, + String lastName, + UUID id, + int acceptanceYear) { + + public UserDTO(GammaUser user) { + this(user.cid().value(), + user.nick().value(), + user.firstName().value(), + user.lastName().value(), + user.id().value(), + user.acceptanceYear().value()); + } + } + + public record UserGroupDTO(GroupFacade.GroupDTO group, PostFacade.PostDTO post) { + public UserGroupDTO(UserMembership userMembership) { + this( + new GroupFacade.GroupDTO(userMembership.group()), + new PostFacade.PostDTO(userMembership.post()) + ); + } + } + + public record UserWithGroupsDTO(UserDTO user, List groups) { + } + + public record UserExtendedDTO(String cid, + String nick, + String firstName, + String lastName, + UUID id, + int version, + int acceptanceYear, + String email, + boolean locked, + boolean userAgreement, + String language) { + + public UserExtendedDTO(GammaUser user) { + this(user.cid().value(), + user.nick().value(), + user.firstName().value(), + user.lastName().value(), + user.id().value(), + user.extended().version(), + user.acceptanceYear().value(), + user.extended().email().value(), + user.extended().locked(), + user.extended().acceptedUserAgreement(), + user.language().name() + ); + } + } + + public record UserExtendedWithGroupsDTO(UserExtendedDTO user, List groups) { + } + + public record UpdateUser(UUID id, + String nick, + String firstName, + String lastName, + String email, + String language, + int acceptanceYear) { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/UserGdprTrainingFacade.java b/backend/src/main/java/it/chalmers/gamma/app/user/UserGdprTrainingFacade.java new file mode 100644 index 000000000..7821a1b5c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/UserGdprTrainingFacade.java @@ -0,0 +1,40 @@ +package it.chalmers.gamma.app.user; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.user.domain.GammaUser; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserRepository; +import it.chalmers.gamma.app.user.gdpr.GdprTrainedRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; + +@Service +public class UserGdprTrainingFacade extends Facade { + + private final GdprTrainedRepository gdprTrainedRepository; + private final UserRepository userRepository; + + public UserGdprTrainingFacade(AccessGuard accessGuard, GdprTrainedRepository gdprTrainedRepository, UserRepository userRepository) { + super(accessGuard); + this.gdprTrainedRepository = gdprTrainedRepository; + this.userRepository = userRepository; + } + + public List getGdprTrained() { + this.accessGuard.require(isAdmin()); + + return this.gdprTrainedRepository.getAll().stream().map(UserId::value).toList(); + } + + public void updateGdprTrainedStatus(UUID userId, boolean gdprTrained) { + this.accessGuard.require(isAdmin()); + + this.gdprTrainedRepository.setGdprTrainedStatus(new UserId(userId), gdprTrained); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/activation/ActivationCodeFacade.java b/backend/src/main/java/it/chalmers/gamma/app/user/activation/ActivationCodeFacade.java new file mode 100644 index 000000000..e04ccaa3d --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/activation/ActivationCodeFacade.java @@ -0,0 +1,56 @@ +package it.chalmers.gamma.app.user.activation; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.user.activation.domain.UserActivation; +import it.chalmers.gamma.app.user.activation.domain.UserActivationRepository; +import it.chalmers.gamma.app.user.domain.Cid; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; + +@Service +public class ActivationCodeFacade extends Facade { + + private final UserActivationRepository userActivationRepository; + + public ActivationCodeFacade(AccessGuard accessGuard, + UserActivationRepository userActivationRepository) { + super(accessGuard); + this.userActivationRepository = userActivationRepository; + } + + public Optional get(String cid) { + return this.userActivationRepository.get(new Cid(cid)) + .map(UserActivationDTO::new); + } + + public List getAllUserActivations() { + this.accessGuard.require(isAdmin()); + + return this.userActivationRepository.getAll() + .stream() + .map(UserActivationDTO::new) + .toList(); + } + + public void removeUserActivation(String cid) { + this.accessGuard.require(isAdmin()); + + this.userActivationRepository.removeActivation(new Cid(cid)); + } + + public record UserActivationDTO(String cid, + String token, + Instant createdAt) { + public UserActivationDTO(UserActivation userActivation) { + this(userActivation.cid().value(), + userActivation.token().value(), + userActivation.createdAt()); + } + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivation.java b/backend/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivation.java new file mode 100644 index 000000000..56bff3ca0 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivation.java @@ -0,0 +1,18 @@ +package it.chalmers.gamma.app.user.activation.domain; + +import it.chalmers.gamma.app.user.domain.Cid; + +import java.time.Instant; +import java.util.Objects; + +public record UserActivation(Cid cid, + UserActivationToken token, + Instant createdAt) { + + public UserActivation { + Objects.requireNonNull(cid); + Objects.requireNonNull(token); + Objects.requireNonNull(createdAt); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationRepository.java b/backend/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationRepository.java new file mode 100644 index 000000000..7a99c4c5b --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationRepository.java @@ -0,0 +1,35 @@ +package it.chalmers.gamma.app.user.activation.domain; + +import it.chalmers.gamma.app.user.domain.Cid; + +import java.util.List; +import java.util.Optional; + +public interface UserActivationRepository { + + /** + * Creates an activation token that is connected to the cid. + * If there already is a token generated, then a new one will be generated. + * + * @param cid A cid that has been allowed + * @return A token that can be used to create an account with the given cid + */ + UserActivationToken createActivationToken(Cid cid) throws CidNotAllowedException; + + Optional get(Cid cid); + + List getAll(); + + Cid getByToken(UserActivationToken token) throws TokenNotActivatedException; + + void removeActivation(Cid cid) throws CidNotActivatedException; + + class TokenNotActivatedException extends RuntimeException { + } + + class CidNotActivatedException extends RuntimeException { + } + + class CidNotAllowedException extends Exception { + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationToken.java b/backend/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationToken.java new file mode 100644 index 000000000..cf569859a --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationToken.java @@ -0,0 +1,15 @@ +package it.chalmers.gamma.app.user.activation.domain; + +import it.chalmers.gamma.util.TokenUtils; + +public record UserActivationToken(String value) { + + //TODO add validation that length must be 9 in only numbers + + public static UserActivationToken generate() { + String value = TokenUtils.generateToken(9, TokenUtils.CharacterTypes.NUMBERS); + return new UserActivationToken(value); + } + +} + diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/allowlist/AllowListFacade.java b/backend/src/main/java/it/chalmers/gamma/app/user/allowlist/AllowListFacade.java new file mode 100644 index 000000000..455af7134 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/allowlist/AllowListFacade.java @@ -0,0 +1,61 @@ +package it.chalmers.gamma.app.user.allowlist; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.apikey.domain.ApiKeyType; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.user.domain.Cid; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; +import static it.chalmers.gamma.app.authentication.AccessGuard.isApi; + +@Service +public class AllowListFacade extends Facade { + + private final AllowListRepository allowListRepository; + + public AllowListFacade(AccessGuard accessGuard, + AllowListRepository allowListRepository) { + super(accessGuard); + this.allowListRepository = allowListRepository; + } + + public List getAllowList() { + this.accessGuard.require(isAdmin()); + + return this.allowListRepository.getAllowList() + .stream() + .map(Cid::value) + .toList(); + } + + public void allow(String cid) throws AllowListRepository.AlreadyAllowedException { + this.accessGuard.requireEither( + isAdmin(), + isApi(ApiKeyType.ALLOW_LIST) + ); + + this.allowListRepository.allow(new Cid(cid)); + } + + public void removeFromAllowList(String cid) throws AllowListRepository.NotOnAllowListException { + this.accessGuard.requireEither( + isAdmin(), + isApi(ApiKeyType.ALLOW_LIST) + ); + + this.allowListRepository.remove(new Cid(cid)); + } + + public boolean isAllowed(String cid) { + this.accessGuard.requireEither( + isAdmin(), + isApi(ApiKeyType.ALLOW_LIST) + ); + + return this.allowListRepository.isAllowed(new Cid(cid)); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/allowlist/AllowListRepository.java b/backend/src/main/java/it/chalmers/gamma/app/user/allowlist/AllowListRepository.java new file mode 100644 index 000000000..80f334901 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/allowlist/AllowListRepository.java @@ -0,0 +1,23 @@ +package it.chalmers.gamma.app.user.allowlist; + +import it.chalmers.gamma.app.user.domain.Cid; + +import java.util.List; + +public interface AllowListRepository { + + void allow(Cid cid) throws AlreadyAllowedException; + + void remove(Cid cid) throws NotOnAllowListException; + + boolean isAllowed(Cid cid); + + List getAllowList(); + + class AlreadyAllowedException extends Exception { + } + + class NotOnAllowListException extends RuntimeException { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/AcceptanceYear.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/AcceptanceYear.java new file mode 100644 index 000000000..bfc777278 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/AcceptanceYear.java @@ -0,0 +1,13 @@ +package it.chalmers.gamma.app.user.domain; + +import java.io.Serializable; + +public record AcceptanceYear(int value) implements Serializable { + + public AcceptanceYear { + if (value < 2001) { + throw new IllegalArgumentException("Acceptance year cannot be less than 2001"); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/Cid.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/Cid.java new file mode 100644 index 000000000..6a4a3a74e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/Cid.java @@ -0,0 +1,35 @@ +package it.chalmers.gamma.app.user.domain; + +import it.chalmers.gamma.app.common.Id; + +import java.io.Serializable; +import java.util.Locale; + +public record Cid(String value) implements Id, UserIdentifier, Serializable { + + public Cid { + if (value == null) { + throw new NullPointerException("Cid cannot be null"); + } + + value = value.toLowerCase(Locale.ROOT); + + if (!value.matches("^([a-z]{4,12})$")) { + throw new IllegalArgumentException("Input: " + value + "; Cid length must be between 4 and 12, and only have letters between a - z"); + } + + } + + public static Cid valueOf(String cid) { + return new Cid(cid); + } + + public static boolean isValidCid(String possibleCid) { + return possibleCid != null && possibleCid.matches("^([a-z]{4,12})$"); + } + + @Override + public String getValue() { + return this.value; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/FirstName.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/FirstName.java new file mode 100644 index 000000000..bb9c06b7f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/FirstName.java @@ -0,0 +1,16 @@ +package it.chalmers.gamma.app.user.domain; + +import java.io.Serializable; + +public record FirstName(String value) implements Serializable { + + public FirstName { + if (value == null) { + throw new NullPointerException("First name cannot be null"); + } else if (value.length() < 1 || value.length() > 50) { + throw new IllegalArgumentException("First name length must be between 1 and 50"); + } + } + +} + diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/GammaUser.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/GammaUser.java new file mode 100644 index 000000000..114400a9c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/GammaUser.java @@ -0,0 +1,27 @@ +package it.chalmers.gamma.app.user.domain; + +import io.soabase.recordbuilder.core.RecordBuilder; + +import java.io.Serializable; +import java.util.Objects; + +@RecordBuilder +public record GammaUser(UserId id, + Cid cid, + Nick nick, + FirstName firstName, + LastName lastName, + AcceptanceYear acceptanceYear, + Language language, + UserExtended extended) implements GammaUserBuilder.With, Serializable { + + public GammaUser { + Objects.requireNonNull(id); + Objects.requireNonNull(cid); + Objects.requireNonNull(nick); + Objects.requireNonNull(firstName); + Objects.requireNonNull(lastName); + Objects.requireNonNull(acceptanceYear); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/Language.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/Language.java new file mode 100644 index 000000000..a19598b64 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/Language.java @@ -0,0 +1,5 @@ +package it.chalmers.gamma.app.user.domain; + +public enum Language { + SV, EN +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/LastName.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/LastName.java new file mode 100644 index 000000000..a5034403b --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/LastName.java @@ -0,0 +1,15 @@ +package it.chalmers.gamma.app.user.domain; + +import java.io.Serializable; + +public record LastName(String value) implements Serializable { + + public LastName { + if (value == null) { + throw new NullPointerException("Last name cannot be null"); + } else if (value.length() < 1 || value.length() > 50) { + throw new IllegalArgumentException("Last name length must be between 1 and 50"); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/Name.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/Name.java new file mode 100644 index 000000000..4d36ab6a1 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/Name.java @@ -0,0 +1,19 @@ +package it.chalmers.gamma.app.user.domain; + +import java.util.Locale; + +public record Name(String value) { + + public Name { + if (value == null) { + throw new NullPointerException("Name cannot be null"); + } + + value = value.toLowerCase(Locale.ROOT); + + if (!value.matches("^([0-9a-z]{3,30})$")) { + throw new IllegalArgumentException("Name: [" + value + "] must be letters a - z and be of length between 5 - 30"); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/Nick.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/Nick.java new file mode 100644 index 000000000..b022d3e9e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/Nick.java @@ -0,0 +1,15 @@ +package it.chalmers.gamma.app.user.domain; + +import java.io.Serializable; + +public record Nick(String value) implements Serializable { + + public Nick { + if (value == null) { + throw new NullPointerException("Nick cannot be null"); + } else if (value.length() < 1 || value.length() > 30) { + throw new IllegalArgumentException("Nick length must be between 1 and 30"); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/Password.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/Password.java new file mode 100644 index 000000000..e5ae4507f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/Password.java @@ -0,0 +1,23 @@ +package it.chalmers.gamma.app.user.domain; + +import java.util.regex.Pattern; + +public record Password(String value) { + + private static final Pattern passwordStartPattern = Pattern.compile("^\\{.+}.+"); + + public Password { + if (value == null) { + throw new IllegalArgumentException(); + } else if (!passwordStartPattern.matcher(value).matches()) { + throw new IllegalArgumentException("Password must start with what type the encoding of the value is," + + "such as {bcrypt} or {noop}"); + } + } + + @Override + public String toString() { + return ""; + } +} + diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/UnencryptedPassword.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/UnencryptedPassword.java new file mode 100644 index 000000000..24aaad89f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/UnencryptedPassword.java @@ -0,0 +1,13 @@ +package it.chalmers.gamma.app.user.domain; + +public record UnencryptedPassword(String value) { + + public UnencryptedPassword { + if (value == null) { + throw new NullPointerException("Password cannot be null"); + } else if (value.length() < 8) { + throw new IllegalArgumentException("Password length must be atleast 8"); + } + + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserExtended.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserExtended.java new file mode 100644 index 000000000..fd46e6e91 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserExtended.java @@ -0,0 +1,22 @@ +package it.chalmers.gamma.app.user.domain; + +import io.soabase.recordbuilder.core.RecordBuilder; +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.image.domain.ImageUri; + +/** + * Extension of User. + *

+ * Usually, records in Gamma always have to be valid, but UserExtended is an exception. + * To limit the amount of data that can accidentally be leaked, UserExtended can be partial complete. + * For example, Goldapps needs to have access to email and gdprTrained, but not the rest of the data. + */ +@RecordBuilder +public record UserExtended(Email email, + int version, + boolean acceptedUserAgreement, + boolean locked, + ImageUri avatarUri) implements UserExtendedBuilder.With { + +} + diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserId.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserId.java new file mode 100644 index 000000000..7c03fc800 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserId.java @@ -0,0 +1,32 @@ +package it.chalmers.gamma.app.user.domain; + +import it.chalmers.gamma.app.common.Id; + +import java.util.Objects; +import java.util.UUID; + +public record UserId(UUID value) implements Id, UserIdentifier { + + public UserId { + Objects.requireNonNull(value); + } + + public static UserId generate() { + return new UserId(UUID.randomUUID()); + } + + public static UserId valueOf(String value) { + return new UserId(UUID.fromString(value)); + } + + public static boolean validUserId(String possibleUserId) { + return possibleUserId != null + && possibleUserId + .matches("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/"); + } + + @Override + public UUID getValue() { + return value; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserIdentifier.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserIdentifier.java new file mode 100644 index 000000000..ab3e04611 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserIdentifier.java @@ -0,0 +1,4 @@ +package it.chalmers.gamma.app.user.domain; + +public interface UserIdentifier { +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserMembership.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserMembership.java new file mode 100644 index 000000000..150519a44 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserMembership.java @@ -0,0 +1,17 @@ +package it.chalmers.gamma.app.user.domain; + +import it.chalmers.gamma.app.group.domain.Group; +import it.chalmers.gamma.app.group.domain.UnofficialPostName; +import it.chalmers.gamma.app.post.domain.Post; + +import java.util.Objects; + +public record UserMembership(Post post, + Group group, + UnofficialPostName unofficialPostName) { + public UserMembership { + Objects.requireNonNull(post); + Objects.requireNonNull(group); + Objects.requireNonNull(unofficialPostName); + } +} \ No newline at end of file diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserRepository.java b/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserRepository.java new file mode 100644 index 000000000..bb1d53332 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/domain/UserRepository.java @@ -0,0 +1,46 @@ +package it.chalmers.gamma.app.user.domain; + +import it.chalmers.gamma.app.common.Email; + +import java.util.List; +import java.util.Optional; + +public interface UserRepository { + + void create(GammaUser user, UnencryptedPassword password) + throws UserAlreadyExistsException, CidAlreadyInUseException, EmailAlreadyInUseException; + + void save(GammaUser user); + + void delete(UserId userId) throws UserNotFoundException; + + List getAll(); + + Optional get(UserId userId); + + Optional get(Cid cid); + + Optional get(Email email); + + boolean checkPassword(UserId userId, UnencryptedPassword password) throws UserNotFoundException; + + void setPassword(UserId userId, UnencryptedPassword newPassword) throws UserNotFoundException; + + void acceptUserAgreement(UserId userId) throws UserNotFoundException; + + class UserNotFoundException extends RuntimeException { + } + + /** + * A user with the given id already exists. Use save instead. + */ + class UserAlreadyExistsException extends RuntimeException { + } + + class CidAlreadyInUseException extends Exception { + } + + class EmailAlreadyInUseException extends Exception { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/gdpr/GdprTrainedRepository.java b/backend/src/main/java/it/chalmers/gamma/app/user/gdpr/GdprTrainedRepository.java new file mode 100644 index 000000000..436bd5114 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/gdpr/GdprTrainedRepository.java @@ -0,0 +1,13 @@ +package it.chalmers.gamma.app.user.gdpr; + +import it.chalmers.gamma.app.user.domain.UserId; + +import java.util.List; + +public interface GdprTrainedRepository { + + void setGdprTrainedStatus(UserId userId, boolean gdprTrained); + List getAll(); + + +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/passwordreset/UserResetPasswordFacade.java b/backend/src/main/java/it/chalmers/gamma/app/user/passwordreset/UserResetPasswordFacade.java new file mode 100644 index 000000000..2ba52f256 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/passwordreset/UserResetPasswordFacade.java @@ -0,0 +1,97 @@ +package it.chalmers.gamma.app.user.passwordreset; + +import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.mail.domain.MailService; +import it.chalmers.gamma.app.user.domain.GammaUser; +import it.chalmers.gamma.app.user.domain.UnencryptedPassword; +import it.chalmers.gamma.app.user.domain.UserRepository; +import it.chalmers.gamma.app.user.passwordreset.domain.PasswordReset; +import it.chalmers.gamma.app.user.passwordreset.domain.PasswordResetRepository; +import it.chalmers.gamma.app.user.passwordreset.domain.PasswordResetToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isNotSignedIn; + +@Service +public class UserResetPasswordFacade extends Facade { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserResetPasswordFacade.class); + private final MailService mailService; + private final UserRepository userRepository; + private final PasswordResetRepository passwordResetRepository; + + public UserResetPasswordFacade(AccessGuard accessGuard, + MailService mailService, + UserRepository userRepository, + PasswordResetRepository passwordResetRepository) { + super(accessGuard); + this.mailService = mailService; + this.userRepository = userRepository; + this.passwordResetRepository = passwordResetRepository; + } + + public void startResetPasswordProcess(String emailString) throws PasswordResetProcessException { + this.accessGuard.require(isNotSignedIn()); + + Email email = new Email(emailString); + + try { + PasswordResetToken token = this.passwordResetRepository.createNewToken(email); + sendPasswordResetTokenMail(email, token); + } catch (PasswordResetRepository.UserNotFoundException e) { + LOGGER.debug("Someone tried to reset the password for the email " + emailString + " that doesn't exist"); + throw new PasswordResetProcessException(); + } + + } + + public void finishResetPasswordProcess(String emailString, String inputTokenRaw, String newPassword) throws PasswordResetProcessException { + this.accessGuard.require(isNotSignedIn()); + + Email email = new Email(emailString); + + Optional maybeUser = this.userRepository.get(email); + + if (maybeUser.isEmpty()) { + LOGGER.debug("Someone tried to finish the reset value process for " + emailString + " that doesn't exist"); + throw new PasswordResetProcessException(); + } + + GammaUser user = maybeUser.get(); + Optional maybeToken = this.passwordResetRepository.getToken(user.id()); + + if (maybeToken.isEmpty()) { + LOGGER.debug("No code exists for the user " + user); + throw new PasswordResetProcessException(); + } + + PasswordResetToken token = maybeToken.get(); + PasswordResetToken inputToken = new PasswordResetToken(inputTokenRaw); + + if (token.equals(inputToken)) { + this.passwordResetRepository.removeToken(token); + this.userRepository.setPassword(user.id(), new UnencryptedPassword(newPassword)); + } else { + LOGGER.debug("Incorrect value reset code for user " + user); + throw new PasswordResetProcessException(); + } + } + + private void sendPasswordResetTokenMail(Email email, PasswordResetToken token) { + String subject = "Password reset for Account at IT division of Chalmers"; + String message = "A value reset have been requested for this account, if you have not requested " + + "this mail, feel free to ignore it. \n Your reset code : " + token.value(); + this.mailService.sendMail(email.value(), subject, message); + } + + //Vague for security reasons + public static class PasswordResetProcessException extends Exception { + + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordReset.java b/backend/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordReset.java new file mode 100644 index 000000000..9a9ed2761 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordReset.java @@ -0,0 +1,17 @@ +package it.chalmers.gamma.app.user.passwordreset.domain; + +import it.chalmers.gamma.app.user.domain.UserId; + +import java.time.Instant; +import java.util.Objects; + +public record PasswordReset(UserId userId, PasswordResetToken token, Instant createdAt) { + + public PasswordReset { + Objects.requireNonNull(userId); + Objects.requireNonNull(token); + Objects.requireNonNull(createdAt); + } + +} + diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetRepository.java b/backend/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetRepository.java new file mode 100644 index 000000000..947913cc9 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetRepository.java @@ -0,0 +1,19 @@ +package it.chalmers.gamma.app.user.passwordreset.domain; + +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.user.domain.GammaUser; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserIdentifier; + +import java.util.Optional; + +public interface PasswordResetRepository { + + PasswordResetToken createNewToken(Email email) throws UserNotFoundException; + + Optional getToken(UserId id); + + void removeToken(PasswordResetToken token); + + class UserNotFoundException extends Exception {} +} diff --git a/backend/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetToken.java b/backend/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetToken.java new file mode 100644 index 000000000..ef37b34bf --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetToken.java @@ -0,0 +1,16 @@ +package it.chalmers.gamma.app.user.passwordreset.domain; + +import it.chalmers.gamma.util.TokenUtils; + +public record PasswordResetToken(String value) { + + public static PasswordResetToken generate() { + String value = TokenUtils.generateToken( + 75, + TokenUtils.CharacterTypes.UPPERCASE, + TokenUtils.CharacterTypes.NUMBERS + ); + return new PasswordResetToken(value); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/bootstrap/ApiKeyBootstrap.java b/backend/src/main/java/it/chalmers/gamma/bootstrap/ApiKeyBootstrap.java new file mode 100644 index 000000000..28a1d790d --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/bootstrap/ApiKeyBootstrap.java @@ -0,0 +1,51 @@ +package it.chalmers.gamma.bootstrap; + +import it.chalmers.gamma.app.apikey.domain.*; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class ApiKeyBootstrap { + + private static final Logger LOGGER = LoggerFactory.getLogger(ApiKeyBootstrap.class); + + private final ApiKeyRepository apiKeyRepository; + private final BootstrapSettings bootstrapSettings; + + public ApiKeyBootstrap(ApiKeyRepository apiKeyRepository, + BootstrapSettings bootstrapSettings) { + this.apiKeyRepository = apiKeyRepository; + this.bootstrapSettings = bootstrapSettings; + } + + public void ensureApiKeys() { + if (!this.bootstrapSettings.mocking() || this.apiKeyRepository.getAll().size() != 1) { + return; + } + + LOGGER.info("========== API BOOTSTRAP =========="); + LOGGER.info("Generating mock api keys..."); + + for (ApiKeyType apiKeyType : ApiKeyType.values()) { + if (apiKeyType != ApiKeyType.CLIENT) { + ApiKeyToken apiKeyToken = new ApiKeyToken(apiKeyType.name() + "-super-secret-code"); + this.apiKeyRepository.create( + new ApiKey( + ApiKeyId.generate(), + new PrettyName(apiKeyType.name() + "-mock"), + new Text(), + apiKeyType, + apiKeyToken + ) + ); + + LOGGER.info("Api key of type " + apiKeyType.name() + " has been generated with code: " + apiKeyToken.value()); + } + } + LOGGER.info("========== =========="); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/bootstrap/BootstrapAuthenticated.java b/backend/src/main/java/it/chalmers/gamma/bootstrap/BootstrapAuthenticated.java new file mode 100644 index 000000000..8e4fb4c18 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/bootstrap/BootstrapAuthenticated.java @@ -0,0 +1,36 @@ +package it.chalmers.gamma.bootstrap; + +import it.chalmers.gamma.security.authentication.LocalRunnerAuthentication; +import org.springframework.security.authentication.AbstractAuthenticationToken; + +import java.time.Instant; + +import static org.springframework.security.core.authority.AuthorityUtils.NO_AUTHORITIES; + +public class BootstrapAuthenticated extends AbstractAuthenticationToken { + + private final LocalRunnerAuthentication localRunnerPrincipal; + + public BootstrapAuthenticated() { + super(NO_AUTHORITIES); + setAuthenticated(true); + + final String instantiatedAt = "Local runner instantiated at " + Instant.now(); + localRunnerPrincipal = new LocalRunnerAuthentication() { + @Override + public String toString() { + return instantiatedAt; + } + }; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return localRunnerPrincipal; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/bootstrap/BootstrapSettings.java b/backend/src/main/java/it/chalmers/gamma/bootstrap/BootstrapSettings.java new file mode 100644 index 000000000..4e1721c95 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/bootstrap/BootstrapSettings.java @@ -0,0 +1,4 @@ +package it.chalmers.gamma.bootstrap; + +public record BootstrapSettings(boolean adminSetup, boolean mocking) { +} diff --git a/backend/src/main/java/it/chalmers/gamma/bootstrap/ClientBootstrap.java b/backend/src/main/java/it/chalmers/gamma/bootstrap/ClientBootstrap.java new file mode 100644 index 000000000..4a9ee9a8d --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/bootstrap/ClientBootstrap.java @@ -0,0 +1,78 @@ +package it.chalmers.gamma.bootstrap; + +import it.chalmers.gamma.app.apikey.domain.ApiKey; +import it.chalmers.gamma.app.apikey.domain.ApiKeyId; +import it.chalmers.gamma.app.apikey.domain.ApiKeyToken; +import it.chalmers.gamma.app.apikey.domain.ApiKeyType; +import it.chalmers.gamma.app.client.domain.*; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; +import it.chalmers.gamma.property.DefaultOAuth2Client; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.Arrays; + +@Component +public class ClientBootstrap { + + private static final Logger LOGGER = LoggerFactory.getLogger(ClientBootstrap.class); + private final BootstrapSettings bootstrapSettings; + private final DefaultOAuth2Client defaultOAuth2Client; + private final ClientRepository clientRepository; + + public ClientBootstrap(BootstrapSettings bootstrapSettings, + DefaultOAuth2Client defaultOAuth2Client, + ClientRepository clientRepository) { + this.bootstrapSettings = bootstrapSettings; + this.defaultOAuth2Client = defaultOAuth2Client; + this.clientRepository = clientRepository; + } + + public void runOauthClient() { + if (!this.bootstrapSettings.mocking() || !this.clientRepository.getAll().isEmpty()) { + return; + } + LOGGER.info("========== CLIENT BOOTSTRAP =========="); + LOGGER.info("Creating test client..."); + + ClientUid clientUid = ClientUid.generate(); + ClientId clientId = new ClientId(defaultOAuth2Client.clientId()); + ClientSecret clientSecret = new ClientSecret("{noop}" + defaultOAuth2Client.clientSecret()); + ApiKeyToken apiKeyToken = new ApiKeyToken(defaultOAuth2Client.apiKey()); + PrettyName prettyName = new PrettyName(defaultOAuth2Client.clientName()); + ClientRedirectUrl clientRedirectUrl = new ClientRedirectUrl(defaultOAuth2Client.redirectUrl()); + + this.clientRepository.save( + new Client( + clientUid, + clientId, + clientSecret, + clientRedirectUrl, + prettyName, + new Text(), + Arrays.stream(defaultOAuth2Client.scopes().split(",")) + .map(String::toUpperCase) + .map(Scope::valueOf) + .toList(), + new ApiKey( + ApiKeyId.generate(), + prettyName, + new Text(), + ApiKeyType.CLIENT, + apiKeyToken + ), + new ClientOwnerOfficial(), + null + ) + ); + + LOGGER.info("Client generated with information:"); + LOGGER.info("ClientId: " + clientId.value()); + LOGGER.info("ClientSecret: " + clientSecret.value().substring("{noop}".length())); + LOGGER.info("Client redirect uri: " + clientRedirectUrl.value()); + LOGGER.info("An API key was also generated with the client, it has the code: " + apiKeyToken.value()); + LOGGER.info("========== =========="); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/bootstrap/EnsureAnAdminUserBootstrap.java b/backend/src/main/java/it/chalmers/gamma/bootstrap/EnsureAnAdminUserBootstrap.java new file mode 100644 index 000000000..a61a366a4 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/bootstrap/EnsureAnAdminUserBootstrap.java @@ -0,0 +1,110 @@ +package it.chalmers.gamma.bootstrap; + +import it.chalmers.gamma.app.admin.domain.AdminRepository; +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.user.domain.*; +import it.chalmers.gamma.app.user.gdpr.GdprTrainedRepository; +import it.chalmers.gamma.util.TokenUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class EnsureAnAdminUserBootstrap { + + private static final Logger LOGGER = LoggerFactory.getLogger(EnsureAnAdminUserBootstrap.class); + + private final UserRepository userRepository; + private final AdminRepository adminRepository; + private final GdprTrainedRepository gdprTrainedRepository; + private final BootstrapSettings bootstrapSettings; + private final boolean production; + + public EnsureAnAdminUserBootstrap(UserRepository userRepository, + AdminRepository adminRepository, + GdprTrainedRepository gdprTrainedRepository, BootstrapSettings bootstrapSettings, + @Value("${application.production}") + boolean production) { + this.userRepository = userRepository; + this.adminRepository = adminRepository; + this.gdprTrainedRepository = gdprTrainedRepository; + this.bootstrapSettings = bootstrapSettings; + this.production = production; + } + + public void ensureAnAdminUser() { + LOGGER.info("========== ENSURE AN ADMIN BOOTSTRAP =========="); + + if (!this.bootstrapSettings.adminSetup()) { + LOGGER.info("Ensuring admin setup is disabled. I hope you know what you're doing..."); + return; + } + + if(adminRepository.getAll().size() > 0) { + LOGGER.info("There is already at least one user that is admin. Not creating a new admin user..."); + return; + } + + String admin = "admin"; + + if (this.userRepository.get(new Cid(admin)).isPresent()) { + LOGGER.error("There's no user that is admin right now, but there is a user that is named admin. " + + "Note that there are no admin users right now and none will be created." + ); + return; + } + + String password; + if(!production) { + password = "password"; + } else { + password = TokenUtils.generateToken( + 75, + TokenUtils.CharacterTypes.LOWERCASE, + TokenUtils.CharacterTypes.UPPERCASE, + TokenUtils.CharacterTypes.NUMBERS + ); + } + + UserId adminId = UserId.generate(); + String name = "admin"; + + GammaUser adminUser = new GammaUser( + adminId, + new Cid(name), + new Nick(name), + new FirstName(name), + new LastName(name), + new AcceptanceYear(2018), + Language.EN, + new UserExtended( + new Email(name + "@chalmers.it"), + 0, + true, + false, + null + ) + ); + + try { + this.userRepository.create( + adminUser, + new UnencryptedPassword(password) + ); + } catch (UserRepository.CidAlreadyInUseException | UserRepository.EmailAlreadyInUseException e) { + LOGGER.error(e.getMessage()); + return; + } + + this.adminRepository.setAdmin(adminUser.id(), true); + this.gdprTrainedRepository.setGdprTrainedStatus(adminUser.id(), true); + + LOGGER.info("Admin user created!"); + LOGGER.info("cid: " + name); + LOGGER.info("password: " + password); + + LOGGER.info("========== =========="); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/bootstrap/EnsureSettingsBootstrap.java b/backend/src/main/java/it/chalmers/gamma/bootstrap/EnsureSettingsBootstrap.java new file mode 100644 index 000000000..bf1cefea9 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/bootstrap/EnsureSettingsBootstrap.java @@ -0,0 +1,39 @@ +package it.chalmers.gamma.bootstrap; + +import it.chalmers.gamma.app.settings.domain.Settings; +import it.chalmers.gamma.app.settings.domain.SettingsRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.Collections; + +@Component +public class EnsureSettingsBootstrap { + + private static final Logger LOGGER = LoggerFactory.getLogger(EnsureSettingsBootstrap.class); + + private final SettingsRepository settingsRepository; + + public EnsureSettingsBootstrap(SettingsRepository settingsRepository) { + this.settingsRepository = settingsRepository; + } + + public void ensureAppSettings() { + if (!this.settingsRepository.hasSettings()) { + LOGGER.info("========== ENSURE APP SETTINGS BOOTSTRAP =========="); + + Settings settings = new Settings( + Instant.now(), + Collections.emptyList() + ); + this.settingsRepository.setSettings(settings); + LOGGER.info("Adding default settings"); + LOGGER.info(String.valueOf(settings)); + + LOGGER.info("========== =========="); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/bootstrap/GroupBootstrap.java b/backend/src/main/java/it/chalmers/gamma/bootstrap/GroupBootstrap.java new file mode 100644 index 000000000..0ed8f703c --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/bootstrap/GroupBootstrap.java @@ -0,0 +1,120 @@ +package it.chalmers.gamma.bootstrap; + +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.group.domain.*; +import it.chalmers.gamma.app.post.domain.PostId; +import it.chalmers.gamma.app.post.domain.PostRepository; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupRepository; +import it.chalmers.gamma.app.user.domain.Name; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; + +@Component +public class GroupBootstrap { + + private static final Logger LOGGER = LoggerFactory.getLogger(GroupBootstrap.class); + + private final MockData mockData; + private final GroupRepository groupRepository; + private final PostRepository postRepository; + private final UserRepository userRepository; + private final SuperGroupRepository superGroupRepository; + private final BootstrapSettings bootstrapSettings; + + public GroupBootstrap(MockData mockData, + GroupRepository groupRepository, + PostRepository postRepository, + UserRepository userRepository, + SuperGroupRepository superGroupRepository, + BootstrapSettings bootstrapSettings) { + this.mockData = mockData; + this.groupRepository = groupRepository; + this.postRepository = postRepository; + this.userRepository = userRepository; + this.superGroupRepository = superGroupRepository; + this.bootstrapSettings = bootstrapSettings; + } + + public void createGroups() { + if (!this.bootstrapSettings.mocking() || !this.groupRepository.getAll().isEmpty()) { + return; + } + + LOGGER.info("========== GROUP BOOTSTRAP =========="); + + Calendar activeGroupBecomesActive = toCalendar( + Instant.now().minus(1, ChronoUnit.DAYS) + ); + Calendar inactiveGroupBecomesActive = toCalendar( + Instant.now() + .minus(366, ChronoUnit.DAYS) + ); + int activeYear = activeGroupBecomesActive.get(Calendar.YEAR); + int inactiveYear = inactiveGroupBecomesActive.get(Calendar.YEAR); + + mockData.groups().forEach(mockGroup -> { + String type = mockData.superGroups() + .stream() + .filter(sg -> sg.id().equals(mockGroup.superGroupId())) + .findFirst() + .orElseThrow() + .type(); + + boolean active = !type.equalsIgnoreCase("alumni"); + int year = active ? activeYear : inactiveYear; + Name name = new Name(mockGroup.name() + year); + PrettyName prettyName = new PrettyName(mockGroup.prettyName() + year); + + Group group = new Group( + new GroupId(mockGroup.id()), + 0, + name, + prettyName, + superGroupRepository.get(new SuperGroupId(mockGroup.superGroupId())).orElseThrow(), + mockGroup.members() + .stream() + .map(mockMembership -> new GroupMember( + postRepository.get(new PostId(mockMembership.postId())).orElseThrow(), + mockMembership.unofficialPostName() == null + ? UnofficialPostName.none() + : new UnofficialPostName(mockMembership.unofficialPostName()), + userRepository.get(new UserId(mockMembership.userId())) + .orElseThrow(IllegalArgumentException::new) + )) + .toList(), + Optional.empty(), + Optional.empty() + ); + + try { + this.groupRepository.save(group); + } catch (GroupRepository.GroupNameAlreadyExistsException e) { + e.printStackTrace(); + } + }); + + LOGGER.info("========== =========="); + } + + private Calendar toCalendar(Instant i) { + return GregorianCalendar.from( + ZonedDateTime.ofInstant( + i, + ZoneOffset.UTC + ) + ); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/bootstrap/MiscBootstrap.java b/backend/src/main/java/it/chalmers/gamma/bootstrap/MiscBootstrap.java new file mode 100644 index 000000000..c1a7a7e09 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/bootstrap/MiscBootstrap.java @@ -0,0 +1,53 @@ +package it.chalmers.gamma.bootstrap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +@Component +public class MiscBootstrap { + + private static final Logger LOGGER = LoggerFactory.getLogger(MiscBootstrap.class); + @Value("classpath:/image/default_user_avatar.jpg") + private Resource defaultUserAvatar; + @Value("classpath:/image/default_group_banner.jpg") + private Resource defaultGroupBanner; + @Value("classpath:/image/default_group_avatar.jpg") + private Resource defaultGroupAvatar; + @Value("${application.files.path}") + private String targetDir; + + public void runImageBootstrap() { + assureFileExistsInUpload(defaultUserAvatar); + assureFileExistsInUpload(defaultGroupAvatar); + assureFileExistsInUpload(defaultGroupBanner); + } + + private void assureFileExistsInUpload(Resource resource) { + File targetFile = new File(String.format("%s/%s", this.targetDir, resource.getFilename())); + if (!targetFile.exists()) { + LOGGER.info("default file: " + resource.getFilename() + " does not exist in " + this.targetDir + ", creating a new one"); + try { + File defaultFile = resource.getFile(); + if (defaultFile.isFile()) { + File targetDirFile = new File(this.targetDir); + if (!targetDirFile.mkdir()) { + LOGGER.warn("Could not create target directory"); + } + Files.copy(defaultFile.toPath(), targetFile.toPath()); + } else { + throw new IOException(); + } + } catch (IOException e) { + LOGGER.warn("Could not copy: " + resource.getFilename()); + LOGGER.error(e.getMessage()); + } + } + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/bootstrap/MockBootstrap.java b/backend/src/main/java/it/chalmers/gamma/bootstrap/MockBootstrap.java new file mode 100644 index 000000000..494a71c5d --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/bootstrap/MockBootstrap.java @@ -0,0 +1,48 @@ +package it.chalmers.gamma.bootstrap; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class MockBootstrap { + + private static final Logger LOGGER = LoggerFactory.getLogger(MockBootstrap.class); + + private final ResourceLoader resourceLoader; + + public MockBootstrap(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @Bean + public BootstrapSettings loadBootstrapSettings(@Value("${application.admin-setup}") boolean adminSetup, + @Value("${application.mocking}") boolean mocking) { + return new BootstrapSettings(adminSetup, mocking); + } + + @Bean + public MockData mockData(BootstrapSettings bootstrapSettings) { + if (!bootstrapSettings.mocking()) { + LOGGER.info("Not running mock..."); + return MockData.empty(); + } + + Resource resource = this.resourceLoader.getResource("classpath:/mock/mock.json"); + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.readValue(resource.getInputStream(), MockData.class); + } catch (IOException e) { + LOGGER.error("Error when trying to read mock.json", e); + return MockData.empty(); + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/bootstrap/MockData.java b/backend/src/main/java/it/chalmers/gamma/bootstrap/MockData.java new file mode 100644 index 000000000..8c4f5db3b --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/bootstrap/MockData.java @@ -0,0 +1,60 @@ +package it.chalmers.gamma.bootstrap; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +public record MockData(List users, + List groups, + List superGroups, + List posts) { + + public static MockData empty() { + return new MockData( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + ); + } + + public record MockGroup(UUID id, + String name, + String prettyName, + List members, + UUID superGroupId) { + } + + public record MockMembership(UUID userId, + UUID postId, + String unofficialPostName) { + } + + + public record MockText(String sv, String en) { + } + + public record MockPost(UUID id, + MockText postName) { + } + + public record MockSuperGroup(UUID id, + String name, + String prettyName, + String type, + List authorities) { + } + + public record MockUser( + UUID id, + String cid, + String nick, + String firstName, + String lastName, + int acceptanceYear, + boolean admin) { + } + +} + + diff --git a/backend/src/main/java/it/chalmers/gamma/bootstrap/PostBootstrap.java b/backend/src/main/java/it/chalmers/gamma/bootstrap/PostBootstrap.java new file mode 100644 index 000000000..6a4efa3a7 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/bootstrap/PostBootstrap.java @@ -0,0 +1,53 @@ +package it.chalmers.gamma.bootstrap; + +import it.chalmers.gamma.app.common.Text; +import it.chalmers.gamma.app.group.domain.EmailPrefix; +import it.chalmers.gamma.app.post.domain.Post; +import it.chalmers.gamma.app.post.domain.PostId; +import it.chalmers.gamma.app.post.domain.PostRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class PostBootstrap { + + private static final Logger LOGGER = LoggerFactory.getLogger(PostBootstrap.class); + + private final MockData mockData; + private final PostRepository postRepository; + private final BootstrapSettings bootstrapSettings; + + public PostBootstrap(MockData mockData, + PostRepository postRepository, + BootstrapSettings bootstrapSettings) { + this.mockData = mockData; + this.postRepository = postRepository; + this.bootstrapSettings = bootstrapSettings; + } + + public void createPosts() { + if (!this.bootstrapSettings.mocking() || !this.postRepository.getAll().isEmpty()) { + return; + } + + LOGGER.info("========== POST BOOTSTRAP =========="); + + mockData.posts().forEach(mockPost -> + this.postRepository.save( + new Post( + new PostId(mockPost.id()), + 0, + new Text( + mockPost.postName().sv(), + mockPost.postName().en() + ), + EmailPrefix.none() + ) + ) + ); + LOGGER.info("Posts created"); + LOGGER.info("========== =========="); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/bootstrap/SuperGroupBootstrap.java b/backend/src/main/java/it/chalmers/gamma/bootstrap/SuperGroupBootstrap.java new file mode 100644 index 000000000..76e619a7e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/bootstrap/SuperGroupBootstrap.java @@ -0,0 +1,65 @@ +package it.chalmers.gamma.bootstrap; + +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; +import it.chalmers.gamma.app.supergroup.domain.*; +import it.chalmers.gamma.app.user.domain.Name; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class SuperGroupBootstrap { + + private static final Logger LOGGER = LoggerFactory.getLogger(SuperGroupBootstrap.class); + + private final MockData mockData; + private final SuperGroupTypeRepository superGroupTypeRepository; + private final SuperGroupRepository superGroupRepository; + private final BootstrapSettings bootstrapSettings; + + public SuperGroupBootstrap(MockData mockData, + SuperGroupTypeRepository superGroupTypeRepository, + SuperGroupRepository superGroupRepository, + BootstrapSettings bootstrapSettings) { + this.mockData = mockData; + this.superGroupTypeRepository = superGroupTypeRepository; + this.superGroupRepository = superGroupRepository; + this.bootstrapSettings = bootstrapSettings; + } + + public void createSuperGroups() { + if (!this.bootstrapSettings.mocking() || !this.superGroupRepository.getAll().isEmpty()) { + return; + } + + LOGGER.info("========== SUPERGROUP BOOTSTRAP =========="); + + mockData.superGroups().stream().map(MockData.MockSuperGroup::type).distinct().forEach(type -> { + try { + this.superGroupTypeRepository.add(new SuperGroupType(type)); + } catch (SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException e) { + LOGGER.error("Error creating supergroup type: " + type + ";"); + } + }); + + LOGGER.info("Supergroup types created"); + + mockData.superGroups().forEach(mockSuperGroup -> { + this.superGroupRepository.save(new SuperGroup( + new SuperGroupId(mockSuperGroup.id()), + 0, + new Name(mockSuperGroup.name()), + new PrettyName(mockSuperGroup.prettyName()), + new SuperGroupType(mockSuperGroup.type()), + new Text()) + ); + // LOGGER.error("Error creating supergroup: " + mockSuperGroup.name() + "; Super group already exists, skipping..."); + }); + + LOGGER.info("Supergroups created"); + + LOGGER.info("========== =========="); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/bootstrap/UserBootstrap.java b/backend/src/main/java/it/chalmers/gamma/bootstrap/UserBootstrap.java new file mode 100644 index 000000000..ec692a538 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/bootstrap/UserBootstrap.java @@ -0,0 +1,70 @@ +package it.chalmers.gamma.bootstrap; + +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.user.domain.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.stream.Collectors; + +@Component +public class UserBootstrap { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserBootstrap.class); + + private final MockData mockData; + private final UserRepository userRepository; + private final BootstrapSettings bootstrapSettings; + + public UserBootstrap(MockData mockData, + UserRepository userRepository, + BootstrapSettings bootstrapSettings) { + this.mockData = mockData; + this.userRepository = userRepository; + this.bootstrapSettings = bootstrapSettings; + } + + public void createUsers() { + if (!this.bootstrapSettings.mocking() || this.userRepository.getAll().size() > 1) { + return; + } + + LOGGER.info("========== USER BOOTSTRAP =========="); + + this.mockData.users().forEach(mockUser -> { + try { + this.userRepository.create( + new GammaUser( + new UserId(mockUser.id()), + new Cid(mockUser.cid()), + new Nick(mockUser.nick()), + new FirstName(mockUser.firstName()), + new LastName(mockUser.lastName()), + new AcceptanceYear(mockUser.acceptanceYear()), + Language.EN, + new UserExtended( + new Email(mockUser.cid() + "@student.chalmers.it"), + 0, + true, + false, + null + ) + ), + new UnencryptedPassword("password") + ); + } catch (UserRepository.CidAlreadyInUseException | UserRepository.EmailAlreadyInUseException e) { + e.printStackTrace(); + } + }); + + LOGGER.info("Generated the users: " + + this.mockData.users() + .stream() + .map(MockData.MockUser::cid) + .collect(Collectors.joining(", ")) + ); + LOGGER.info("Use a cid from the row above and use the value: value to sign in"); + LOGGER.info("========== =========="); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/property/DefaultOAuth2Client.java b/backend/src/main/java/it/chalmers/gamma/property/DefaultOAuth2Client.java new file mode 100644 index 000000000..e19182c67 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/property/DefaultOAuth2Client.java @@ -0,0 +1,14 @@ +package it.chalmers.gamma.property; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("application.default-oauth2-client") +public record DefaultOAuth2Client( + String clientName, + String clientId, + String clientSecret, + String redirectUrl, + String apiKey, + String scopes +) { +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/CookieConfig.java b/backend/src/main/java/it/chalmers/gamma/security/CookieConfig.java new file mode 100644 index 000000000..34f82d5e1 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/CookieConfig.java @@ -0,0 +1,59 @@ +package it.chalmers.gamma.security; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier; +import org.springframework.context.annotation.Bean; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.session.web.http.CookieSerializer; +import org.springframework.session.web.http.DefaultCookieSerializer; +import org.springframework.stereotype.Component; + +/** + * All other cookie config can be found in application.yml and application-production.yml + */ +@Component +public class CookieConfig { + + @Value("${application.production}") + private boolean production; + + @Value("${application.cookie.domain}") + private String domain; + + @Value("${application.cookie.path}") + private String path; + + @Value("${application.cookie.validity-time}") + private int validityTime; + + @Bean + public CookieSerializer cookieSerializer() { + DefaultCookieSerializer serializer = new DefaultCookieSerializer(); + serializer.setSameSite("STRICT"); + serializer.setUseSecureCookie(this.production); + serializer.setCookieName("gamma"); + serializer.setDomainName(this.domain); + serializer.setUseHttpOnlyCookie(true); + serializer.setCookiePath(this.path); + serializer.setCookieMaxAge(this.validityTime); + return serializer; + } + + @Bean + public CookieCsrfTokenRepository cookieCsrfTokenRepository() { + CookieCsrfTokenRepository repo = new CookieCsrfTokenRepository(); + repo.setCookieDomain(this.domain); + repo.setCookiePath(this.path); + repo.setSecure(this.production); + repo.setCookieHttpOnly(false); + return repo; + } + + // Sets the CSRF cookie to use strict, since the above repository doesn't support it. + @Bean + public CookieSameSiteSupplier applicationCookieSameSiteSupplier() { + return CookieSameSiteSupplier.ofStrict().whenHasName("XSRF-TOKEN"); + } + +} \ No newline at end of file diff --git a/backend/src/main/java/it/chalmers/gamma/security/CorsConfig.java b/backend/src/main/java/it/chalmers/gamma/security/CorsConfig.java new file mode 100644 index 000000000..348238b66 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/CorsConfig.java @@ -0,0 +1,24 @@ +package it.chalmers.gamma.security; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +public class CorsConfig { + + @Bean + CorsConfigurationSource corsConfigurationSource(@Value("${application.allowed-origin}") String frontendUrl) { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + config.addAllowedOrigin(frontendUrl); + source.registerCorsConfiguration("/**", config); + return source; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/GammaRequestCache.java b/backend/src/main/java/it/chalmers/gamma/security/GammaRequestCache.java new file mode 100644 index 000000000..9fee6a436 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/GammaRequestCache.java @@ -0,0 +1,40 @@ +package it.chalmers.gamma.security; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.security.web.savedrequest.SavedRequest; +import org.springframework.stereotype.Component; + +@Component +public class GammaRequestCache implements RequestCache { + + private final RequestCache requestCache; + + public GammaRequestCache() { + requestCache = new HttpSessionRequestCache(); + } + + @Override + public void saveRequest(HttpServletRequest request, HttpServletResponse response) { + if(request.getAuthType() == null) { + requestCache.saveRequest(request, response); + } + } + + @Override + public SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response) { + return requestCache.getRequest(request, response); + } + + @Override + public HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response) { + return requestCache.getMatchingRequest(request, response); + } + + @Override + public void removeRequest(HttpServletRequest request, HttpServletResponse response) { + requestCache.removeRequest(request, response); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/JWKConfig.java b/backend/src/main/java/it/chalmers/gamma/security/JWKConfig.java new file mode 100644 index 000000000..a3b6e92c2 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/JWKConfig.java @@ -0,0 +1,43 @@ +package it.chalmers.gamma.security; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.UUID; + +@Configuration +public class JWKConfig { + + private static KeyPair generateRsaKey() throws NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + return keyPairGenerator.generateKeyPair(); + } + + @Bean + public JWKSource jwkSource(RSAKey rsaKey) { + JWKSet jwkSet = new JWKSet(rsaKey); + return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); + } + + @Bean + public RSAKey generateRsa() throws NoSuchAlgorithmException { + KeyPair keyPair = generateRsaKey(); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + return new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/JWTConfig.java b/backend/src/main/java/it/chalmers/gamma/security/JWTConfig.java new file mode 100644 index 000000000..8edf019f9 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/JWTConfig.java @@ -0,0 +1,20 @@ +package it.chalmers.gamma.security; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.RSAKey; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; + +@Configuration +public class JWTConfig { + + @Bean + public JwtDecoder jwtDecoder(RSAKey rsaKey) throws JOSEException { + return NimbusJwtDecoder + .withPublicKey(rsaKey.toRSAPublicKey()) + .build(); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/LoginCustomizer.java b/backend/src/main/java/it/chalmers/gamma/security/LoginCustomizer.java new file mode 100644 index 000000000..11b76bcf3 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/LoginCustomizer.java @@ -0,0 +1,16 @@ +package it.chalmers.gamma.security; + +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; + +public class LoginCustomizer implements Customizer> { + + @Override + public void customize(FormLoginConfigurer login) { + login + .loginPage("/login") + .usernameParameter("username") + .passwordParameter("password"); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/LogoutCustomizer.java b/backend/src/main/java/it/chalmers/gamma/security/LogoutCustomizer.java new file mode 100644 index 000000000..c1505a6d9 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/LogoutCustomizer.java @@ -0,0 +1,18 @@ +package it.chalmers.gamma.security; + +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +public class LogoutCustomizer implements Customizer> { + + @Override + public void customize(LogoutConfigurer logout) { + logout + .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) + .logoutSuccessUrl("/login") + .deleteCookies("gamma"); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/MvcConfig.java b/backend/src/main/java/it/chalmers/gamma/security/MvcConfig.java new file mode 100644 index 000000000..2a942062e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/MvcConfig.java @@ -0,0 +1,25 @@ +package it.chalmers.gamma.security; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class MvcConfig implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/css/**") + .addResourceLocations("classpath:/static/css/"); + + registry.addResourceHandler("/js/**") + .addResourceLocations("classpath:/static/js/"); + + registry.addResourceHandler("/img/**") + .addResourceLocations("classpath:/static/img/"); + + registry.addResourceHandler("/webjars/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/"); + } + +} \ No newline at end of file diff --git a/backend/src/main/java/it/chalmers/gamma/security/RedisConfig.java b/backend/src/main/java/it/chalmers/gamma/security/RedisConfig.java new file mode 100644 index 000000000..2f9fed9be --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/RedisConfig.java @@ -0,0 +1,19 @@ +package it.chalmers.gamma.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + return template; + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/SecurityFiltersConfig.java b/backend/src/main/java/it/chalmers/gamma/security/SecurityFiltersConfig.java new file mode 100644 index 000000000..e25482649 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/SecurityFiltersConfig.java @@ -0,0 +1,129 @@ +package it.chalmers.gamma.security; + +import it.chalmers.gamma.adapter.secondary.jpa.user.TrustedUserDetailsRepository; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserJpaRepository; +import it.chalmers.gamma.app.admin.domain.AdminRepository; +import it.chalmers.gamma.app.apikey.domain.ApiKeyRepository; +import it.chalmers.gamma.app.client.domain.ClientRepository; +import it.chalmers.gamma.app.settings.domain.SettingsRepository; +import it.chalmers.gamma.security.api.ApiAuthenticationFilter; +import it.chalmers.gamma.security.api.ApiAuthenticationProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RegexRequestMatcher; + +import java.util.stream.Stream; + +@Configuration +public class SecurityFiltersConfig { + + /** + * Sets up the security for the api that is used by the frontend. + */ + @Bean + SecurityFilterChain internalSecurityFilterChain(HttpSecurity http, + CsrfTokenRepository csrfTokenRepository, + GammaRequestCache requestCache, + PasswordEncoder passwordEncoder, + UserJpaRepository userJpaRepository, + SettingsRepository settingsRepository, + AdminRepository adminRepository) throws Exception { + + TrustedUserDetailsRepository trustedUserDetails = new TrustedUserDetailsRepository( + userJpaRepository, + settingsRepository + ); + + DaoAuthenticationProvider userAuthenticationProvider = new DaoAuthenticationProvider(); + userAuthenticationProvider.setUserDetailsService(trustedUserDetails); + userAuthenticationProvider.setPasswordEncoder(passwordEncoder); + + RegexRequestMatcher internalRequestMatcher = new RegexRequestMatcher("/internal.+", null); + RegexRequestMatcher loginRequestMatcher = new RegexRequestMatcher("/login.*", null); + RegexRequestMatcher logoutRequestMatcher = new RegexRequestMatcher("/logout", null); + + + AntPathRequestMatcher[] permittedRequests = Stream.of( + "/login", + "/internal/users/create", + "/internal/allow-list/activate-cid", + "/internal/users/reset_password", + "/internal/users/reset_password/finish" + ).map(AntPathRequestMatcher::antMatcher).toArray(AntPathRequestMatcher[]::new); + + http + .securityMatchers(matcher -> matcher.requestMatchers(internalRequestMatcher, loginRequestMatcher, logoutRequestMatcher)) + .addFilterAfter(new UpdateUserPrincipalFilter(trustedUserDetails, adminRepository), UsernamePasswordAuthenticationFilter.class) + .authorizeHttpRequests(authorization -> + authorization + .requestMatchers(new OrRequestMatcher(permittedRequests)).hasRole("ANONYMOUS") + .anyRequest().authenticated() + ) + .httpBasic().disable() + .formLogin(new LoginCustomizer()) + .logout(new LogoutCustomizer()) + .authenticationProvider(userAuthenticationProvider) + .sessionManagement(sessionManagement -> sessionManagement + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + ) + .csrf(csrf -> csrf + .csrfTokenRepository(csrfTokenRepository) + .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) + ) + .cors(Customizer.withDefaults()) + .exceptionHandling() + .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)); + return http.build(); + } + + @Bean + SecurityFilterChain externalSecurityFilterChain(HttpSecurity http, + ApiKeyRepository apiKeyRepository, + ClientRepository clientRepository) + throws Exception { + + ApiAuthenticationProvider apiAuthenticationProvider = new ApiAuthenticationProvider(apiKeyRepository, clientRepository); + + RegexRequestMatcher regexRequestMatcher = new RegexRequestMatcher("\\/external.+", null); + http + .securityMatcher(regexRequestMatcher) + .addFilterBefore(new ApiAuthenticationFilter(new ProviderManager(apiAuthenticationProvider)), BasicAuthenticationFilter.class) + .authorizeHttpRequests(authorization -> authorization.anyRequest().authenticated()) + .sessionManagement(sessionManagement -> sessionManagement + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + //Since only backends will call the /external + .csrf(csrf -> csrf.disable()); + return http.build(); + } + + @Bean + SecurityFilterChain imagesSecurityFilterChain(HttpSecurity http) throws Exception { + http + .securityMatcher("\\/images.+") + .authorizeHttpRequests(authorization -> + authorization.anyRequest().permitAll() + ) + .sessionManagement(sessionManagement -> sessionManagement + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .csrf(csrf -> csrf.disable()); + return http.build(); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/UpdateUserPrincipalFilter.java b/backend/src/main/java/it/chalmers/gamma/security/UpdateUserPrincipalFilter.java new file mode 100644 index 000000000..9c66c4326 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/UpdateUserPrincipalFilter.java @@ -0,0 +1,51 @@ +package it.chalmers.gamma.security; + +import it.chalmers.gamma.adapter.secondary.jpa.user.TrustedUserDetailsRepository; +import it.chalmers.gamma.app.admin.domain.AdminRepository; +import it.chalmers.gamma.app.user.domain.GammaUser; +import it.chalmers.gamma.security.authentication.UserAuthentication; +import jakarta.servlet.*; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.io.IOException; + +public class UpdateUserPrincipalFilter implements Filter { + + private final TrustedUserDetailsRepository userDetailsRepository; + private final AdminRepository adminRepository; + + public UpdateUserPrincipalFilter(TrustedUserDetailsRepository userDetailsRepository, AdminRepository adminRepository) { + this.userDetailsRepository = userDetailsRepository; + this.adminRepository = adminRepository; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + if (SecurityContextHolder.getContext().getAuthentication() instanceof UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) { + if (!usernamePasswordAuthenticationToken.isAuthenticated()) { + return; + } + + final GammaUser gammaUser = userDetailsRepository.getGammaUserByUser(); + final boolean isAdmin = adminRepository.isAdmin(gammaUser.id()); + + UserAuthentication userPrincipal = new UserAuthentication() { + @Override + public GammaUser get() { + return gammaUser; + } + + @Override + public boolean isAdmin() { + return isAdmin; + } + }; + + + usernamePasswordAuthenticationToken.setDetails(userPrincipal); + } + + chain.doFilter(request, response); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/api/ApiAuthenticationConverter.java b/backend/src/main/java/it/chalmers/gamma/security/api/ApiAuthenticationConverter.java new file mode 100644 index 000000000..3f764ed50 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/api/ApiAuthenticationConverter.java @@ -0,0 +1,31 @@ +package it.chalmers.gamma.security.api; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationConverter; + +import java.util.Optional; + +public class ApiAuthenticationConverter implements AuthenticationConverter { + + @Override + public Authentication convert(HttpServletRequest request) { + Optional maybeApiKeyToken = resolveToken(request); + return maybeApiKeyToken.map(ApiAuthenticationToken::fromApiKeyToken).orElse(null); + } + + private Optional resolveToken(HttpServletRequest req) { + String basicToken = req.getHeader("Authorization"); + if (basicToken != null && basicToken.startsWith("pre-shared ")) { + basicToken = removePreShared(basicToken); + } else { + basicToken = null; + } + return Optional.ofNullable(basicToken); + } + + private String removePreShared(String token) { + return token.substring(11); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/api/ApiAuthenticationFilter.java b/backend/src/main/java/it/chalmers/gamma/security/api/ApiAuthenticationFilter.java new file mode 100644 index 000000000..fede88654 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/api/ApiAuthenticationFilter.java @@ -0,0 +1,16 @@ +package it.chalmers.gamma.security.api; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.web.authentication.AuthenticationFilter; + +public class ApiAuthenticationFilter extends AuthenticationFilter { + + public ApiAuthenticationFilter(AuthenticationManager authenticationManager) { + super(authenticationManager, new ApiAuthenticationConverter()); + + // Do nothing on successful authentication since we need to authenticate on every request + setSuccessHandler((request, response, authentication) -> { + }); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/api/ApiAuthenticationProvider.java b/backend/src/main/java/it/chalmers/gamma/security/api/ApiAuthenticationProvider.java new file mode 100644 index 000000000..0e0d7f924 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/api/ApiAuthenticationProvider.java @@ -0,0 +1,56 @@ +package it.chalmers.gamma.security.api; + +import it.chalmers.gamma.app.apikey.domain.ApiKey; +import it.chalmers.gamma.app.apikey.domain.ApiKeyRepository; +import it.chalmers.gamma.app.apikey.domain.ApiKeyToken; +import it.chalmers.gamma.app.client.domain.Client; +import it.chalmers.gamma.app.client.domain.ClientRepository; +import it.chalmers.gamma.security.authentication.ApiAuthentication; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +import java.util.Optional; + +public class ApiAuthenticationProvider implements AuthenticationProvider { + + private final ApiKeyRepository apiKeyRepository; + private final ClientRepository clientRepository; + + public ApiAuthenticationProvider(ApiKeyRepository apiKeyRepository, + ClientRepository clientRepository) { + this.apiKeyRepository = apiKeyRepository; + this.clientRepository = clientRepository; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + ApiAuthenticationToken apiAuthenticationToken = (ApiAuthenticationToken) authentication; + final ApiKey apiKey = this.apiKeyRepository.getByToken(new ApiKeyToken((String) apiAuthenticationToken.getCredentials())) + .orElseThrow(ApiAuthenticationException::new); + final Optional maybeClient = this.clientRepository.getByApiKey(apiKey.apiKeyToken()); + + return ApiAuthenticationToken.fromAuthenticatedApiKey(new ApiAuthentication() { + @Override + public ApiKey get() { + return apiKey; + } + + @Override + public Optional getClient() { + return maybeClient; + } + }); + } + + @Override + public boolean supports(Class authentication) { + return ApiAuthenticationToken.class.isAssignableFrom(authentication); + } + + public static class ApiAuthenticationException extends AuthenticationException { + public ApiAuthenticationException() { + super("Exception when trying to authenticate api key"); + } + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/api/ApiAuthenticationToken.java b/backend/src/main/java/it/chalmers/gamma/security/api/ApiAuthenticationToken.java new file mode 100644 index 000000000..b114430f3 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/api/ApiAuthenticationToken.java @@ -0,0 +1,51 @@ +package it.chalmers.gamma.security.api; + +import it.chalmers.gamma.security.authentication.ApiAuthentication; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Transient; + +import static org.springframework.security.core.authority.AuthorityUtils.NO_AUTHORITIES; + +@Transient +public class ApiAuthenticationToken extends AbstractAuthenticationToken { + + private final String apiKeyToken; + private final ApiAuthentication apiPrincipal; + + private ApiAuthenticationToken(ApiAuthentication apiPrincipal) { + super(NO_AUTHORITIES); + this.apiKeyToken = null; + this.apiPrincipal = apiPrincipal; + super.setAuthenticated(true); + } + + private ApiAuthenticationToken(String apiKeyToken) { + super(NO_AUTHORITIES); + this.apiKeyToken = apiKeyToken; + this.apiPrincipal = null; + super.setAuthenticated(false); + } + + public static ApiAuthenticationToken fromApiKeyToken(String apiKeyToken) { + return new ApiAuthenticationToken(apiKeyToken); + } + + public static ApiAuthenticationToken fromAuthenticatedApiKey(ApiAuthentication apiPrincipal) { + return new ApiAuthenticationToken(apiPrincipal); + } + + @Override + public void setAuthenticated(boolean authenticated) { + throw new IllegalCallerException("You cannot call set authenticated on an already created ApiAuthenticationToken"); + } + + @Override + public Object getCredentials() { + return apiKeyToken; + } + + @Override + public ApiAuthentication getPrincipal() { + return this.apiPrincipal; + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/authentication/ApiAuthentication.java b/backend/src/main/java/it/chalmers/gamma/security/authentication/ApiAuthentication.java new file mode 100644 index 000000000..eb7c1c296 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/authentication/ApiAuthentication.java @@ -0,0 +1,15 @@ +package it.chalmers.gamma.security.authentication; + +import it.chalmers.gamma.app.apikey.domain.ApiKey; +import it.chalmers.gamma.app.client.domain.Client; + +import java.util.Optional; + +public non-sealed interface ApiAuthentication extends GammaAuthentication { + ApiKey get(); + + /** + * Api key might be connected to a client. + */ + Optional getClient(); +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/authentication/AuthenticationExtractor.java b/backend/src/main/java/it/chalmers/gamma/security/authentication/AuthenticationExtractor.java new file mode 100644 index 000000000..195960cc1 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/authentication/AuthenticationExtractor.java @@ -0,0 +1,27 @@ +package it.chalmers.gamma.security.authentication; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +public final class AuthenticationExtractor { + + private AuthenticationExtractor() { + } + + public static GammaAuthentication getAuthentication() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null) { + return null; + } + + if (authentication.getPrincipal() instanceof GammaAuthentication gammaAuthentication) { + return gammaAuthentication; + } else if (authentication.getDetails() instanceof GammaAuthentication gammaAuthentication) { + return gammaAuthentication; + } else { + return null; + } + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/authentication/GammaAuthentication.java b/backend/src/main/java/it/chalmers/gamma/security/authentication/GammaAuthentication.java new file mode 100644 index 000000000..19a0c0762 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/authentication/GammaAuthentication.java @@ -0,0 +1,7 @@ +package it.chalmers.gamma.security.authentication; + +/** + * These are the different + */ +public sealed interface GammaAuthentication permits ApiAuthentication, UserAuthentication, LocalRunnerAuthentication { +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/authentication/LocalRunnerAuthentication.java b/backend/src/main/java/it/chalmers/gamma/security/authentication/LocalRunnerAuthentication.java new file mode 100644 index 000000000..61cb559a6 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/authentication/LocalRunnerAuthentication.java @@ -0,0 +1,4 @@ +package it.chalmers.gamma.security.authentication; + +public non-sealed interface LocalRunnerAuthentication extends GammaAuthentication { +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/authentication/UserAuthentication.java b/backend/src/main/java/it/chalmers/gamma/security/authentication/UserAuthentication.java new file mode 100644 index 000000000..64ae08f92 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/authentication/UserAuthentication.java @@ -0,0 +1,10 @@ +package it.chalmers.gamma.security.authentication; + +import it.chalmers.gamma.app.user.domain.GammaUser; + +public non-sealed interface UserAuthentication extends GammaAuthentication { + + GammaUser get(); + boolean isAdmin(); + +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/user/PasswordConfiguration.java b/backend/src/main/java/it/chalmers/gamma/security/user/PasswordConfiguration.java new file mode 100644 index 000000000..e8e0c6a6e --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/user/PasswordConfiguration.java @@ -0,0 +1,14 @@ +package it.chalmers.gamma.security.user; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordConfiguration { + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/security/user/UserPasswordRetriever.java b/backend/src/main/java/it/chalmers/gamma/security/user/UserPasswordRetriever.java new file mode 100644 index 000000000..648074830 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/security/user/UserPasswordRetriever.java @@ -0,0 +1,15 @@ +package it.chalmers.gamma.security.user; + +import it.chalmers.gamma.app.user.domain.Password; +import it.chalmers.gamma.app.user.domain.UserId; +import org.springframework.lang.Nullable; + +//TODO: Add ArchUnit that only UserConfig can use this. +public interface UserPasswordRetriever { + @Nullable + Password getPassword(UserId id); + + class UserNotFoundException extends RuntimeException { + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/util/ClassNameGeneratorUtils.java b/backend/src/main/java/it/chalmers/gamma/util/ClassNameGeneratorUtils.java new file mode 100644 index 000000000..ccb175b4f --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/util/ClassNameGeneratorUtils.java @@ -0,0 +1,24 @@ +package it.chalmers.gamma.util; + +public class ClassNameGeneratorUtils { + + private ClassNameGeneratorUtils() { + } + + public static String classToScreamingSnakeCase(Class c) { + String className = c.getSimpleName(); + String[] camelCaseWords = className.split("(?=[A-Z])"); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < camelCaseWords.length - 1; i++) { + sb.append(camelCaseWords[i].toUpperCase()); + sb.append("_"); + } + + sb.append(camelCaseWords[camelCaseWords.length - 1].toUpperCase()); + + return sb.toString(); + + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/util/Serializer.java b/backend/src/main/java/it/chalmers/gamma/util/Serializer.java new file mode 100644 index 000000000..82ec1a2dc --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/util/Serializer.java @@ -0,0 +1,27 @@ +package it.chalmers.gamma.util; + +import java.io.*; +import java.util.Base64; + +public final class Serializer { + + private Serializer() { + } + + public static Object fromString(String s) throws IOException, ClassNotFoundException { + byte[] data = Base64.getDecoder().decode(s); + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data)); + Object o = ois.readObject(); + ois.close(); + return o; + } + + public static String toString(Serializable o) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(o); + oos.close(); + return Base64.getEncoder().encodeToString(baos.toByteArray()); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/util/TokenUtils.java b/backend/src/main/java/it/chalmers/gamma/util/TokenUtils.java new file mode 100644 index 000000000..f0ccc0b15 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/util/TokenUtils.java @@ -0,0 +1,41 @@ +package it.chalmers.gamma.util; + +import java.util.Arrays; +import java.util.Random; +import java.util.stream.Collectors; + +public final class TokenUtils { + + private TokenUtils() { + } + + public static String generateToken(int length, CharacterTypes... types) { + String characters = Arrays.stream(types) + .map(CharacterTypes::getCharacters) + .collect(Collectors.joining()); + Random rand = new Random(); + StringBuilder code = new StringBuilder(); + for (int i = 0; i < length; i++) { + code.append(characters.charAt(rand.nextInt(characters.length() - 1))); + } + return code.toString(); + } + + public enum CharacterTypes { + UPPERCASE("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), + LOWERCASE("abcdefghijklmnopqrstuvwxyz"), + NUMBERS("123456789"); + + private final String characters; + + CharacterTypes(String characters) { + this.characters = characters; + } + + public String getCharacters() { + return this.characters; + } + + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/util/controller/ErrorHandlingControllerAdvice.java b/backend/src/main/java/it/chalmers/gamma/util/controller/ErrorHandlingControllerAdvice.java new file mode 100644 index 000000000..738e01562 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/util/controller/ErrorHandlingControllerAdvice.java @@ -0,0 +1,46 @@ +package it.chalmers.gamma.util.controller; + +import it.chalmers.gamma.app.authentication.AccessGuard; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.NoHandlerFoundException; + +@RestControllerAdvice +public class ErrorHandlingControllerAdvice { + + @ExceptionHandler({ + AccessGuard.AccessDeniedException.class, + AccessDeniedException.class + }) + @ResponseStatus(HttpStatus.FORBIDDEN) + @ResponseBody + public Error accessDenied() { + return new Error( + "ACCESS_DENIED", + "ACCESS_DENIED" + ); + } + + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public Error onIllegalArgumentException(IllegalArgumentException e) { + return new Error(e.getStackTrace()[0].getClassName(), e.getMessage()); + } + + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public Error asdf() { + return new Error("asdf", "fdsa"); + } + + public record Error(String origin, String message) { + } + + +} diff --git a/backend/src/main/java/it/chalmers/gamma/util/response/AlreadyExistsResponse.java b/backend/src/main/java/it/chalmers/gamma/util/response/AlreadyExistsResponse.java new file mode 100644 index 000000000..246b91dfd --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/util/response/AlreadyExistsResponse.java @@ -0,0 +1,9 @@ +package it.chalmers.gamma.util.response; + +import org.springframework.http.HttpStatus; + +public abstract class AlreadyExistsResponse extends ErrorResponse { + public AlreadyExistsResponse() { + super(HttpStatus.CONFLICT); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/util/response/BadRequestResponse.java b/backend/src/main/java/it/chalmers/gamma/util/response/BadRequestResponse.java new file mode 100644 index 000000000..01141f064 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/util/response/BadRequestResponse.java @@ -0,0 +1,9 @@ +package it.chalmers.gamma.util.response; + +import org.springframework.http.HttpStatus; + +public class BadRequestResponse extends ErrorResponse { + public BadRequestResponse() { + super(HttpStatus.BAD_REQUEST); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/util/response/ErrorResponse.java b/backend/src/main/java/it/chalmers/gamma/util/response/ErrorResponse.java new file mode 100644 index 000000000..44dbbb4f5 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/util/response/ErrorResponse.java @@ -0,0 +1,37 @@ +package it.chalmers.gamma.util.response; + +import it.chalmers.gamma.util.ClassNameGeneratorUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public class ErrorResponse extends ResponseStatusException { + + private static final Logger LOGGER = LoggerFactory.getLogger(ErrorResponse.class); + + public ErrorResponse(HttpStatus status) { + super(status); + + LOGGER.error(String.format( + "An exception was thrown in the application: status: %d, Reason: %s", + status.value(), + ClassNameGeneratorUtils.classToScreamingSnakeCase(this.getClass()))); + LOGGER.debug(String.format("Stacktrace: \n %s:", Arrays.stream(super.fillInStackTrace().getStackTrace()) + .map(StackTraceElement::toString).collect(Collectors.joining("\n ")))); + } + + @Override + public synchronized Throwable fillInStackTrace() { + return null; + } + + @Override + public String getReason() { + return ClassNameGeneratorUtils.classToScreamingSnakeCase(this.getClass()); + } + +} diff --git a/backend/src/main/java/it/chalmers/gamma/util/response/NotFoundResponse.java b/backend/src/main/java/it/chalmers/gamma/util/response/NotFoundResponse.java new file mode 100644 index 000000000..adbfd737a --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/util/response/NotFoundResponse.java @@ -0,0 +1,9 @@ +package it.chalmers.gamma.util.response; + +import org.springframework.http.HttpStatus; + +public abstract class NotFoundResponse extends ErrorResponse { + public NotFoundResponse() { + super(HttpStatus.NOT_FOUND); + } +} diff --git a/backend/src/main/java/it/chalmers/gamma/util/response/SuccessResponse.java b/backend/src/main/java/it/chalmers/gamma/util/response/SuccessResponse.java new file mode 100644 index 000000000..0a19edb94 --- /dev/null +++ b/backend/src/main/java/it/chalmers/gamma/util/response/SuccessResponse.java @@ -0,0 +1,27 @@ +package it.chalmers.gamma.util.response; + +import it.chalmers.gamma.util.ClassNameGeneratorUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class SuccessResponse extends ResponseEntity { + + public SuccessResponse() { + super(HttpStatus.OK); + } + + public SuccessResponse(HttpStatus status) { + super(status); + } + + @Override + public SuccessResponseData getBody() { + return new SuccessResponseData( + ClassNameGeneratorUtils.classToScreamingSnakeCase(this.getClass()), + this.getStatusCode().value() + ); + } + + public record SuccessResponseData(String name, int code) { } + +} diff --git a/backend/src/main/resources/application-production.yml b/backend/src/main/resources/application-production.yml new file mode 100644 index 000000000..431b196a0 --- /dev/null +++ b/backend/src/main/resources/application-production.yml @@ -0,0 +1,78 @@ +spring: + datasource: + username: ${DB_USER:user} + password: ${DB_PASSWORD:password} + url: jdbc:postgresql://${DB_HOST:db}:${DB_PORT:5432}/${DB_NAME:postgres} + flyway: + baseline-on-migrate: true + locations: + classpath:/db/migration + output: + ansi: + enabled: ALWAYS + jpa: + database-platform: org.hibernate.dialect.PostgreSQL9Dialect + properties: + hibernate: + globally_quoted_identifiers: true + temp: + use_jdbc_metadata_defaults: false + dialect: org.hibernate.dialect.PostgreSQL9Dialect + hibernate: + ddl-auto: validate + open-in-view: false + servlet: + multipart: + max-file-size: 2MB + max-request-size: 2MB + data: + redis: + host: ${REDIS_HOST:0.0.0.0} + password: ${REDIS_PASSWORD:} + port: ${REDIS_PORT:6379} + thymeleaf: + cache: false + check-template: false + check-template-location: false + enabled: true + encoding: UTF-8 + mode: HTML + prefix: classpath:/templates/ + suffix: .html + servlet: + content-type: text/html + session: + store-type: redis + +server: + port: ${SERVER_PORT:9090} + error: + whitelabel: + enabled: false + path: /error + servlet: + context-path: /api + session: + timeout: ${SESSION_TIMEOUT:43200} + +logging: + file: + name: ${LOGGING_FILE:production.log} + level: + root: ${ROOT_DEBUG_LEVEL:WARN} + +application: + base-uri: ${BACKEND_URI:http://gamma:9090} + production: ${PRODUCTION:true} + cookie: + domain: ${COOKIE_DOMAIN:https://gamma.chalmers.it} + path: ${COOKIE_PATH:/} + validity-time: 31536000 # One year + frontend-client-details: + successful-login-uri: ${SUCCESSFUL_LOGIN:http://gamma:8080} + mocking: ${IS_MOCKING:false} + gotify: + key: ${GOTIFY_KEY:""} + url: ${GOTIFY_URL:""} + allowed-origin: ${CORS_ALLOWED_ORIGIN:https://gamma.chalmers.it} + diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 000000000..bbb3f0804 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,82 @@ +spring: + datasource: + username: ${DB_USER:postgres} + password: ${DB_PASSWORD:example} + url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:postgres} + flyway: + baseline-on-migrate: true + locations: classpath:/db/migration + output: + ansi: + enabled: ALWAYS + jpa: + database-platform: org.hibernate.dialect.PostgreSQL9Dialect + properties: + hibernate: + globally_quoted_identifiers: true + temp: + use_jdbc_metadata_defaults: false + dialect: org.hibernate.dialect.PostgreSQL9Dialect + hibernate: + ddl-auto: validate + open-in-view: false + servlet: + multipart: + max-file-size: 2MB + max-request-size: 2MB + data: + redis: + host: ${REDIS_HOST:0.0.0.0} + password: ${REDIS_PASSWORD:} + port: ${REDIS_PORT:6379} + repositories: + enabled: true + thymeleaf: + cache: false + check-template: true + check-template-location: true + enabled: true + encoding: UTF-8 + mode: HTML + prefix: classpath:/templates/ + suffix: .html + servlet: + content-type: text/html + +server: + port: ${SERVER_PORT:8081} + error: + whitelabel: + enabled: true + path: /error + servlet: + context-path: /api + +logging: + level: + root: ${ROOT_DEBUG_LEVEL:INFO} + +application: + base-uri: http://gamma:8081 + production: false + files: + path: uploads/ + cookie: + domain: ${COOKIE_DOMAIN:gamma} + path: ${COOKIE_PATH:/} + validity-time: 31536000 + mocking: ${IS_MOCKING:true} + admin-setup: ${ADMIN_SETUP:true} + default-oauth2-client: + client-name: ${DEFAULT_CLIENT_NAME:test-client} + client-id: ${DEFAULT_CLIENT_ID:test} + client-secret: ${DEFAULT_CLIENT_SECRET:secret} + redirect-url: ${DEFAULT_REDIRECT_URL:http://client:3001/login/oauth2/code/gamma} + api-key: ${DEFAULT_API_KEY:test-api-key-secret-code} + scopes: ${DEFAULT_SCOPES:profile,email} + frontend-client-details: + successful-login-uri: ${SUCCESSFUL_LOGIN:http://gamma:3000} + gotify: + key: "123abc" # This is not needed, but application crashes since it looks for these values. + url: "localhost:7000" + allowed-origin: ${CORS_ALLOWED_ORIGIN:http://gamma:3000} diff --git a/backend/src/main/resources/db/migration/README.md b/backend/src/main/resources/db/migration/README.md new file mode 100644 index 000000000..b2922e18c --- /dev/null +++ b/backend/src/main/resources/db/migration/README.md @@ -0,0 +1,23 @@ +# How to write migrations + +Create a sql file with a V that is +1 of the last one. + +Here're some examples with an sql file named `V99__website-changes.sql`: + +```sql + +-- Add column +ALTER TABLE website + ADD test_column varchar(100) not null; + +-- Rename column +ALTER TABLE website + RENAME COLUMN name TO name_new; + +-- Modify column +ALTER TABLE website + ALTER COLUMN pretty_name TYPE varchar(200); + +``` + +More examples here, check for PostgresSQL: https://www.postgresql.org/docs/9.4/ddl-alter.html diff --git a/backend/src/main/resources/db/migration/V1__BASE.sql b/backend/src/main/resources/db/migration/V1__BASE.sql new file mode 100644 index 000000000..7a870aa3e --- /dev/null +++ b/backend/src/main/resources/db/migration/V1__BASE.sql @@ -0,0 +1,227 @@ +-- g_ = gamma prefix to prevent using reserved names + +CREATE TABLE g_text +( + text_id UUID PRIMARY KEY, + sv VARCHAR(2048) NOT NULL, + en VARCHAR(2048) NOT NULL +); + +CREATE TABLE g_user +( + user_id UUID PRIMARY KEY, + cid VARCHAR(12) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + nick VARCHAR(50) NOT NULL, + first_name VARCHAR(50) NOT NULL, + last_name VARCHAR(50) NOT NULL, + email VARCHAR(100) NOT NULL UNIQUE, + language VARCHAR(15) NULL, + user_agreement_accepted TIMESTAMP NOT NULL DEFAULT NOW(), + acceptance_year INTEGER, + version INT, + -- TODO: Maybe move to its own table? Along with a reason why it is locked + locked BOOLEAN DEFAULT FALSE +); + +CREATE TABLE g_user_avatar_uri +( + user_id UUID REFERENCES g_user ON DELETE CASCADE, + avatar_uri VARCHAR(255) NOT NULL, + version INT +); + +CREATE TABLE g_admin_user +( + user_id UUID PRIMARY KEY REFERENCES g_user(user_id) ON DELETE CASCADE +); + +CREATE TABLE g_gdpr_trained +( + user_id UUID PRIMARY KEY REFERENCES g_user(user_id) ON DELETE CASCADE +); + +CREATE TABLE g_password_reset +( + token VARCHAR(100) UNIQUE, + user_id UUID PRIMARY KEY REFERENCES g_user ON DELETE CASCADE, + created_at TIMESTAMP NOT NULL DEFAULT current_timestamp +); + +CREATE TABLE g_super_group_type +( + super_group_type_name VARCHAR(30) PRIMARY KEY +); + +CREATE TABLE g_super_group +( + super_group_id UUID PRIMARY KEY, + e_name VARCHAR(50) NOT NULL UNIQUE, + pretty_name VARCHAR(50) NOT NULL, + super_group_type_name VARCHAR(30) NOT NULL REFERENCES g_super_group_type, + description UUID REFERENCES g_text ON DELETE CASCADE, + version INT +); + +CREATE TABLE g_group +( + group_id UUID PRIMARY KEY, + e_name VARCHAR(50) NOT NULL UNIQUE, + pretty_name VARCHAR(50) NOT NULL, + super_group_id UUID NOT NULL REFERENCES g_super_group, + version INT +); + +CREATE TABLE g_post +( + post_id UUID PRIMARY KEY, + post_name UUID NOT NULL REFERENCES g_text ON DELETE CASCADE, + email_prefix VARCHAR(20), + version INT +); + +CREATE TABLE g_membership +( + user_id UUID REFERENCES g_user ON DELETE CASCADE, + group_id UUID REFERENCES g_group ON DELETE CASCADE, + post_id UUID REFERENCES g_post ON DELETE CASCADE, + unofficial_post_name VARCHAR(100), + PRIMARY KEY (user_id, group_id, post_id) +); + +CREATE TABLE g_allowlist +( + cid VARCHAR(10) PRIMARY KEY CHECK (LOWER(cid) = cid) +); + +CREATE TABLE g_user_activation +( + cid VARCHAR(10) PRIMARY KEY REFERENCES g_allowlist, + token VARCHAR(10) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT current_timestamp +); + +CREATE TABLE g_client +( + client_uid UUID PRIMARY KEY, + client_id VARCHAR(100) UNIQUE, + client_secret VARCHAR(100) NOT NULL, + redirect_uri VARCHAR(256) NOT NULL, + pretty_name VARCHAR(30) NOT NULL, + description UUID REFERENCES g_text ON DELETE CASCADE +); + +CREATE TABLE g_client_owner +( + user_id UUID REFERENCES g_user(user_id), + client_uid UUID REFERENCES g_client(client_uid), + PRIMARY KEY (user_id, client_uid) +); + +CREATE TABLE g_client_scope +( + client_uid UUID REFERENCES g_client, + scope VARCHAR(30) NOT NULL, + PRIMARY KEY (client_uid, scope) +); + +CREATE TABLE g_apikey +( + api_key_id UUID PRIMARY KEY, + pretty_name VARCHAR(30) NOT NULL, + description UUID REFERENCES g_text ON DELETE CASCADE, + token VARCHAR(150) UNIQUE, + key_type VARCHAR(30) NOT NULL, + version INT +); + +CREATE TABLE g_client_apikey +( + client_uid UUID PRIMARY KEY REFERENCES g_client ON DELETE CASCADE, + api_key_id UUID REFERENCES g_apikey ON DELETE CASCADE +); + +CREATE TABLE g_user_approval +( + user_id UUID REFERENCES g_user ON DELETE CASCADE, + client_uid UUID REFERENCES g_client ON DELETE CASCADE, + PRIMARY KEY (user_id, client_uid) +); + +CREATE TABLE g_group_images_uri +( + group_id UUID REFERENCES g_group ON DELETE CASCADE, + avatar_uri VARCHAR(255), + banner_uri VARCHAR(255), + version INT +); + +CREATE TABLE g_settings +( + id UUID PRIMARY KEY, + updated_at TIMESTAMP NOT NULL, + last_updated_user_agreement TIMESTAMP, + version INT +); + + +CREATE TABLE g_settings_info_api_super_group_types +( + settings_id UUID REFERENCES g_settings, + super_group_type_name VARCHAR(30) REFERENCES g_super_group_type, + PRIMARY KEY (settings_id, super_group_type_name) +); + +CREATE TABLE g_client_authority +( + client_uid UUID REFERENCES g_client(client_uid) ON DELETE CASCADE, + authority_name VARCHAR(30), + PRIMARY KEY (client_uid, authority_name) +); + +CREATE TABLE g_client_authority_post +( + super_group_id UUID REFERENCES g_super_group, + post_id UUID REFERENCES g_post, + client_uid UUID, + authority_name VARCHAR(30), + PRIMARY KEY (post_id, super_group_id, client_uid, authority_name), + FOREIGN KEY (client_uid, authority_name) + REFERENCES g_client_authority(client_uid, authority_name) + ON DELETE CASCADE +); + +CREATE TABLE g_client_authority_super_group +( + super_group_id UUID REFERENCES g_super_group, + client_uid UUID, + authority_name VARCHAR(30), + PRIMARY KEY (super_group_id, client_uid, authority_name), + FOREIGN KEY (client_uid, authority_name) + REFERENCES g_client_authority(client_uid, authority_name) + ON DELETE CASCADE +); + +CREATE TABLE g_client_authority_user +( + user_id UUID REFERENCES g_user ON DELETE CASCADE, + client_uid UUID, + authority_name VARCHAR(30), + PRIMARY KEY (user_id, client_uid, authority_name), + FOREIGN KEY (client_uid, authority_name) + REFERENCES g_client_authority(client_uid, authority_name) + ON DELETE CASCADE +); + +CREATE TABLE g_client_restriction ( + restriction_id UUID, + client_uid UUID REFERENCES g_client ON DELETE CASCADE, + PRIMARY KEY (client_uid) +); + +CREATE TABLE g_client_restriction_super_group +( + super_group_id UUID REFERENCES g_super_group ON DELETE CASCADE, + restriction_id UUID REFERENCES g_client_restriction ON DELETE CASCADE, + PRIMARY KEY (super_group_id, restriction_id) +); diff --git a/backend/src/main/resources/image/default_group_avatar.jpg b/backend/src/main/resources/image/default_group_avatar.jpg new file mode 100644 index 000000000..c5abc8af7 Binary files /dev/null and b/backend/src/main/resources/image/default_group_avatar.jpg differ diff --git a/backend/src/main/resources/image/default_group_banner.jpg b/backend/src/main/resources/image/default_group_banner.jpg new file mode 100644 index 000000000..6dd1e1993 Binary files /dev/null and b/backend/src/main/resources/image/default_group_banner.jpg differ diff --git a/backend/src/main/resources/image/default_user_avatar.jpg b/backend/src/main/resources/image/default_user_avatar.jpg new file mode 100644 index 000000000..b88da39a2 Binary files /dev/null and b/backend/src/main/resources/image/default_user_avatar.jpg differ diff --git a/backend/src/main/resources/mock/README.md b/backend/src/main/resources/mock/README.md new file mode 100644 index 000000000..8c0de7c11 --- /dev/null +++ b/backend/src/main/resources/mock/README.md @@ -0,0 +1,89 @@ +# Gamma Mocking + +Using mock.json, or providing your .json file is an easy way to create users, groups, posts, and supergroups for usage +when creating mock data in your application that uses gamma. By always providing the same ID:s, you can easily, for +example, create a booking with a given user id that will be the same for everyone who tries to develop on your +application and tries to use your mock data. + +Note that this file will only run if there's no admin user, i.e. the database is empty. + +The json document represents an object of `MockData.java`. `DbInitalizer.java` has the logic to actually insert the mock +data into the database. But here's a quick overview of the different props that can be used: + +## `groups` + +Creates `FKITGroup`:s. Each object in the array is represented in code by `MockFKITGroup.java`. The available props are: + +* `id`: UUID +* `name`: String +* `prettyName`: String +* `active`: boolean +* `superGroup`: Object +* `members`: Array +* `function`: Object | null +* `description`: Object | null + +###`active` + +If `active` is false, then the `becomesActive` date will be a year ago, and `becomesInactive` will be yesterday. +If `active` is true, then `becomesActive` will be yesterday and `becomesInactive` will be a year from now. + +### `members` + +An array of `MockMembership.java`. Props are: + +* `userId`: UUID +* `postId`: UUID +* `unofficialPostName`: String | null + +### `function` and `description` + +Are `Text.java` objects which means they take in an object that has the properties `sv` and `en`. The value for them are +strings. + +### Other notes + +`email` will be `name` + @chalmers.it. + +##`users` + +Creates `ITUser.java`. Each object in the array is represented in code by `MockITUser.java`. The available props are: + +* `id`: UUID +* `cid`: String +* `nick`: String +* `firstName`: String +* `lastName`: String +* `acceptanceYear`: Year + +`acceptanceYear` can be a value between 2001 - current year. + +## `posts` + +Create `Post.java`. Each object in the array is represented in code by `MockPost.java`. The available props are: + +* `id`: UUID +* `postName`: Object + +### `postName` + +As with `function` and `description` in groups, `postName` is represented by the `Text.java` class. + +## `superGroups` + +Creates `FKITSuperGroup.java`. Each object in the array is represented in code by `MockFKITSuperGroup.java`. The +available props are: + +* `id`: UUID +* `name`: String +* `prettyName`: String +* `type`: GroupType +* `groups`: Array + +### `type` + +An enum where the possible values are: `ADMIN`, `SOCIETY`, `COMMITTEE`, `BOARD`, `ALUMNI` and `FUNCTIONARIES`. + +### `groups` + +Only an array of UUID that represents a `FKITGroup`. \ No newline at end of file diff --git a/backend/src/main/resources/mock/mock.json b/backend/src/main/resources/mock/mock.json new file mode 100644 index 000000000..814bb82d8 --- /dev/null +++ b/backend/src/main/resources/mock/mock.json @@ -0,0 +1,415 @@ +{ + "users": [ + { + "id": "88eec5c2-5ebb-4e13-9a76-fcc4dac9e74f", + "firstName": "Wyatt", + "lastName": "MacMakin", + "cid": "wmacmak", + "nick": "Chokladkaka", + "acceptanceYear": 2014, + "admin": true + }, + { + "id": "bc605869-9a4d-46ec-8a29-d00819d4c195", + "firstName": "Mellie", + "lastName": "Juorio", + "cid": "mjuorio", + "nick": "Kladdkaka", + "acceptanceYear": 2016 + }, + { + "id": "ec8987d7-4087-461d-bed5-9365086b6e3b", + "firstName": "Lane", + "lastName": "Twell", + "cid": "tltwell", + "nick": "Mylta", + "acceptanceYear": 2012 + }, + { + "id": "0c67c90b-dfdf-473a-98e3-b551e2f2f0f1", + "firstName": "Hy", + "lastName": "Borg-Bartolo", + "cid": "hborgba", + "nick": "Strössel", + "acceptanceYear": 2017 + }, + { + "id": "858e5acc-c289-40d3-9422-d6d317f40299", + "firstName": "Sorcha", + "lastName": "Vanni", + "cid": "svanni", + "nick": "Våffla", + "acceptanceYear": 2016 + }, + { + "id": "9ad8946d-cfef-4f6f-8b48-cfb536d0c9eb", + "firstName": "Hobey", + "lastName": "Spaarritt", + "cid": "hsparritt", + "nick": "Fruktsallad", + "acceptanceYear": 2007 + }, + { + "id": "4efb340f-540c-4b15-a362-d402aab10195", + "firstName": "Wolfy", + "lastName": "Bulloch", + "cid": "wbulloch", + "nick": "Crème brûlée", + "acceptanceYear": 2018 + }, + { + "id": "08e4abb5-e4d6-413b-94f2-6e1aa63716e7", + "firstName": "Dolly", + "lastName": "Mathy", + "cid": "dmathy", + "nick": "Glassbomb", + "acceptanceYear": 2017 + }, + { + "id": "43af2838-43b9-4665-b3d7-9c615f5038fb", + "firstName": "Matelda", + "lastName": "Novotne", + "cid": "mnovotne", + "nick": "Chokladpudding", + "acceptanceYear": 2011 + }, + { + "id": "4542ab3d-7996-4097-ae4a-4fe61eaf2f20", + "firstName": "Farra", + "lastName": "Longshaw", + "cid": "flongshaw", + "nick": "Äppelmos", + "acceptanceYear": 2013 + }, + { + "id": "0a799f6d-c65a-4d20-8588-2ff5375d6cce", + "firstName": "Dorri", + "lastName": "Barneville", + "cid": "dbarnevi", + "nick": "Chokladtryffel", + "acceptanceYear": 2002 + }, + { + "id": "4fcf6566-45d8-4d5d-b7d4-4f6f52bb0ac2", + "firstName": "Alberik", + "lastName": "Nunson", + "cid": "anunson", + "nick": "O’hoj", + "acceptanceYear": 2004 + }, + { + "id": "e6a76e6a-3499-4611-ae28-e1281ffa6e80", + "firstName": "Joyce", + "lastName": "Hanhard", + "cid": "jhanhard", + "nick": "Marängsviss", + "acceptanceYear": 2016 + } + ], + "groups": [ + { + "id": "047ac437-a789-4cc5-bb6e-ba50efd7c509", + "name": "digit", + "prettyName": "digIT", + "superGroupId": "364a359a-f9eb-4d81-bb99-25cc5adf176d", + "members": [ + { + "userId": "bc605869-9a4d-46ec-8a29-d00819d4c195", + "postId": "7bb1db15-730d-4864-bfc3-99abe7c0ccf8", + "unofficialPostName": "root" + }, + { + "userId": "ec8987d7-4087-461d-bed5-9365086b6e3b", + "postId": "844067b3-e95d-4a28-a586-7388f155b8fb", + "unofficialPostName": "cache-chef" + }, + { + "userId": "0c67c90b-dfdf-473a-98e3-b551e2f2f0f1", + "postId": "08efcf3a-1805-4b5f-a60e-da6ce0d33f58", + "unofficialPostName": "dev-ooops" + } + ] + }, + { + "id": "2abe2264-fd61-4899-ba46-851279d85229", + "name": "digit", + "prettyName": "digIT", + "superGroupId": "aed27030-ad90-4526-855c-1e909b1dcecb", + "members": [ + { + "userId": "858e5acc-c289-40d3-9422-d6d317f40299", + "postId": "7bb1db15-730d-4864-bfc3-99abe7c0ccf8", + "unofficialPostName": "root" + }, + { + "userId": "9ad8946d-cfef-4f6f-8b48-cfb536d0c9eb", + "postId": "844067b3-e95d-4a28-a586-7388f155b8fb", + "unofficialPostName": "cache-chef" + }, + { + "userId": "4efb340f-540c-4b15-a362-d402aab10195", + "postId": "08efcf3a-1805-4b5f-a60e-da6ce0d33f58", + "unofficialPostName": "dev-ooops" + } + ] + }, + { + "id": "a2f06d3a-7432-4655-a778-69c9142912f1", + "name": "styrit", + "prettyName": "styrIT", + "superGroupId": "30c2ee3b-b761-46d0-9029-215a9b484f7a", + "members": [ + { + "userId": "9ad8946d-cfef-4f6f-8b48-cfb536d0c9eb", + "postId": "7bb1db15-730d-4864-bfc3-99abe7c0ccf8", + "unofficialPostName": "Ordf" + }, + { + "userId": "bc605869-9a4d-46ec-8a29-d00819d4c195", + "postId": "844067b3-e95d-4a28-a586-7388f155b8fb", + "unofficialPostName": "Kassör" + }, + { + "userId": "4542ab3d-7996-4097-ae4a-4fe61eaf2f20", + "postId": "08efcf3a-1805-4b5f-a60e-da6ce0d33f58", + "unofficialPostName": "IT-ansvarig" + }, + { + "userId": "0a799f6d-c65a-4d20-8588-2ff5375d6cce", + "postId": "524db9a7-e8be-403e-a07c-a41803ea5ee7", + "unofficialPostName": "VO" + } + ] + }, + { + "id": "834651d1-34c1-4bac-b148-6546368a8454", + "name": "styrit", + "prettyName": "styrIT", + "superGroupId": "2157ee72-04cd-4029-8d57-77142d3ef5fa", + "members": [ + { + "userId": "4fcf6566-45d8-4d5d-b7d4-4f6f52bb0ac2", + "postId": "7bb1db15-730d-4864-bfc3-99abe7c0ccf8", + "unofficialPostName": "Ordf" + }, + { + "userId": "bc605869-9a4d-46ec-8a29-d00819d4c195", + "postId": "844067b3-e95d-4a28-a586-7388f155b8fb", + "unofficialPostName": "Kassör" + }, + { + "userId": "ec8987d7-4087-461d-bed5-9365086b6e3b", + "postId": "08efcf3a-1805-4b5f-a60e-da6ce0d33f58", + "unofficialPostName": "IT-ansvarig" + }, + { + "userId": "0c67c90b-dfdf-473a-98e3-b551e2f2f0f1", + "postId": "524db9a7-e8be-403e-a07c-a41803ea5ee7", + "unofficialPostName": "VO" + } + ] + }, + { + "id": "672db849-8afb-4160-9f12-7f8c1d379fcc", + "name": "drawit", + "prettyName": "DrawIT", + "superGroupId": "5a427d4d-adb7-4de7-9c87-a569014c7b58", + "members": [ + { + "userId": "858e5acc-c289-40d3-9422-d6d317f40299", + "postId": "7bb1db15-730d-4864-bfc3-99abe7c0ccf8", + "unofficialPostName": "Ordf" + }, + { + "userId": "9ad8946d-cfef-4f6f-8b48-cfb536d0c9eb", + "postId": "844067b3-e95d-4a28-a586-7388f155b8fb", + "unofficialPostName": "Kassör" + }, + { + "userId": "4efb340f-540c-4b15-a362-d402aab10195", + "postId": "08efcf3a-1805-4b5f-a60e-da6ce0d33f58", + "unofficialPostName": "Knapansvarig" + } + ] + }, + { + "id": "9b239de9-88a3-4992-96d1-b8dea2a637ec", + "name": "drawit", + "prettyName": "DrawIT", + "superGroupId": "b8dbca3a-52e7-4299-9499-e58ec93a0c2c", + "members": [ + { + "userId": "4542ab3d-7996-4097-ae4a-4fe61eaf2f20", + "postId": "7bb1db15-730d-4864-bfc3-99abe7c0ccf8", + "unofficialPostName": "Ordf" + }, + { + "userId": "4542ab3d-7996-4097-ae4a-4fe61eaf2f20", + "postId": "844067b3-e95d-4a28-a586-7388f155b8fb", + "unofficialPostName": "Kassör" + }, + { + "userId": "4542ab3d-7996-4097-ae4a-4fe61eaf2f20", + "postId": "08efcf3a-1805-4b5f-a60e-da6ce0d33f58", + "unofficialPostName": "Knapansvarig" + } + ] + }, + { + "id": "5f26a10c-e668-4ec1-b072-a7dd8f11735c", + "name": "prit", + "prettyName": "P.R.I.T.", + "superGroupId": "326807b4-ae68-4626-8382-919a15a8e23c", + "members": [ + { + "userId": "0a799f6d-c65a-4d20-8588-2ff5375d6cce", + "postId": "7bb1db15-730d-4864-bfc3-99abe7c0ccf8", + "unofficialPostName": "ChefChef" + }, + { + "userId": "e6a76e6a-3499-4611-ae28-e1281ffa6e80", + "postId": "844067b3-e95d-4a28-a586-7388f155b8fb", + "unofficialPostName": "Ka$$Chef" + }, + { + "userId": "4542ab3d-7996-4097-ae4a-4fe61eaf2f20", + "postId": "08efcf3a-1805-4b5f-a60e-da6ce0d33f58", + "unofficialPostName": "BösChef" + } + ] + }, + { + "id": "1ed91274-13c8-4d6d-ab75-37c9d732b51b", + "name": "prit", + "prettyName": "P.R.I.T.", + "superGroupId": "b3bcbbcc-0b93-4c41-a3c7-1792448c6fc1", + "members": [ + { + "userId": "bc605869-9a4d-46ec-8a29-d00819d4c195", + "postId": "7bb1db15-730d-4864-bfc3-99abe7c0ccf8", + "unofficialPostName": "ChefChef" + }, + { + "userId": "ec8987d7-4087-461d-bed5-9365086b6e3b", + "postId": "844067b3-e95d-4a28-a586-7388f155b8fb", + "unofficialPostName": "Ka$$Chef" + }, + { + "userId": "0c67c90b-dfdf-473a-98e3-b551e2f2f0f1", + "postId": "08efcf3a-1805-4b5f-a60e-da6ce0d33f58", + "unofficialPostName": "BösChef" + } + ] + }, + { + "id": "ee4153d5-830d-445f-acb3-ec09c53e7c0c", + "name": "kandidatmiddagen", + "prettyName": "Kandidatmiddagen", + "superGroupId": "712e21f5-f3c6-49fc-a9e7-5b7ec3ff31ab", + "members": [ + { + "userId": "858e5acc-c289-40d3-9422-d6d317f40299", + "postId": "7bb1db15-730d-4864-bfc3-99abe7c0ccf8" + }, + { + "userId": "9ad8946d-cfef-4f6f-8b48-cfb536d0c9eb", + "postId": "08efcf3a-1805-4b5f-a60e-da6ce0d33f58" + }, + { + "userId": "4efb340f-540c-4b15-a362-d402aab10195", + "postId": "08efcf3a-1805-4b5f-a60e-da6ce0d33f58" + }, + { + "userId": "4542ab3d-7996-4097-ae4a-4fe61eaf2f20", + "postId": "08efcf3a-1805-4b5f-a60e-da6ce0d33f58" + } + ] + } + ], + "superGroups": [ + { + "id": "aed27030-ad90-4526-855c-1e909b1dcecb", + "name": "digit", + "prettyName": "digIT", + "type": "COMMITTEE" + }, + { + "id": "2157ee72-04cd-4029-8d57-77142d3ef5fa", + "name": "styrit", + "prettyName": "styrIT", + "type": "BOARD" + }, + { + "id": "b8dbca3a-52e7-4299-9499-e58ec93a0c2c", + "name": "drawit", + "prettyName": "DrawIT", + "type": "SOCIETY" + }, + { + "id": "b3bcbbcc-0b93-4c41-a3c7-1792448c6fc1", + "name": "prit", + "prettyName": "P.R.I.T.", + "type": "COMMITTEE" + }, + { + "id": "364a359a-f9eb-4d81-bb99-25cc5adf176d", + "name": "didit", + "prettyName": "didIT", + "type": "ALUMNI" + }, + { + "id": "30c2ee3b-b761-46d0-9029-215a9b484f7a", + "name": "emeritus", + "prettyName": "EmerITus", + "type": "ALUMNI" + }, + { + "id": "5a427d4d-adb7-4de7-9c87-a569014c7b58", + "name": "dragit", + "prettyName": "DragIT", + "type": "ALUMNI" + }, + { + "id": "326807b4-ae68-4626-8382-919a15a8e23c", + "name": "sprit", + "prettyName": "S.P.R.I.T.", + "type": "ALUMNI" + }, + { + "id": "712e21f5-f3c6-49fc-a9e7-5b7ec3ff31ab", + "name": "kandidatmiddagen", + "prettyName": "Kandidatmiddagen", + "type": "FUNCTIONARIES" + } + ], + "posts": [ + { + "id": "7bb1db15-730d-4864-bfc3-99abe7c0ccf8", + "postName": { + "sv": "Ordförande", + "en": "Chairman" + } + }, + { + "id": "844067b3-e95d-4a28-a586-7388f155b8fb", + "postName": { + "sv": "Kassör", + "en": "Treasurer" + } + }, + { + "id": "08efcf3a-1805-4b5f-a60e-da6ce0d33f58", + "postName": { + "sv": "Ledamot", + "en": "Member" + } + }, + { + "id": "524db9a7-e8be-403e-a07c-a41803ea5ee7", + "postName": { + "sv": "Vice Ordförande", + "en": "Vice-chairman" + } + } + ] +} diff --git a/backend/src/main/resources/static/css/main.css b/backend/src/main/resources/static/css/main.css new file mode 100644 index 000000000..e7685c637 --- /dev/null +++ b/backend/src/main/resources/static/css/main.css @@ -0,0 +1,156 @@ + +* { + --mdc-theme-primary: #2196F3; + --mdc-theme-secondary: #2196F3; +} + +.mdc-text-field--focused:not(.mdc-text-field--disabled) .mdc-floating-label { + color: var(--mdc-theme-primary) !important; +} + +body { + margin: 0; + height: 100vh; +} + +canvas { + position: absolute; + z-index: 1; +} + +body { + background: black; + margin: 0; +} + +.fadeInBackground { + width: 100vw; + height: 100vh; + position: absolute; + + animation-name: fadeIn; + animation-iteration-count: 1; + animation-timing-function: ease-in-out; + animation-duration: 0.5s; + animation-fill-mode: forwards; + + object-fit: cover; + object-position: center; +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +footer { + text-align: center; + color: white; + + position: absolute; + bottom: 8px; + width: 100%; +} + +#it-logo { + width: 50px; + height: 50px; +} + +.text { + font-size: 16px; + font-weight: normal; + margin: 0; +} + +.center-vertical { + display: block; + margin-top: auto; + margin-bottom: auto; +} + +.text--large { + font-size: 23px; + font-weight: 900; +} + +.text--small { + font-size: 13px !important; +} + +.text--roboto { + font-family: "Roboto", serif; +} + +.card { + margin: auto; + + display: flex; + flex-direction: column; + + max-width: 280px; + min-width: 250px; + + padding: 16px; + + background-color: white; +} + +.padding { + display: block; + width: 16px; + height: 16px; +} + +.row { + display: flex; + flex-direction: row; +} + +.row--space-between { + justify-content: space-between; +} + +.black-color { + color: black !important; +} + +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #bbb; + margin: 0; + margin-top: 8px; + padding: 0; +} + +.centerCard { + position: absolute; + z-index: 2; + width: 100%; + height: 100%; + + flex-grow: 1; + flex-shrink: 1; + + display: grid; + grid-template-columns: auto; + grid-template-rows: auto; + justify-content: center; + align-content: center; +} + +footer { + z-index: 2; + position: absolute; + bottom: 16px; +} + +.text-link { + color: #09cdda; +} \ No newline at end of file diff --git a/backend/src/main/resources/static/css/mcw.min.css b/backend/src/main/resources/static/css/mcw.min.css new file mode 100644 index 000000000..6056e53ea --- /dev/null +++ b/backend/src/main/resources/static/css/mcw.min.css @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/material-components/material-components-web/blob/master/LICENSE + */ +.mdc-touch-target-wrapper{display:inline}.mdc-elevation-overlay{position:absolute;border-radius:inherit;opacity:0;pointer-events:none;transition:opacity 280ms cubic-bezier(0.4, 0, 0.2, 1);background-color:#fff}.mdc-button{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-button-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-button-font-size, 0.875rem);line-height:2.25rem;line-height:var(--mdc-typography-button-line-height, 2.25rem);font-weight:500;font-weight:var(--mdc-typography-button-font-weight, 500);letter-spacing:.0892857143em;letter-spacing:var(--mdc-typography-button-letter-spacing, 0.0892857143em);text-decoration:none;-webkit-text-decoration:var(--mdc-typography-button-text-decoration, none);text-decoration:var(--mdc-typography-button-text-decoration, none);text-transform:uppercase;text-transform:var(--mdc-typography-button-text-transform, uppercase);padding:0 8px 0 8px;position:relative;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;min-width:64px;border:none;outline:none;line-height:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-appearance:none;overflow:visible;vertical-align:middle;border-radius:4px}.mdc-button .mdc-elevation-overlay{width:100%;height:100%;top:0;left:0}.mdc-button::-moz-focus-inner{padding:0;border:0}.mdc-button:active{outline:none}.mdc-button:hover{cursor:pointer}.mdc-button:disabled{cursor:default;pointer-events:none}.mdc-button .mdc-button__ripple{border-radius:4px}.mdc-button:not(:disabled){background-color:transparent}.mdc-button:disabled{background-color:transparent}.mdc-button .mdc-button__icon{margin-left:0;margin-right:8px;display:inline-block;width:18px;height:18px;font-size:18px;vertical-align:top}[dir=rtl] .mdc-button .mdc-button__icon,.mdc-button .mdc-button__icon[dir=rtl]{margin-left:8px;margin-right:0}.mdc-button .mdc-button__touch{position:absolute;top:50%;right:0;height:48px;left:0;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.mdc-button:not(:disabled){color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}.mdc-button:disabled{color:rgba(0,0,0,.38)}.mdc-button__label+.mdc-button__icon{margin-left:8px;margin-right:0}[dir=rtl] .mdc-button__label+.mdc-button__icon,.mdc-button__label+.mdc-button__icon[dir=rtl]{margin-left:0;margin-right:8px}svg.mdc-button__icon{fill:currentColor}.mdc-button--raised .mdc-button__icon,.mdc-button--unelevated .mdc-button__icon,.mdc-button--outlined .mdc-button__icon{margin-left:-4px;margin-right:8px}[dir=rtl] .mdc-button--raised .mdc-button__icon,.mdc-button--raised .mdc-button__icon[dir=rtl],[dir=rtl] .mdc-button--unelevated .mdc-button__icon,.mdc-button--unelevated .mdc-button__icon[dir=rtl],[dir=rtl] .mdc-button--outlined .mdc-button__icon,.mdc-button--outlined .mdc-button__icon[dir=rtl]{margin-left:8px;margin-right:-4px}.mdc-button--raised .mdc-button__label+.mdc-button__icon,.mdc-button--unelevated .mdc-button__label+.mdc-button__icon,.mdc-button--outlined .mdc-button__label+.mdc-button__icon{margin-left:8px;margin-right:-4px}[dir=rtl] .mdc-button--raised .mdc-button__label+.mdc-button__icon,.mdc-button--raised .mdc-button__label+.mdc-button__icon[dir=rtl],[dir=rtl] .mdc-button--unelevated .mdc-button__label+.mdc-button__icon,.mdc-button--unelevated .mdc-button__label+.mdc-button__icon[dir=rtl],[dir=rtl] .mdc-button--outlined .mdc-button__label+.mdc-button__icon,.mdc-button--outlined .mdc-button__label+.mdc-button__icon[dir=rtl]{margin-left:-4px;margin-right:8px}.mdc-button--raised,.mdc-button--unelevated{padding:0 16px 0 16px}.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){color:#fff;color:var(--mdc-theme-on-primary, #fff)}.mdc-button--raised:disabled,.mdc-button--unelevated:disabled{background-color:rgba(0,0,0,.12)}.mdc-button--raised:disabled,.mdc-button--unelevated:disabled{color:rgba(0,0,0,.38)}.mdc-button--raised{box-shadow:0px 3px 1px -2px rgba(0, 0, 0, 0.2),0px 2px 2px 0px rgba(0, 0, 0, 0.14),0px 1px 5px 0px rgba(0,0,0,.12);transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-button--raised:hover,.mdc-button--raised:focus{box-shadow:0px 2px 4px -1px rgba(0, 0, 0, 0.2),0px 4px 5px 0px rgba(0, 0, 0, 0.14),0px 1px 10px 0px rgba(0,0,0,.12)}.mdc-button--raised:active{box-shadow:0px 5px 5px -3px rgba(0, 0, 0, 0.2),0px 8px 10px 1px rgba(0, 0, 0, 0.14),0px 3px 14px 2px rgba(0,0,0,.12)}.mdc-button--raised:disabled{box-shadow:0px 0px 0px 0px rgba(0, 0, 0, 0.2),0px 0px 0px 0px rgba(0, 0, 0, 0.14),0px 0px 0px 0px rgba(0,0,0,.12)}.mdc-button--outlined{padding:0 15px 0 15px;border-width:1px;border-style:solid}.mdc-button--outlined .mdc-button__ripple{top:-1px;left:-1px;border:1px solid transparent}.mdc-button--outlined .mdc-button__touch{left:-1px;width:calc(100% + 2 * 1px)}.mdc-button--outlined:not(:disabled){border-color:rgba(0,0,0,.12)}.mdc-button--outlined:disabled{border-color:rgba(0,0,0,.12)}.mdc-button--touch{margin-top:6px;margin-bottom:6px}@-webkit-keyframes mdc-ripple-fg-radius-in{from{-webkit-animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);-webkit-transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-radius-in{from{-webkit-animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);-webkit-transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@-webkit-keyframes mdc-ripple-fg-opacity-in{from{-webkit-animation-timing-function:linear;animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-in{from{-webkit-animation-timing-function:linear;animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@-webkit-keyframes mdc-ripple-fg-opacity-out{from{-webkit-animation-timing-function:linear;animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}@keyframes mdc-ripple-fg-opacity-out{from{-webkit-animation-timing-function:linear;animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}.mdc-button{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mdc-button .mdc-button__ripple::before,.mdc-button .mdc-button__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-button .mdc-button__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-button.mdc-ripple-upgraded .mdc-button__ripple::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-button.mdc-ripple-upgraded .mdc-button__ripple::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}.mdc-button.mdc-ripple-upgraded--unbounded .mdc-button__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-button.mdc-ripple-upgraded--foreground-activation .mdc-button__ripple::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-button.mdc-ripple-upgraded--foreground-deactivation .mdc-button__ripple::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-button .mdc-button__ripple::before,.mdc-button .mdc-button__ripple::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-button.mdc-ripple-upgraded .mdc-button__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-button .mdc-button__ripple::before,.mdc-button .mdc-button__ripple::after{background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}.mdc-button:hover .mdc-button__ripple::before{opacity:.04}.mdc-button.mdc-ripple-upgraded--background-focused .mdc-button__ripple::before,.mdc-button:not(.mdc-ripple-upgraded):focus .mdc-button__ripple::before{transition-duration:75ms;opacity:.12}.mdc-button:not(.mdc-ripple-upgraded) .mdc-button__ripple::after{transition:opacity 150ms linear}.mdc-button:not(.mdc-ripple-upgraded):active .mdc-button__ripple::after{transition-duration:75ms;opacity:.12}.mdc-button.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-button .mdc-button__ripple{position:absolute;box-sizing:content-box;width:100%;height:100%;overflow:hidden}.mdc-button:not(.mdc-button--outlined) .mdc-button__ripple{top:0;left:0}.mdc-button--raised .mdc-button__ripple::before,.mdc-button--raised .mdc-button__ripple::after,.mdc-button--unelevated .mdc-button__ripple::before,.mdc-button--unelevated .mdc-button__ripple::after{background-color:#fff;background-color:var(--mdc-theme-on-primary, #fff)}.mdc-button--raised:hover .mdc-button__ripple::before,.mdc-button--unelevated:hover .mdc-button__ripple::before{opacity:.08}.mdc-button--raised.mdc-ripple-upgraded--background-focused .mdc-button__ripple::before,.mdc-button--raised:not(.mdc-ripple-upgraded):focus .mdc-button__ripple::before,.mdc-button--unelevated.mdc-ripple-upgraded--background-focused .mdc-button__ripple::before,.mdc-button--unelevated:not(.mdc-ripple-upgraded):focus .mdc-button__ripple::before{transition-duration:75ms;opacity:.24}.mdc-button--raised:not(.mdc-ripple-upgraded) .mdc-button__ripple::after,.mdc-button--unelevated:not(.mdc-ripple-upgraded) .mdc-button__ripple::after{transition:opacity 150ms linear}.mdc-button--raised:not(.mdc-ripple-upgraded):active .mdc-button__ripple::after,.mdc-button--unelevated:not(.mdc-ripple-upgraded):active .mdc-button__ripple::after{transition-duration:75ms;opacity:.24}.mdc-button--raised.mdc-ripple-upgraded,.mdc-button--unelevated.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.24}.mdc-button{height:36px}.mdc-card{border-radius:4px;background-color:#fff;background-color:var(--mdc-theme-surface, #fff);position:relative;box-shadow:0px 2px 1px -1px rgba(0, 0, 0, 0.2),0px 1px 1px 0px rgba(0, 0, 0, 0.14),0px 1px 3px 0px rgba(0,0,0,.12);display:flex;flex-direction:column;box-sizing:border-box}.mdc-card .mdc-elevation-overlay{width:100%;height:100%;top:0;left:0}.mdc-card--outlined{box-shadow:0px 0px 0px 0px rgba(0, 0, 0, 0.2),0px 0px 0px 0px rgba(0, 0, 0, 0.14),0px 0px 0px 0px rgba(0,0,0,.12);border-width:1px;border-style:solid;border-color:#e0e0e0}.mdc-card__media{position:relative;box-sizing:border-box;background-repeat:no-repeat;background-position:center;background-size:cover}.mdc-card__media::before{display:block;content:""}.mdc-card__media:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.mdc-card__media:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.mdc-card__media--square::before{margin-top:100%}.mdc-card__media--16-9::before{margin-top:56.25%}.mdc-card__media-content{position:absolute;top:0;right:0;bottom:0;left:0;box-sizing:border-box}.mdc-card__primary-action{display:flex;flex-direction:column;box-sizing:border-box;position:relative;outline:none;color:inherit;text-decoration:none;cursor:pointer;overflow:hidden}.mdc-card__primary-action:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.mdc-card__primary-action:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.mdc-card__actions{display:flex;flex-direction:row;align-items:center;box-sizing:border-box;min-height:52px;padding:8px}.mdc-card__actions--full-bleed{padding:0}.mdc-card__action-buttons,.mdc-card__action-icons{display:flex;flex-direction:row;align-items:center;box-sizing:border-box}.mdc-card__action-icons{color:rgba(0,0,0,.6);flex-grow:1;justify-content:flex-end}.mdc-card__action-buttons+.mdc-card__action-icons{margin-left:16px;margin-right:0}[dir=rtl] .mdc-card__action-buttons+.mdc-card__action-icons,.mdc-card__action-buttons+.mdc-card__action-icons[dir=rtl]{margin-left:0;margin-right:16px}.mdc-card__action{display:inline-flex;flex-direction:row;align-items:center;box-sizing:border-box;justify-content:center;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdc-card__action:focus{outline:none}.mdc-card__action--button{margin-left:0;margin-right:8px;padding:0 8px}[dir=rtl] .mdc-card__action--button,.mdc-card__action--button[dir=rtl]{margin-left:8px;margin-right:0}.mdc-card__action--button:last-child{margin-left:0;margin-right:0}[dir=rtl] .mdc-card__action--button:last-child,.mdc-card__action--button:last-child[dir=rtl]{margin-left:0;margin-right:0}.mdc-card__actions--full-bleed .mdc-card__action--button{justify-content:space-between;width:100%;height:auto;max-height:none;margin:0;padding:8px 16px;text-align:left}[dir=rtl] .mdc-card__actions--full-bleed .mdc-card__action--button,.mdc-card__actions--full-bleed .mdc-card__action--button[dir=rtl]{text-align:right}.mdc-card__action--icon{margin:-6px 0;padding:12px}.mdc-card__action--icon:not(:disabled){color:rgba(0,0,0,.6)}.mdc-card__primary-action{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mdc-card__primary-action::before,.mdc-card__primary-action::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-card__primary-action::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-card__primary-action.mdc-ripple-upgraded::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-card__primary-action.mdc-ripple-upgraded::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}.mdc-card__primary-action.mdc-ripple-upgraded--unbounded::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-card__primary-action.mdc-ripple-upgraded--foreground-activation::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-card__primary-action.mdc-ripple-upgraded--foreground-deactivation::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-card__primary-action::before,.mdc-card__primary-action::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-card__primary-action.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-card__primary-action::before,.mdc-card__primary-action::after{background-color:#000}.mdc-card__primary-action:hover::before{opacity:.04}.mdc-card__primary-action.mdc-ripple-upgraded--background-focused::before,.mdc-card__primary-action:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.12}.mdc-card__primary-action:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-card__primary-action:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.12}.mdc-card__primary-action.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}@-webkit-keyframes mdc-checkbox-unchecked-checked-checkmark-path{0%,50%{stroke-dashoffset:29.7833385}50%{-webkit-animation-timing-function:cubic-bezier(0, 0, 0.2, 1);animation-timing-function:cubic-bezier(0, 0, 0.2, 1)}100%{stroke-dashoffset:0}}@keyframes mdc-checkbox-unchecked-checked-checkmark-path{0%,50%{stroke-dashoffset:29.7833385}50%{-webkit-animation-timing-function:cubic-bezier(0, 0, 0.2, 1);animation-timing-function:cubic-bezier(0, 0, 0.2, 1)}100%{stroke-dashoffset:0}}@-webkit-keyframes mdc-checkbox-unchecked-indeterminate-mixedmark{0%,68.2%{-webkit-transform:scaleX(0);transform:scaleX(0)}68.2%{-webkit-animation-timing-function:cubic-bezier(0, 0, 0, 1);animation-timing-function:cubic-bezier(0, 0, 0, 1)}100%{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes mdc-checkbox-unchecked-indeterminate-mixedmark{0%,68.2%{-webkit-transform:scaleX(0);transform:scaleX(0)}68.2%{-webkit-animation-timing-function:cubic-bezier(0, 0, 0, 1);animation-timing-function:cubic-bezier(0, 0, 0, 1)}100%{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes mdc-checkbox-checked-unchecked-checkmark-path{from{-webkit-animation-timing-function:cubic-bezier(0.4, 0, 1, 1);animation-timing-function:cubic-bezier(0.4, 0, 1, 1);opacity:1;stroke-dashoffset:0}to{opacity:0;stroke-dashoffset:-29.7833385}}@keyframes mdc-checkbox-checked-unchecked-checkmark-path{from{-webkit-animation-timing-function:cubic-bezier(0.4, 0, 1, 1);animation-timing-function:cubic-bezier(0.4, 0, 1, 1);opacity:1;stroke-dashoffset:0}to{opacity:0;stroke-dashoffset:-29.7833385}}@-webkit-keyframes mdc-checkbox-checked-indeterminate-checkmark{from{-webkit-animation-timing-function:cubic-bezier(0, 0, 0.2, 1);animation-timing-function:cubic-bezier(0, 0, 0.2, 1);-webkit-transform:rotate(0deg);transform:rotate(0deg);opacity:1}to{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes mdc-checkbox-checked-indeterminate-checkmark{from{-webkit-animation-timing-function:cubic-bezier(0, 0, 0.2, 1);animation-timing-function:cubic-bezier(0, 0, 0.2, 1);-webkit-transform:rotate(0deg);transform:rotate(0deg);opacity:1}to{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@-webkit-keyframes mdc-checkbox-indeterminate-checked-checkmark{from{-webkit-animation-timing-function:cubic-bezier(0.14, 0, 0, 1);animation-timing-function:cubic-bezier(0.14, 0, 0, 1);-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:rotate(360deg);transform:rotate(360deg);opacity:1}}@keyframes mdc-checkbox-indeterminate-checked-checkmark{from{-webkit-animation-timing-function:cubic-bezier(0.14, 0, 0, 1);animation-timing-function:cubic-bezier(0.14, 0, 0, 1);-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:rotate(360deg);transform:rotate(360deg);opacity:1}}@-webkit-keyframes mdc-checkbox-checked-indeterminate-mixedmark{from{-webkit-animation-timing-function:mdc-animation-deceleration-curve-timing-function;animation-timing-function:mdc-animation-deceleration-curve-timing-function;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}to{-webkit-transform:rotate(0deg);transform:rotate(0deg);opacity:1}}@keyframes mdc-checkbox-checked-indeterminate-mixedmark{from{-webkit-animation-timing-function:mdc-animation-deceleration-curve-timing-function;animation-timing-function:mdc-animation-deceleration-curve-timing-function;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}to{-webkit-transform:rotate(0deg);transform:rotate(0deg);opacity:1}}@-webkit-keyframes mdc-checkbox-indeterminate-checked-mixedmark{from{-webkit-animation-timing-function:cubic-bezier(0.14, 0, 0, 1);animation-timing-function:cubic-bezier(0.14, 0, 0, 1);-webkit-transform:rotate(0deg);transform:rotate(0deg);opacity:1}to{-webkit-transform:rotate(315deg);transform:rotate(315deg);opacity:0}}@keyframes mdc-checkbox-indeterminate-checked-mixedmark{from{-webkit-animation-timing-function:cubic-bezier(0.14, 0, 0, 1);animation-timing-function:cubic-bezier(0.14, 0, 0, 1);-webkit-transform:rotate(0deg);transform:rotate(0deg);opacity:1}to{-webkit-transform:rotate(315deg);transform:rotate(315deg);opacity:0}}@-webkit-keyframes mdc-checkbox-indeterminate-unchecked-mixedmark{0%{-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-transform:scaleX(1);transform:scaleX(1);opacity:1}32.8%,100%{-webkit-transform:scaleX(0);transform:scaleX(0);opacity:0}}@keyframes mdc-checkbox-indeterminate-unchecked-mixedmark{0%{-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-transform:scaleX(1);transform:scaleX(1);opacity:1}32.8%,100%{-webkit-transform:scaleX(0);transform:scaleX(0);opacity:0}}.mdc-checkbox{display:inline-block;position:relative;flex:0 0 18px;box-sizing:content-box;width:18px;height:18px;line-height:0;white-space:nowrap;cursor:pointer;vertical-align:bottom;padding:11px}.mdc-checkbox .mdc-checkbox__native-control:checked~.mdc-checkbox__background::before,.mdc-checkbox .mdc-checkbox__native-control:indeterminate~.mdc-checkbox__background::before,.mdc-checkbox .mdc-checkbox__native-control[data-indeterminate=true]~.mdc-checkbox__background::before{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-checkbox.mdc-checkbox--selected .mdc-checkbox__ripple::before,.mdc-checkbox.mdc-checkbox--selected .mdc-checkbox__ripple::after{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-checkbox.mdc-checkbox--selected:hover .mdc-checkbox__ripple::before{opacity:.04}.mdc-checkbox.mdc-checkbox--selected.mdc-ripple-upgraded--background-focused .mdc-checkbox__ripple::before,.mdc-checkbox.mdc-checkbox--selected:not(.mdc-ripple-upgraded):focus .mdc-checkbox__ripple::before{transition-duration:75ms;opacity:.12}.mdc-checkbox.mdc-checkbox--selected:not(.mdc-ripple-upgraded) .mdc-checkbox__ripple::after{transition:opacity 150ms linear}.mdc-checkbox.mdc-checkbox--selected:not(.mdc-ripple-upgraded):active .mdc-checkbox__ripple::after{transition-duration:75ms;opacity:.12}.mdc-checkbox.mdc-checkbox--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-checkbox.mdc-ripple-upgraded--background-focused.mdc-checkbox--selected .mdc-checkbox__ripple::before,.mdc-checkbox.mdc-ripple-upgraded--background-focused.mdc-checkbox--selected .mdc-checkbox__ripple::after{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-checkbox .mdc-checkbox__background{top:11px;left:11px}.mdc-checkbox .mdc-checkbox__background::before{top:-13px;left:-13px;width:40px;height:40px}.mdc-checkbox .mdc-checkbox__native-control{top:0px;right:0px;left:0px;width:40px;height:40px}.mdc-checkbox__native-control:enabled:not(:checked):not(:indeterminate):not([data-indeterminate=true])~.mdc-checkbox__background{border-color:rgba(0,0,0,.54);background-color:transparent}.mdc-checkbox__native-control:enabled:checked~.mdc-checkbox__background,.mdc-checkbox__native-control:enabled:indeterminate~.mdc-checkbox__background,.mdc-checkbox__native-control[data-indeterminate=true]:enabled~.mdc-checkbox__background{border-color:#018786;border-color:var(--mdc-theme-secondary, #018786);background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}@-webkit-keyframes mdc-checkbox-fade-in-background-8A000000secondary00000000secondary{0%{border-color:rgba(0,0,0,.54);background-color:transparent}50%{border-color:#018786;border-color:var(--mdc-theme-secondary, #018786);background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}}@keyframes mdc-checkbox-fade-in-background-8A000000secondary00000000secondary{0%{border-color:rgba(0,0,0,.54);background-color:transparent}50%{border-color:#018786;border-color:var(--mdc-theme-secondary, #018786);background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}}@-webkit-keyframes mdc-checkbox-fade-out-background-8A000000secondary00000000secondary{0%,80%{border-color:#018786;border-color:var(--mdc-theme-secondary, #018786);background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}100%{border-color:rgba(0,0,0,.54);background-color:transparent}}@keyframes mdc-checkbox-fade-out-background-8A000000secondary00000000secondary{0%,80%{border-color:#018786;border-color:var(--mdc-theme-secondary, #018786);background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}100%{border-color:rgba(0,0,0,.54);background-color:transparent}}.mdc-checkbox--anim-unchecked-checked .mdc-checkbox__native-control:enabled~.mdc-checkbox__background,.mdc-checkbox--anim-unchecked-indeterminate .mdc-checkbox__native-control:enabled~.mdc-checkbox__background{-webkit-animation-name:mdc-checkbox-fade-in-background-8A000000secondary00000000secondary;animation-name:mdc-checkbox-fade-in-background-8A000000secondary00000000secondary}.mdc-checkbox--anim-checked-unchecked .mdc-checkbox__native-control:enabled~.mdc-checkbox__background,.mdc-checkbox--anim-indeterminate-unchecked .mdc-checkbox__native-control:enabled~.mdc-checkbox__background{-webkit-animation-name:mdc-checkbox-fade-out-background-8A000000secondary00000000secondary;animation-name:mdc-checkbox-fade-out-background-8A000000secondary00000000secondary}.mdc-checkbox__native-control[disabled]:not(:checked):not(:indeterminate):not([data-indeterminate=true])~.mdc-checkbox__background{border-color:rgba(0,0,0,.38);background-color:transparent}.mdc-checkbox__native-control[disabled]:checked~.mdc-checkbox__background,.mdc-checkbox__native-control[disabled]:indeterminate~.mdc-checkbox__background,.mdc-checkbox__native-control[data-indeterminate=true][disabled]~.mdc-checkbox__background{border-color:transparent;background-color:rgba(0,0,0,.38)}.mdc-checkbox__native-control:enabled~.mdc-checkbox__background .mdc-checkbox__checkmark{color:#fff}.mdc-checkbox__native-control:enabled~.mdc-checkbox__background .mdc-checkbox__mixedmark{border-color:#fff}.mdc-checkbox__native-control:disabled~.mdc-checkbox__background .mdc-checkbox__checkmark{color:#fff}.mdc-checkbox__native-control:disabled~.mdc-checkbox__background .mdc-checkbox__mixedmark{border-color:#fff}@media screen and (-ms-high-contrast: active){.mdc-checkbox__native-control[disabled]:not(:checked):not(:indeterminate):not([data-indeterminate=true])~.mdc-checkbox__background{border-color:GrayText;background-color:transparent}.mdc-checkbox__native-control[disabled]:checked~.mdc-checkbox__background,.mdc-checkbox__native-control[disabled]:indeterminate~.mdc-checkbox__background,.mdc-checkbox__native-control[data-indeterminate=true][disabled]~.mdc-checkbox__background{border-color:GrayText;background-color:transparent}.mdc-checkbox__native-control:disabled~.mdc-checkbox__background .mdc-checkbox__checkmark{color:GrayText}.mdc-checkbox__native-control:disabled~.mdc-checkbox__background .mdc-checkbox__mixedmark{border-color:GrayText}.mdc-checkbox__mixedmark{margin:0 1px}}.mdc-checkbox--disabled{cursor:default;pointer-events:none}.mdc-checkbox__background{display:inline-flex;position:absolute;align-items:center;justify-content:center;box-sizing:border-box;width:18px;height:18px;border:2px solid currentColor;border-radius:2px;background-color:transparent;pointer-events:none;will-change:background-color,border-color;transition:background-color 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1),border-color 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1)}.mdc-checkbox__background .mdc-checkbox__background::before{background-color:#000;background-color:var(--mdc-theme-on-surface, #000)}.mdc-checkbox__checkmark{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;opacity:0;transition:opacity 180ms 0ms cubic-bezier(0.4, 0, 0.6, 1)}.mdc-checkbox--upgraded .mdc-checkbox__checkmark{opacity:1}.mdc-checkbox__checkmark-path{transition:stroke-dashoffset 180ms 0ms cubic-bezier(0.4, 0, 0.6, 1);stroke:currentColor;stroke-width:3.12px;stroke-dashoffset:29.7833385;stroke-dasharray:29.7833385}.mdc-checkbox__mixedmark{width:100%;height:0;-webkit-transform:scaleX(0) rotate(0deg);transform:scaleX(0) rotate(0deg);border-width:1px;border-style:solid;opacity:0;transition:opacity 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1),-webkit-transform 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1);transition:opacity 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1),transform 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1);transition:opacity 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1),transform 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1),-webkit-transform 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1)}.mdc-checkbox--upgraded .mdc-checkbox__background,.mdc-checkbox--upgraded .mdc-checkbox__checkmark,.mdc-checkbox--upgraded .mdc-checkbox__checkmark-path,.mdc-checkbox--upgraded .mdc-checkbox__mixedmark{transition:none !important}.mdc-checkbox--anim-unchecked-checked .mdc-checkbox__background,.mdc-checkbox--anim-unchecked-indeterminate .mdc-checkbox__background,.mdc-checkbox--anim-checked-unchecked .mdc-checkbox__background,.mdc-checkbox--anim-indeterminate-unchecked .mdc-checkbox__background{-webkit-animation-duration:180ms;animation-duration:180ms;-webkit-animation-timing-function:linear;animation-timing-function:linear}.mdc-checkbox--anim-unchecked-checked .mdc-checkbox__checkmark-path{-webkit-animation:mdc-checkbox-unchecked-checked-checkmark-path 180ms linear 0s;animation:mdc-checkbox-unchecked-checked-checkmark-path 180ms linear 0s;transition:none}.mdc-checkbox--anim-unchecked-indeterminate .mdc-checkbox__mixedmark{-webkit-animation:mdc-checkbox-unchecked-indeterminate-mixedmark 90ms linear 0s;animation:mdc-checkbox-unchecked-indeterminate-mixedmark 90ms linear 0s;transition:none}.mdc-checkbox--anim-checked-unchecked .mdc-checkbox__checkmark-path{-webkit-animation:mdc-checkbox-checked-unchecked-checkmark-path 90ms linear 0s;animation:mdc-checkbox-checked-unchecked-checkmark-path 90ms linear 0s;transition:none}.mdc-checkbox--anim-checked-indeterminate .mdc-checkbox__checkmark{-webkit-animation:mdc-checkbox-checked-indeterminate-checkmark 90ms linear 0s;animation:mdc-checkbox-checked-indeterminate-checkmark 90ms linear 0s;transition:none}.mdc-checkbox--anim-checked-indeterminate .mdc-checkbox__mixedmark{-webkit-animation:mdc-checkbox-checked-indeterminate-mixedmark 90ms linear 0s;animation:mdc-checkbox-checked-indeterminate-mixedmark 90ms linear 0s;transition:none}.mdc-checkbox--anim-indeterminate-checked .mdc-checkbox__checkmark{-webkit-animation:mdc-checkbox-indeterminate-checked-checkmark 500ms linear 0s;animation:mdc-checkbox-indeterminate-checked-checkmark 500ms linear 0s;transition:none}.mdc-checkbox--anim-indeterminate-checked .mdc-checkbox__mixedmark{-webkit-animation:mdc-checkbox-indeterminate-checked-mixedmark 500ms linear 0s;animation:mdc-checkbox-indeterminate-checked-mixedmark 500ms linear 0s;transition:none}.mdc-checkbox--anim-indeterminate-unchecked .mdc-checkbox__mixedmark{-webkit-animation:mdc-checkbox-indeterminate-unchecked-mixedmark 300ms linear 0s;animation:mdc-checkbox-indeterminate-unchecked-mixedmark 300ms linear 0s;transition:none}.mdc-checkbox__native-control:checked~.mdc-checkbox__background,.mdc-checkbox__native-control:indeterminate~.mdc-checkbox__background,.mdc-checkbox__native-control[data-indeterminate=true]~.mdc-checkbox__background{transition:border-color 90ms 0ms cubic-bezier(0, 0, 0.2, 1),background-color 90ms 0ms cubic-bezier(0, 0, 0.2, 1)}.mdc-checkbox__native-control:checked~.mdc-checkbox__background .mdc-checkbox__checkmark-path,.mdc-checkbox__native-control:indeterminate~.mdc-checkbox__background .mdc-checkbox__checkmark-path,.mdc-checkbox__native-control[data-indeterminate=true]~.mdc-checkbox__background .mdc-checkbox__checkmark-path{stroke-dashoffset:0}.mdc-checkbox__background::before{position:absolute;-webkit-transform:scale(0, 0);transform:scale(0, 0);border-radius:50%;opacity:0;pointer-events:none;content:"";will-change:opacity,transform;transition:opacity 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1),-webkit-transform 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1);transition:opacity 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1),transform 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1);transition:opacity 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1),transform 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1),-webkit-transform 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1)}.mdc-checkbox__native-control:focus~.mdc-checkbox__background::before{-webkit-transform:scale(1);transform:scale(1);opacity:.12;transition:opacity 80ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 80ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:opacity 80ms 0ms cubic-bezier(0, 0, 0.2, 1),transform 80ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:opacity 80ms 0ms cubic-bezier(0, 0, 0.2, 1),transform 80ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 80ms 0ms cubic-bezier(0, 0, 0.2, 1)}.mdc-checkbox__native-control{position:absolute;margin:0;padding:0;opacity:0;cursor:inherit}.mdc-checkbox__native-control:disabled{cursor:default;pointer-events:none}.mdc-checkbox--touch{margin-top:4px;margin-bottom:4px;margin-right:4px;margin-left:4px}.mdc-checkbox--touch .mdc-checkbox__native-control{top:-4px;right:-4px;left:-4px;width:48px;height:48px}.mdc-checkbox__native-control:checked~.mdc-checkbox__background .mdc-checkbox__checkmark{transition:opacity 180ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 180ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:opacity 180ms 0ms cubic-bezier(0, 0, 0.2, 1),transform 180ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:opacity 180ms 0ms cubic-bezier(0, 0, 0.2, 1),transform 180ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 180ms 0ms cubic-bezier(0, 0, 0.2, 1);opacity:1}.mdc-checkbox__native-control:checked~.mdc-checkbox__background .mdc-checkbox__mixedmark{-webkit-transform:scaleX(1) rotate(-45deg);transform:scaleX(1) rotate(-45deg)}.mdc-checkbox__native-control:indeterminate~.mdc-checkbox__background .mdc-checkbox__checkmark,.mdc-checkbox__native-control[data-indeterminate=true]~.mdc-checkbox__background .mdc-checkbox__checkmark{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0;transition:opacity 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1),-webkit-transform 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1);transition:opacity 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1),transform 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1);transition:opacity 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1),transform 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1),-webkit-transform 90ms 0ms cubic-bezier(0.4, 0, 0.6, 1)}.mdc-checkbox__native-control:indeterminate~.mdc-checkbox__background .mdc-checkbox__mixedmark,.mdc-checkbox__native-control[data-indeterminate=true]~.mdc-checkbox__background .mdc-checkbox__mixedmark{-webkit-transform:scaleX(1) rotate(0deg);transform:scaleX(1) rotate(0deg);opacity:1}.mdc-checkbox{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mdc-checkbox .mdc-checkbox__ripple::before,.mdc-checkbox .mdc-checkbox__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-checkbox .mdc-checkbox__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-checkbox.mdc-ripple-upgraded .mdc-checkbox__ripple::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-checkbox.mdc-ripple-upgraded .mdc-checkbox__ripple::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}.mdc-checkbox.mdc-ripple-upgraded--unbounded .mdc-checkbox__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-checkbox.mdc-ripple-upgraded--foreground-activation .mdc-checkbox__ripple::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-checkbox.mdc-ripple-upgraded--foreground-deactivation .mdc-checkbox__ripple::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-checkbox .mdc-checkbox__ripple::before,.mdc-checkbox .mdc-checkbox__ripple::after{background-color:#000;background-color:var(--mdc-theme-on-surface, #000)}.mdc-checkbox:hover .mdc-checkbox__ripple::before{opacity:.04}.mdc-checkbox.mdc-ripple-upgraded--background-focused .mdc-checkbox__ripple::before,.mdc-checkbox:not(.mdc-ripple-upgraded):focus .mdc-checkbox__ripple::before{transition-duration:75ms;opacity:.12}.mdc-checkbox:not(.mdc-ripple-upgraded) .mdc-checkbox__ripple::after{transition:opacity 150ms linear}.mdc-checkbox:not(.mdc-ripple-upgraded):active .mdc-checkbox__ripple::after{transition-duration:75ms;opacity:.12}.mdc-checkbox.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-checkbox .mdc-checkbox__ripple::before,.mdc-checkbox .mdc-checkbox__ripple::after{top:calc(50% - 50%);left:calc(50% - 50%);width:100%;height:100%}.mdc-checkbox.mdc-ripple-upgraded .mdc-checkbox__ripple::before,.mdc-checkbox.mdc-ripple-upgraded .mdc-checkbox__ripple::after{top:var(--mdc-ripple-top, calc(50% - 50%));left:var(--mdc-ripple-left, calc(50% - 50%));width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-checkbox.mdc-ripple-upgraded .mdc-checkbox__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-checkbox__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.mdc-ripple-upgraded--background-focused .mdc-checkbox__background::before{content:none}.mdc-chip-trailing-action__touch{width:26px}.mdc-chip-trailing-action__touch{position:absolute;top:50%;right:0;height:48px;left:50%;width:48px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%)}.mdc-chip-trailing-action{border:none;display:inline-flex;position:relative;align-items:center;justify-content:center;box-sizing:border-box;padding:0;outline:none;cursor:pointer;-webkit-appearance:none;background:none}.mdc-chip-trailing-action .mdc-chip-trailing-action__icon{height:18px;width:18px;font-size:18px}.mdc-chip-trailing-action .mdc-chip-trailing-action{color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-chip-trailing-action .mdc-chip-trailing-action__icon{fill:currentColor;color:inherit}.mdc-chip-trailing-action{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mdc-chip-trailing-action .mdc-chip-trailing-action__ripple::before,.mdc-chip-trailing-action .mdc-chip-trailing-action__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-chip-trailing-action .mdc-chip-trailing-action__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-chip-trailing-action.mdc-ripple-upgraded .mdc-chip-trailing-action__ripple::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-chip-trailing-action.mdc-ripple-upgraded .mdc-chip-trailing-action__ripple::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}.mdc-chip-trailing-action.mdc-ripple-upgraded--unbounded .mdc-chip-trailing-action__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-chip-trailing-action.mdc-ripple-upgraded--foreground-activation .mdc-chip-trailing-action__ripple::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-chip-trailing-action.mdc-ripple-upgraded--foreground-deactivation .mdc-chip-trailing-action__ripple::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-chip-trailing-action .mdc-chip-trailing-action__ripple::before,.mdc-chip-trailing-action .mdc-chip-trailing-action__ripple::after{top:calc(50% - 50%);left:calc(50% - 50%);width:100%;height:100%}.mdc-chip-trailing-action.mdc-ripple-upgraded .mdc-chip-trailing-action__ripple::before,.mdc-chip-trailing-action.mdc-ripple-upgraded .mdc-chip-trailing-action__ripple::after{top:var(--mdc-ripple-top, calc(50% - 50%));left:var(--mdc-ripple-left, calc(50% - 50%));width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-chip-trailing-action.mdc-ripple-upgraded .mdc-chip-trailing-action__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-chip-trailing-action .mdc-chip-trailing-action__ripple::before,.mdc-chip-trailing-action .mdc-chip-trailing-action__ripple::after{background-color:#000;background-color:var(--mdc-theme-on-surface, #000)}.mdc-chip-trailing-action:hover .mdc-chip-trailing-action__ripple::before{opacity:.04}.mdc-chip-trailing-action.mdc-ripple-upgraded--background-focused .mdc-chip-trailing-action__ripple::before,.mdc-chip-trailing-action:not(.mdc-ripple-upgraded):focus .mdc-chip-trailing-action__ripple::before{transition-duration:75ms;opacity:.12}.mdc-chip-trailing-action:not(.mdc-ripple-upgraded) .mdc-chip-trailing-action__ripple::after{transition:opacity 150ms linear}.mdc-chip-trailing-action:not(.mdc-ripple-upgraded):active .mdc-chip-trailing-action__ripple::after{transition-duration:75ms;opacity:.12}.mdc-chip-trailing-action.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-chip-trailing-action .mdc-chip-trailing-action__ripple{position:absolute;box-sizing:content-box;width:100%;height:100%;overflow:hidden}.mdc-chip__icon--leading{color:rgba(0,0,0,.54)}.mdc-chip-trailing-action{color:#000}.mdc-chip__icon--trailing{color:rgba(0,0,0,.54)}.mdc-chip__icon--trailing:hover{color:rgba(0,0,0,.62)}.mdc-chip__icon--trailing:focus{color:rgba(0,0,0,.87)}.mdc-chip__icon.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden){width:20px;height:20px;font-size:20px}.mdc-chip-trailing-action__icon{height:18px;width:18px;font-size:18px}.mdc-chip__icon.mdc-chip__icon--trailing{width:18px;height:18px;font-size:18px}.mdc-chip-trailing-action{margin-left:4px;margin-right:-4px}[dir=rtl] .mdc-chip-trailing-action,.mdc-chip-trailing-action[dir=rtl]{margin-left:-4px;margin-right:4px}.mdc-chip__icon--trailing{margin-left:4px;margin-right:-4px}[dir=rtl] .mdc-chip__icon--trailing,.mdc-chip__icon--trailing[dir=rtl]{margin-left:-4px;margin-right:4px}.mdc-chip{border-radius:16px;background-color:#e0e0e0;color:rgba(0,0,0,.87);-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);height:32px;position:relative;display:inline-flex;align-items:center;box-sizing:border-box;padding:0 12px;border-width:0;outline:none;cursor:pointer;-webkit-appearance:none}.mdc-chip .mdc-chip__ripple{border-radius:16px}.mdc-chip:hover{color:rgba(0,0,0,.87)}.mdc-chip.mdc-chip--selected .mdc-chip__checkmark,.mdc-chip .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden){margin-left:-4px;margin-right:4px}[dir=rtl] .mdc-chip.mdc-chip--selected .mdc-chip__checkmark,.mdc-chip.mdc-chip--selected .mdc-chip__checkmark[dir=rtl],[dir=rtl] .mdc-chip .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden),.mdc-chip .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden)[dir=rtl]{margin-left:4px;margin-right:-4px}.mdc-chip .mdc-elevation-overlay{width:100%;height:100%;top:0;left:0}.mdc-chip::-moz-focus-inner{padding:0;border:0}.mdc-chip:hover{color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-chip .mdc-chip__touch{position:absolute;top:50%;right:0;height:48px;left:0;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.mdc-chip--exit{transition:opacity 75ms cubic-bezier(0.4, 0, 0.2, 1),width 150ms cubic-bezier(0, 0, 0.2, 1),padding 100ms linear,margin 100ms linear;opacity:0}.mdc-chip__text{white-space:nowrap}.mdc-chip__icon{border-radius:50%;outline:none;vertical-align:middle}.mdc-chip__checkmark{height:20px}.mdc-chip__checkmark-path{transition:stroke-dashoffset 150ms 50ms cubic-bezier(0.4, 0, 0.6, 1);stroke-width:2px;stroke-dashoffset:29.7833385;stroke-dasharray:29.7833385}.mdc-chip__primary-action:focus{outline:none}.mdc-chip--selected .mdc-chip__checkmark-path{stroke-dashoffset:0}.mdc-chip__icon--leading,.mdc-chip__icon--trailing{position:relative}.mdc-chip-set--choice .mdc-chip.mdc-chip--selected{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}.mdc-chip-set--choice .mdc-chip.mdc-chip--selected .mdc-chip__icon--leading{color:rgba(98,0,238,.54)}.mdc-chip-set--choice .mdc-chip.mdc-chip--selected:hover{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}.mdc-chip-set--choice .mdc-chip .mdc-chip__checkmark-path{stroke:#6200ee;stroke:var(--mdc-theme-primary, #6200ee)}.mdc-chip-set--choice .mdc-chip--selected{background-color:#fff;background-color:var(--mdc-theme-surface, #fff)}.mdc-chip__checkmark-svg{width:0;height:20px;transition:width 150ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-chip--selected .mdc-chip__checkmark-svg{width:20px}.mdc-chip-set--filter .mdc-chip__icon--leading{transition:opacity 75ms linear;transition-delay:-50ms;opacity:1}.mdc-chip-set--filter .mdc-chip__icon--leading+.mdc-chip__checkmark{transition:opacity 75ms linear;transition-delay:80ms;opacity:0}.mdc-chip-set--filter .mdc-chip__icon--leading+.mdc-chip__checkmark .mdc-chip__checkmark-svg{transition:width 0ms}.mdc-chip-set--filter .mdc-chip--selected .mdc-chip__icon--leading{opacity:0}.mdc-chip-set--filter .mdc-chip--selected .mdc-chip__icon--leading+.mdc-chip__checkmark{width:0;opacity:1}.mdc-chip-set--filter .mdc-chip__icon--leading-hidden.mdc-chip__icon--leading{width:0;opacity:0}.mdc-chip-set--filter .mdc-chip__icon--leading-hidden.mdc-chip__icon--leading+.mdc-chip__checkmark{width:20px}.mdc-chip{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mdc-chip .mdc-chip__ripple::before,.mdc-chip .mdc-chip__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-chip .mdc-chip__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-chip.mdc-ripple-upgraded .mdc-chip__ripple::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-chip.mdc-ripple-upgraded .mdc-chip__ripple::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}.mdc-chip.mdc-ripple-upgraded--unbounded .mdc-chip__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-chip.mdc-ripple-upgraded--foreground-activation .mdc-chip__ripple::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-chip.mdc-ripple-upgraded--foreground-deactivation .mdc-chip__ripple::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-chip .mdc-chip__ripple::before,.mdc-chip .mdc-chip__ripple::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-chip.mdc-ripple-upgraded .mdc-chip__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-chip .mdc-chip__ripple::before,.mdc-chip .mdc-chip__ripple::after{background-color:rgba(0,0,0,.87)}.mdc-chip:hover .mdc-chip__ripple::before{opacity:.04}.mdc-chip.mdc-ripple-upgraded--background-focused .mdc-chip__ripple::before,.mdc-chip.mdc-ripple-upgraded:focus-within .mdc-chip__ripple::before,.mdc-chip:not(.mdc-ripple-upgraded):focus .mdc-chip__ripple::before,.mdc-chip:not(.mdc-ripple-upgraded):focus-within .mdc-chip__ripple::before{transition-duration:75ms;opacity:.12}.mdc-chip:not(.mdc-ripple-upgraded) .mdc-chip__ripple::after{transition:opacity 150ms linear}.mdc-chip:not(.mdc-ripple-upgraded):active .mdc-chip__ripple::after{transition-duration:75ms;opacity:.12}.mdc-chip.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-chip .mdc-chip__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden}.mdc-chip-set--choice .mdc-chip.mdc-chip--selected .mdc-chip__ripple::before{opacity:.08}.mdc-chip-set--choice .mdc-chip.mdc-chip--selected .mdc-chip__ripple::before,.mdc-chip-set--choice .mdc-chip.mdc-chip--selected .mdc-chip__ripple::after{background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}.mdc-chip-set--choice .mdc-chip.mdc-chip--selected:hover .mdc-chip__ripple::before{opacity:.12}.mdc-chip-set--choice .mdc-chip.mdc-chip--selected.mdc-ripple-upgraded--background-focused .mdc-chip__ripple::before,.mdc-chip-set--choice .mdc-chip.mdc-chip--selected.mdc-ripple-upgraded:focus-within .mdc-chip__ripple::before,.mdc-chip-set--choice .mdc-chip.mdc-chip--selected:not(.mdc-ripple-upgraded):focus .mdc-chip__ripple::before,.mdc-chip-set--choice .mdc-chip.mdc-chip--selected:not(.mdc-ripple-upgraded):focus-within .mdc-chip__ripple::before{transition-duration:75ms;opacity:.2}.mdc-chip-set--choice .mdc-chip.mdc-chip--selected:not(.mdc-ripple-upgraded) .mdc-chip__ripple::after{transition:opacity 150ms linear}.mdc-chip-set--choice .mdc-chip.mdc-chip--selected:not(.mdc-ripple-upgraded):active .mdc-chip__ripple::after{transition-duration:75ms;opacity:.2}.mdc-chip-set--choice .mdc-chip.mdc-chip--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.2}@-webkit-keyframes mdc-chip-entry{from{-webkit-transform:scale(0.8);transform:scale(0.8);opacity:.4}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes mdc-chip-entry{from{-webkit-transform:scale(0.8);transform:scale(0.8);opacity:.4}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.mdc-chip-set{padding:4px;display:flex;flex-wrap:wrap;box-sizing:border-box}.mdc-chip-set .mdc-chip{margin:4px}.mdc-chip-set .mdc-chip--touch{margin-top:8px;margin-bottom:8px}.mdc-chip-set--input .mdc-chip{-webkit-animation:mdc-chip-entry 100ms cubic-bezier(0, 0, 0.2, 1);animation:mdc-chip-entry 100ms cubic-bezier(0, 0, 0.2, 1)}.mdc-circular-progress__determinate-circle,.mdc-circular-progress__indeterminate-circle-graphic{stroke:#6200ee;stroke:var(--mdc-theme-primary, #6200ee)}@-webkit-keyframes mdc-circular-progress-container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes mdc-circular-progress-container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes mdc-circular-progress-spinner-layer-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}100%{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@keyframes mdc-circular-progress-spinner-layer-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}100%{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes mdc-circular-progress-color-1-fade-in-out{from{opacity:.99}25%{opacity:.99}26%{opacity:0}89%{opacity:0}90%{opacity:.99}to{opacity:.99}}@keyframes mdc-circular-progress-color-1-fade-in-out{from{opacity:.99}25%{opacity:.99}26%{opacity:0}89%{opacity:0}90%{opacity:.99}to{opacity:.99}}@-webkit-keyframes mdc-circular-progress-color-2-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:.99}50%{opacity:.99}51%{opacity:0}to{opacity:0}}@keyframes mdc-circular-progress-color-2-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:.99}50%{opacity:.99}51%{opacity:0}to{opacity:0}}@-webkit-keyframes mdc-circular-progress-color-3-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:.99}75%{opacity:.99}76%{opacity:0}to{opacity:0}}@keyframes mdc-circular-progress-color-3-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:.99}75%{opacity:.99}76%{opacity:0}to{opacity:0}}@-webkit-keyframes mdc-circular-progress-color-4-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:.99}90%{opacity:.99}to{opacity:0}}@keyframes mdc-circular-progress-color-4-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:.99}90%{opacity:.99}to{opacity:0}}@-webkit-keyframes mdc-circular-progress-left-spin{from{-webkit-transform:rotate(265deg);transform:rotate(265deg)}50%{-webkit-transform:rotate(130deg);transform:rotate(130deg)}to{-webkit-transform:rotate(265deg);transform:rotate(265deg)}}@keyframes mdc-circular-progress-left-spin{from{-webkit-transform:rotate(265deg);transform:rotate(265deg)}50%{-webkit-transform:rotate(130deg);transform:rotate(130deg)}to{-webkit-transform:rotate(265deg);transform:rotate(265deg)}}@-webkit-keyframes mdc-circular-progress-right-spin{from{-webkit-transform:rotate(-265deg);transform:rotate(-265deg)}50%{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}to{-webkit-transform:rotate(-265deg);transform:rotate(-265deg)}}@keyframes mdc-circular-progress-right-spin{from{-webkit-transform:rotate(-265deg);transform:rotate(-265deg)}50%{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}to{-webkit-transform:rotate(-265deg);transform:rotate(-265deg)}}.mdc-circular-progress{width:48px;height:48px;display:inline-block;position:relative;direction:ltr;transition:opacity 250ms 0ms cubic-bezier(0.4, 0, 0.6, 1)}.mdc-circular-progress .mdc-circular-progress__determinate-circle-graphic,.mdc-circular-progress .mdc-circular-progress__indeterminate-circle-graphic{stroke-width:4px}.mdc-circular-progress .mdc-circular-progress__gap-patch .mdc-circular-progress__indeterminate-circle-graphic{stroke-width:3.2px}.mdc-circular-progress--small{width:24px;height:24px}.mdc-circular-progress--small .mdc-circular-progress__determinate-circle-graphic,.mdc-circular-progress--small .mdc-circular-progress__indeterminate-circle-graphic{stroke-width:2.5px}.mdc-circular-progress--small .mdc-circular-progress__gap-patch .mdc-circular-progress__indeterminate-circle-graphic{stroke-width:2px}.mdc-circular-progress--medium{width:36px;height:36px}.mdc-circular-progress--medium .mdc-circular-progress__determinate-circle-graphic,.mdc-circular-progress--medium .mdc-circular-progress__indeterminate-circle-graphic{stroke-width:3px}.mdc-circular-progress--medium .mdc-circular-progress__gap-patch .mdc-circular-progress__indeterminate-circle-graphic{stroke-width:2.4px}.mdc-circular-progress--large{width:48px;height:48px}.mdc-circular-progress--large .mdc-circular-progress__determinate-circle-graphic,.mdc-circular-progress--large .mdc-circular-progress__indeterminate-circle-graphic{stroke-width:4px}.mdc-circular-progress--large .mdc-circular-progress__gap-patch .mdc-circular-progress__indeterminate-circle-graphic{stroke-width:3.2px}.mdc-circular-progress__determinate-container,.mdc-circular-progress__indeterminate-circle-graphic,.mdc-circular-progress__indeterminate-container,.mdc-circular-progress__spinner-layer{position:absolute;width:100%;height:100%}.mdc-circular-progress__determinate-container{-webkit-transform:rotate(-90deg);transform:rotate(-90deg)}.mdc-circular-progress__indeterminate-container{opacity:0}.mdc-circular-progress__determinate-circle-graphic,.mdc-circular-progress__indeterminate-circle-graphic{fill:transparent}.mdc-circular-progress__determinate-circle{transition:stroke-dashoffset 500ms 0ms cubic-bezier(0, 0, 0.2, 1)}.mdc-circular-progress__gap-patch{position:absolute;top:0;left:47.5%;box-sizing:border-box;width:5%;height:100%;overflow:hidden}.mdc-circular-progress__gap-patch .mdc-circular-progress__indeterminate-circle-graphic{left:-900%;width:2000%;-webkit-transform:rotate(180deg);transform:rotate(180deg)}.mdc-circular-progress__circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden}.mdc-circular-progress__circle-clipper .mdc-circular-progress__indeterminate-circle-graphic{width:200%}.mdc-circular-progress__circle-right .mdc-circular-progress__indeterminate-circle-graphic{left:-100%}.mdc-circular-progress--indeterminate .mdc-circular-progress__determinate-container{opacity:0}.mdc-circular-progress--indeterminate .mdc-circular-progress__indeterminate-container{opacity:1}.mdc-circular-progress--indeterminate .mdc-circular-progress__indeterminate-container{-webkit-animation:mdc-circular-progress-container-rotate 1568.2352941176ms linear infinite;animation:mdc-circular-progress-container-rotate 1568.2352941176ms linear infinite}.mdc-circular-progress--indeterminate .mdc-circular-progress__spinner-layer{-webkit-animation:mdc-circular-progress-spinner-layer-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:mdc-circular-progress-spinner-layer-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.mdc-circular-progress--indeterminate .mdc-circular-progress__color-1{-webkit-animation:mdc-circular-progress-spinner-layer-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,mdc-circular-progress-color-1-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:mdc-circular-progress-spinner-layer-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,mdc-circular-progress-color-1-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.mdc-circular-progress--indeterminate .mdc-circular-progress__color-2{-webkit-animation:mdc-circular-progress-spinner-layer-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,mdc-circular-progress-color-2-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:mdc-circular-progress-spinner-layer-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,mdc-circular-progress-color-2-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.mdc-circular-progress--indeterminate .mdc-circular-progress__color-3{-webkit-animation:mdc-circular-progress-spinner-layer-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,mdc-circular-progress-color-3-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:mdc-circular-progress-spinner-layer-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,mdc-circular-progress-color-3-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.mdc-circular-progress--indeterminate .mdc-circular-progress__color-4{-webkit-animation:mdc-circular-progress-spinner-layer-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,mdc-circular-progress-color-4-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:mdc-circular-progress-spinner-layer-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,mdc-circular-progress-color-4-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.mdc-circular-progress--indeterminate .mdc-circular-progress__circle-left .mdc-circular-progress__indeterminate-circle-graphic{-webkit-animation:mdc-circular-progress-left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:mdc-circular-progress-left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.mdc-circular-progress--indeterminate .mdc-circular-progress__circle-right .mdc-circular-progress__indeterminate-circle-graphic{-webkit-animation:mdc-circular-progress-right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:mdc-circular-progress-right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.mdc-circular-progress--closed{opacity:0}.mdc-data-table__content{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit)}.mdc-data-table{background-color:#fff;background-color:var(--mdc-theme-surface, #fff);border-radius:4px;border-width:1px;border-style:solid;border-color:rgba(0,0,0,.12);-webkit-overflow-scrolling:touch;display:inline-flex;flex-direction:column;box-sizing:border-box;overflow-x:auto;position:relative}.mdc-data-table__row{background-color:inherit}.mdc-data-table__header-row{background-color:inherit}.mdc-data-table__row--selected{background-color:rgba(98,0,238,.04)}.mdc-data-table__row,.mdc-data-table__pagination{border-top-color:rgba(0,0,0,.12)}.mdc-data-table__row,.mdc-data-table__pagination{border-top-width:1px;border-top-style:solid}.mdc-data-table__row:not(.mdc-data-table__row--selected):hover{background-color:rgba(0,0,0,.04)}.mdc-data-table__header-cell{color:rgba(0,0,0,.87)}.mdc-data-table__cell{color:rgba(0,0,0,.87)}.mdc-data-table__cell,.mdc-data-table__pagination{height:52px}.mdc-data-table__header-cell{height:56px}.mdc-data-table__cell,.mdc-data-table__header-cell{padding-right:16px;padding-left:16px}.mdc-data-table__header-cell--checkbox,.mdc-data-table__cell--checkbox{padding-left:16px;padding-right:0}[dir=rtl] .mdc-data-table__header-cell--checkbox,.mdc-data-table__header-cell--checkbox[dir=rtl],[dir=rtl] .mdc-data-table__cell--checkbox,.mdc-data-table__cell--checkbox[dir=rtl]{padding-left:0;padding-right:16px}.mdc-data-table__sort-icon-button{color:rgba(0,0,0,.6)}.mdc-data-table__sort-icon-button::before,.mdc-data-table__sort-icon-button::after{background-color:rgba(0,0,0,.6)}.mdc-data-table__sort-icon-button:hover::before{opacity:.04}.mdc-data-table__sort-icon-button.mdc-ripple-upgraded--background-focused::before,.mdc-data-table__sort-icon-button:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.12}.mdc-data-table__sort-icon-button:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-data-table__sort-icon-button:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.12}.mdc-data-table__sort-icon-button.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-data-table__header-cell--sorted .mdc-data-table__sort-icon-button{color:rgba(0,0,0,.87)}.mdc-data-table__header-cell--sorted .mdc-data-table__sort-icon-button::before,.mdc-data-table__header-cell--sorted .mdc-data-table__sort-icon-button::after{background-color:rgba(0,0,0,.87)}.mdc-data-table__header-cell--sorted .mdc-data-table__sort-icon-button:hover::before{opacity:.04}.mdc-data-table__header-cell--sorted .mdc-data-table__sort-icon-button.mdc-ripple-upgraded--background-focused::before,.mdc-data-table__header-cell--sorted .mdc-data-table__sort-icon-button:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.12}.mdc-data-table__header-cell--sorted .mdc-data-table__sort-icon-button:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-data-table__header-cell--sorted .mdc-data-table__sort-icon-button:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.12}.mdc-data-table__header-cell--sorted .mdc-data-table__sort-icon-button.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-data-table__table{min-width:100%;border:0;white-space:nowrap;border-collapse:collapse;table-layout:fixed}.mdc-data-table__cell{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);box-sizing:border-box;text-overflow:ellipsis;overflow:hidden}.mdc-data-table__cell--numeric{text-align:right}[dir=rtl] .mdc-data-table__cell--numeric,.mdc-data-table__cell--numeric[dir=rtl]{text-align:left}.mdc-data-table__header-cell{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-subtitle2-font-size, 0.875rem);line-height:1.375rem;line-height:var(--mdc-typography-subtitle2-line-height, 1.375rem);font-weight:500;font-weight:var(--mdc-typography-subtitle2-font-weight, 500);letter-spacing:.0071428571em;letter-spacing:var(--mdc-typography-subtitle2-letter-spacing, 0.0071428571em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-subtitle2-text-decoration, inherit);text-decoration:var(--mdc-typography-subtitle2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle2-text-transform, inherit);box-sizing:border-box;text-align:left;text-overflow:ellipsis;overflow:hidden;outline:none}[dir=rtl] .mdc-data-table__header-cell,.mdc-data-table__header-cell[dir=rtl]{text-align:right}.mdc-data-table__header-cell--numeric{text-align:right}[dir=rtl] .mdc-data-table__header-cell--numeric,.mdc-data-table__header-cell--numeric[dir=rtl]{text-align:left}.mdc-data-table__sort-icon-button{width:28px;height:28px;padding:2px;margin-left:4px;margin-right:0;transition:-webkit-transform 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1), -webkit-transform 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0}[dir=rtl] .mdc-data-table__sort-icon-button,.mdc-data-table__sort-icon-button[dir=rtl]{margin-left:0;margin-right:4px}.mdc-data-table__header-cell--numeric .mdc-data-table__sort-icon-button{margin-left:0;margin-right:4px}[dir=rtl] .mdc-data-table__header-cell--numeric .mdc-data-table__sort-icon-button,.mdc-data-table__header-cell--numeric .mdc-data-table__sort-icon-button[dir=rtl]{margin-left:4px;margin-right:0}.mdc-data-table__header-cell--sorted-descending .mdc-data-table__sort-icon-button{-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}.mdc-data-table__sort-icon-button:focus,.mdc-data-table__header-cell:hover .mdc-data-table__sort-icon-button,.mdc-data-table__header-cell--sorted .mdc-data-table__sort-icon-button{opacity:1}.mdc-data-table__header-cell-wrapper{display:inline-flex;align-items:center}.mdc-data-table__header-cell--with-sort{cursor:pointer}.mdc-data-table__progress-indicator{display:none;position:absolute;width:100%}.mdc-data-table--in-progress .mdc-data-table__progress-indicator{display:block}.mdc-data-table__scrim{background-color:#fff;background-color:var(--mdc-theme-surface, #fff);height:100%;opacity:.32;position:absolute;top:0;width:100%}.mdc-data-table__pagination{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);box-sizing:border-box;display:flex;justify-content:flex-end}.mdc-data-table__pagination-trailing{margin-left:4px;margin-right:0;align-items:center;display:flex}[dir=rtl] .mdc-data-table__pagination-trailing,.mdc-data-table__pagination-trailing[dir=rtl]{margin-left:0;margin-right:4px}.mdc-data-table__pagination-button{margin-left:0;margin-right:4px}[dir=rtl] .mdc-data-table__pagination-button .mdc-button__icon,.mdc-data-table__pagination-button .mdc-button__icon[dir=rtl]{-webkit-transform:rotate(180deg);transform:rotate(180deg)}[dir=rtl] .mdc-data-table__pagination-button,.mdc-data-table__pagination-button[dir=rtl]{margin-left:4px;margin-right:0}.mdc-data-table__pagination-total{margin-left:0;margin-right:36px;white-space:nowrap}[dir=rtl] .mdc-data-table__pagination-total,.mdc-data-table__pagination-total[dir=rtl]{margin-left:36px;margin-right:0}.mdc-data-table__header-row-checkbox .mdc-checkbox__native-control:checked~.mdc-checkbox__background::before,.mdc-data-table__header-row-checkbox .mdc-checkbox__native-control:indeterminate~.mdc-checkbox__background::before,.mdc-data-table__header-row-checkbox .mdc-checkbox__native-control[data-indeterminate=true]~.mdc-checkbox__background::before,.mdc-data-table__row-checkbox .mdc-checkbox__native-control:checked~.mdc-checkbox__background::before,.mdc-data-table__row-checkbox .mdc-checkbox__native-control:indeterminate~.mdc-checkbox__background::before,.mdc-data-table__row-checkbox .mdc-checkbox__native-control[data-indeterminate=true]~.mdc-checkbox__background::before{background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}.mdc-data-table__header-row-checkbox.mdc-checkbox--selected .mdc-checkbox__ripple::before,.mdc-data-table__header-row-checkbox.mdc-checkbox--selected .mdc-checkbox__ripple::after,.mdc-data-table__row-checkbox.mdc-checkbox--selected .mdc-checkbox__ripple::before,.mdc-data-table__row-checkbox.mdc-checkbox--selected .mdc-checkbox__ripple::after{background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}.mdc-data-table__header-row-checkbox.mdc-checkbox--selected:hover .mdc-checkbox__ripple::before,.mdc-data-table__row-checkbox.mdc-checkbox--selected:hover .mdc-checkbox__ripple::before{opacity:.04}.mdc-data-table__header-row-checkbox.mdc-checkbox--selected.mdc-ripple-upgraded--background-focused .mdc-checkbox__ripple::before,.mdc-data-table__header-row-checkbox.mdc-checkbox--selected:not(.mdc-ripple-upgraded):focus .mdc-checkbox__ripple::before,.mdc-data-table__row-checkbox.mdc-checkbox--selected.mdc-ripple-upgraded--background-focused .mdc-checkbox__ripple::before,.mdc-data-table__row-checkbox.mdc-checkbox--selected:not(.mdc-ripple-upgraded):focus .mdc-checkbox__ripple::before{transition-duration:75ms;opacity:.12}.mdc-data-table__header-row-checkbox.mdc-checkbox--selected:not(.mdc-ripple-upgraded) .mdc-checkbox__ripple::after,.mdc-data-table__row-checkbox.mdc-checkbox--selected:not(.mdc-ripple-upgraded) .mdc-checkbox__ripple::after{transition:opacity 150ms linear}.mdc-data-table__header-row-checkbox.mdc-checkbox--selected:not(.mdc-ripple-upgraded):active .mdc-checkbox__ripple::after,.mdc-data-table__row-checkbox.mdc-checkbox--selected:not(.mdc-ripple-upgraded):active .mdc-checkbox__ripple::after{transition-duration:75ms;opacity:.12}.mdc-data-table__header-row-checkbox.mdc-checkbox--selected.mdc-ripple-upgraded,.mdc-data-table__row-checkbox.mdc-checkbox--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-data-table__header-row-checkbox.mdc-ripple-upgraded--background-focused.mdc-checkbox--selected .mdc-checkbox__ripple::before,.mdc-data-table__header-row-checkbox.mdc-ripple-upgraded--background-focused.mdc-checkbox--selected .mdc-checkbox__ripple::after,.mdc-data-table__row-checkbox.mdc-ripple-upgraded--background-focused.mdc-checkbox--selected .mdc-checkbox__ripple::before,.mdc-data-table__row-checkbox.mdc-ripple-upgraded--background-focused.mdc-checkbox--selected .mdc-checkbox__ripple::after{background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}.mdc-data-table__header-row-checkbox .mdc-checkbox__native-control:enabled:not(:checked):not(:indeterminate):not([data-indeterminate=true])~.mdc-checkbox__background,.mdc-data-table__row-checkbox .mdc-checkbox__native-control:enabled:not(:checked):not(:indeterminate):not([data-indeterminate=true])~.mdc-checkbox__background{border-color:rgba(0,0,0,.54);background-color:transparent}.mdc-data-table__header-row-checkbox .mdc-checkbox__native-control:enabled:checked~.mdc-checkbox__background,.mdc-data-table__header-row-checkbox .mdc-checkbox__native-control:enabled:indeterminate~.mdc-checkbox__background,.mdc-data-table__header-row-checkbox .mdc-checkbox__native-control[data-indeterminate=true]:enabled~.mdc-checkbox__background,.mdc-data-table__row-checkbox .mdc-checkbox__native-control:enabled:checked~.mdc-checkbox__background,.mdc-data-table__row-checkbox .mdc-checkbox__native-control:enabled:indeterminate~.mdc-checkbox__background,.mdc-data-table__row-checkbox .mdc-checkbox__native-control[data-indeterminate=true]:enabled~.mdc-checkbox__background{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee);background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}@-webkit-keyframes mdc-checkbox-fade-in-background-8A000000primary00000000primary{0%{border-color:rgba(0,0,0,.54);background-color:transparent}50%{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee);background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}}@keyframes mdc-checkbox-fade-in-background-8A000000primary00000000primary{0%{border-color:rgba(0,0,0,.54);background-color:transparent}50%{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee);background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}}@-webkit-keyframes mdc-checkbox-fade-out-background-8A000000primary00000000primary{0%,80%{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee);background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}100%{border-color:rgba(0,0,0,.54);background-color:transparent}}@keyframes mdc-checkbox-fade-out-background-8A000000primary00000000primary{0%,80%{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee);background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}100%{border-color:rgba(0,0,0,.54);background-color:transparent}}.mdc-data-table__header-row-checkbox.mdc-checkbox--anim-unchecked-checked .mdc-checkbox__native-control:enabled~.mdc-checkbox__background,.mdc-data-table__header-row-checkbox.mdc-checkbox--anim-unchecked-indeterminate .mdc-checkbox__native-control:enabled~.mdc-checkbox__background,.mdc-data-table__row-checkbox.mdc-checkbox--anim-unchecked-checked .mdc-checkbox__native-control:enabled~.mdc-checkbox__background,.mdc-data-table__row-checkbox.mdc-checkbox--anim-unchecked-indeterminate .mdc-checkbox__native-control:enabled~.mdc-checkbox__background{-webkit-animation-name:mdc-checkbox-fade-in-background-8A000000primary00000000primary;animation-name:mdc-checkbox-fade-in-background-8A000000primary00000000primary}.mdc-data-table__header-row-checkbox.mdc-checkbox--anim-checked-unchecked .mdc-checkbox__native-control:enabled~.mdc-checkbox__background,.mdc-data-table__header-row-checkbox.mdc-checkbox--anim-indeterminate-unchecked .mdc-checkbox__native-control:enabled~.mdc-checkbox__background,.mdc-data-table__row-checkbox.mdc-checkbox--anim-checked-unchecked .mdc-checkbox__native-control:enabled~.mdc-checkbox__background,.mdc-data-table__row-checkbox.mdc-checkbox--anim-indeterminate-unchecked .mdc-checkbox__native-control:enabled~.mdc-checkbox__background{-webkit-animation-name:mdc-checkbox-fade-out-background-8A000000primary00000000primary;animation-name:mdc-checkbox-fade-out-background-8A000000primary00000000primary}.mdc-dialog,.mdc-dialog__scrim{position:fixed;top:0;left:0;align-items:center;justify-content:center;box-sizing:border-box;width:100%;height:100%}.mdc-dialog{display:none;z-index:7}.mdc-dialog .mdc-dialog__surface{background-color:#fff;background-color:var(--mdc-theme-surface, #fff)}.mdc-dialog .mdc-dialog__scrim{background-color:rgba(0,0,0,.32)}.mdc-dialog .mdc-dialog__title{color:rgba(0,0,0,.87)}.mdc-dialog .mdc-dialog__content{color:rgba(0,0,0,.6)}.mdc-dialog.mdc-dialog--scrollable .mdc-dialog__title,.mdc-dialog.mdc-dialog--scrollable .mdc-dialog__actions{border-color:rgba(0,0,0,.12)}.mdc-dialog .mdc-dialog__surface{min-width:280px}@media(max-width: 592px){.mdc-dialog .mdc-dialog__surface{max-width:calc(100vw - 32px)}}@media(min-width: 592px){.mdc-dialog .mdc-dialog__surface{max-width:560px}}.mdc-dialog .mdc-dialog__surface{max-height:calc(100% - 32px)}.mdc-dialog .mdc-dialog__surface{border-radius:4px}.mdc-dialog__scrim{opacity:0;z-index:-1}.mdc-dialog__container{display:flex;flex-direction:row;align-items:center;justify-content:space-around;box-sizing:border-box;height:100%;-webkit-transform:scale(0.8);transform:scale(0.8);opacity:0;pointer-events:none}.mdc-dialog__surface{position:relative;box-shadow:0px 11px 15px -7px rgba(0, 0, 0, 0.2),0px 24px 38px 3px rgba(0, 0, 0, 0.14),0px 9px 46px 8px rgba(0,0,0,.12);display:flex;flex-direction:column;flex-grow:0;flex-shrink:0;box-sizing:border-box;max-width:100%;max-height:100%;pointer-events:auto;overflow-y:auto}.mdc-dialog__surface .mdc-elevation-overlay{width:100%;height:100%;top:0;left:0}.mdc-dialog[dir=rtl] .mdc-dialog__surface,[dir=rtl] .mdc-dialog .mdc-dialog__surface{text-align:right}.mdc-dialog__title{display:block;margin-top:0;line-height:normal;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-headline6-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1.25rem;font-size:var(--mdc-typography-headline6-font-size, 1.25rem);line-height:2rem;line-height:var(--mdc-typography-headline6-line-height, 2rem);font-weight:500;font-weight:var(--mdc-typography-headline6-font-weight, 500);letter-spacing:.0125em;letter-spacing:var(--mdc-typography-headline6-letter-spacing, 0.0125em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-headline6-text-decoration, inherit);text-decoration:var(--mdc-typography-headline6-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-headline6-text-transform, inherit);position:relative;flex-shrink:0;box-sizing:border-box;margin:0;padding:0 24px 9px;border-bottom:1px solid transparent}.mdc-dialog__title::before{display:inline-block;width:0;height:40px;content:"";vertical-align:0}.mdc-dialog[dir=rtl] .mdc-dialog__title,[dir=rtl] .mdc-dialog .mdc-dialog__title{text-align:right}.mdc-dialog--scrollable .mdc-dialog__title{padding-bottom:15px}.mdc-dialog__content{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-body1-font-size, 1rem);line-height:1.5rem;line-height:var(--mdc-typography-body1-line-height, 1.5rem);font-weight:400;font-weight:var(--mdc-typography-body1-font-weight, 400);letter-spacing:.03125em;letter-spacing:var(--mdc-typography-body1-letter-spacing, 0.03125em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-body1-text-decoration, inherit);text-decoration:var(--mdc-typography-body1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body1-text-transform, inherit);flex-grow:1;box-sizing:border-box;margin:0;padding:20px 24px;overflow:auto;-webkit-overflow-scrolling:touch}.mdc-dialog__content>:first-child{margin-top:0}.mdc-dialog__content>:last-child{margin-bottom:0}.mdc-dialog__title+.mdc-dialog__content{padding-top:0}.mdc-dialog--scrollable .mdc-dialog__content{padding-top:8px;padding-bottom:8px}.mdc-dialog__content .mdc-list:first-child:last-child{padding:6px 0 0}.mdc-dialog--scrollable .mdc-dialog__content .mdc-list:first-child:last-child{padding:0}.mdc-dialog__actions{display:flex;position:relative;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;box-sizing:border-box;min-height:52px;margin:0;padding:8px;border-top:1px solid transparent}.mdc-dialog--stacked .mdc-dialog__actions{flex-direction:column;align-items:flex-end}.mdc-dialog__button{margin-left:8px;margin-right:0;max-width:100%;text-align:right}[dir=rtl] .mdc-dialog__button,.mdc-dialog__button[dir=rtl]{margin-left:0;margin-right:8px}.mdc-dialog__button:first-child{margin-left:0;margin-right:0}[dir=rtl] .mdc-dialog__button:first-child,.mdc-dialog__button:first-child[dir=rtl]{margin-left:0;margin-right:0}.mdc-dialog[dir=rtl] .mdc-dialog__button,[dir=rtl] .mdc-dialog .mdc-dialog__button{text-align:left}.mdc-dialog--stacked .mdc-dialog__button:not(:first-child){margin-top:12px}.mdc-dialog--open,.mdc-dialog--opening,.mdc-dialog--closing{display:flex}.mdc-dialog--opening .mdc-dialog__scrim{transition:opacity 150ms linear}.mdc-dialog--opening .mdc-dialog__container{transition:opacity 75ms linear,-webkit-transform 150ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:opacity 75ms linear,transform 150ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:opacity 75ms linear,transform 150ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 150ms 0ms cubic-bezier(0, 0, 0.2, 1)}.mdc-dialog--closing .mdc-dialog__scrim,.mdc-dialog--closing .mdc-dialog__container{transition:opacity 75ms linear}.mdc-dialog--closing .mdc-dialog__container{-webkit-transform:scale(1);transform:scale(1)}.mdc-dialog--open .mdc-dialog__scrim{opacity:1}.mdc-dialog--open .mdc-dialog__container{-webkit-transform:scale(1);transform:scale(1);opacity:1}.mdc-dialog-scroll-lock{overflow:hidden}.mdc-drawer{border-color:rgba(0,0,0,.12);background-color:#fff;border-radius:0 0 0 0;z-index:6;width:256px;display:flex;flex-direction:column;flex-shrink:0;box-sizing:border-box;height:100%;border-right-width:1px;border-right-style:solid;overflow:hidden;transition-property:-webkit-transform;transition-property:transform;transition-property:transform, -webkit-transform;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1)}.mdc-drawer .mdc-drawer__title{color:rgba(0,0,0,.87)}.mdc-drawer .mdc-list-group__subheader{color:rgba(0,0,0,.6)}.mdc-drawer .mdc-drawer__subtitle{color:rgba(0,0,0,.6)}.mdc-drawer .mdc-list-item__graphic{color:rgba(0,0,0,.6)}.mdc-drawer .mdc-list-item{color:rgba(0,0,0,.87)}.mdc-drawer .mdc-list-item--activated .mdc-list-item__graphic{color:#6200ee}.mdc-drawer .mdc-list-item--activated{color:rgba(98,0,238,.87)}[dir=rtl] .mdc-drawer,.mdc-drawer[dir=rtl]{border-radius:0 0 0 0}.mdc-drawer .mdc-list-item{border-radius:4px}.mdc-drawer.mdc-drawer--open:not(.mdc-drawer--closing)+.mdc-drawer-app-content{margin-left:256px;margin-right:0}[dir=rtl] .mdc-drawer.mdc-drawer--open:not(.mdc-drawer--closing)+.mdc-drawer-app-content,.mdc-drawer.mdc-drawer--open:not(.mdc-drawer--closing)+.mdc-drawer-app-content[dir=rtl]{margin-left:0;margin-right:256px}[dir=rtl] .mdc-drawer,.mdc-drawer[dir=rtl]{border-right-width:0;border-left-width:1px;border-right-style:none;border-left-style:solid}.mdc-drawer .mdc-list-item{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-subtitle2-font-size, 0.875rem);line-height:1.375rem;line-height:var(--mdc-typography-subtitle2-line-height, 1.375rem);font-weight:500;font-weight:var(--mdc-typography-subtitle2-font-weight, 500);letter-spacing:.0071428571em;letter-spacing:var(--mdc-typography-subtitle2-letter-spacing, 0.0071428571em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-subtitle2-text-decoration, inherit);text-decoration:var(--mdc-typography-subtitle2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle2-text-transform, inherit);height:calc(48px - 2 * 4px);margin:8px 8px;padding:0 8px}.mdc-drawer .mdc-list-item:nth-child(1){margin-top:2px}.mdc-drawer .mdc-list-item:nth-last-child(1){margin-bottom:0}.mdc-drawer .mdc-list-group__subheader{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);display:block;margin-top:0;line-height:normal;margin:0;padding:0 16px}.mdc-drawer .mdc-list-group__subheader::before{display:inline-block;width:0;height:24px;content:"";vertical-align:0}.mdc-drawer .mdc-list-divider{margin:3px 0 4px}.mdc-drawer .mdc-list-item__text,.mdc-drawer .mdc-list-item__graphic{pointer-events:none}.mdc-drawer--animate{-webkit-transform:translateX(-100%);transform:translateX(-100%)}[dir=rtl] .mdc-drawer--animate,.mdc-drawer--animate[dir=rtl]{-webkit-transform:translateX(100%);transform:translateX(100%)}.mdc-drawer--opening{-webkit-transform:translateX(0);transform:translateX(0);transition-duration:250ms}[dir=rtl] .mdc-drawer--opening,.mdc-drawer--opening[dir=rtl]{-webkit-transform:translateX(0);transform:translateX(0)}.mdc-drawer--closing{-webkit-transform:translateX(-100%);transform:translateX(-100%);transition-duration:200ms}[dir=rtl] .mdc-drawer--closing,.mdc-drawer--closing[dir=rtl]{-webkit-transform:translateX(100%);transform:translateX(100%)}.mdc-drawer__header{flex-shrink:0;box-sizing:border-box;min-height:64px;padding:0 16px 4px}.mdc-drawer__title{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-headline6-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1.25rem;font-size:var(--mdc-typography-headline6-font-size, 1.25rem);line-height:2rem;line-height:var(--mdc-typography-headline6-line-height, 2rem);font-weight:500;font-weight:var(--mdc-typography-headline6-font-weight, 500);letter-spacing:.0125em;letter-spacing:var(--mdc-typography-headline6-letter-spacing, 0.0125em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-headline6-text-decoration, inherit);text-decoration:var(--mdc-typography-headline6-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-headline6-text-transform, inherit);display:block;margin-top:0;line-height:normal;margin-bottom:-20px}.mdc-drawer__title::before{display:inline-block;width:0;height:36px;content:"";vertical-align:0}.mdc-drawer__title::after{display:inline-block;width:0;height:20px;content:"";vertical-align:-20px}.mdc-drawer__subtitle{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);display:block;margin-top:0;line-height:normal;margin-bottom:0}.mdc-drawer__subtitle::before{display:inline-block;width:0;height:20px;content:"";vertical-align:0}.mdc-drawer__content{height:100%;overflow-y:auto;-webkit-overflow-scrolling:touch}.mdc-drawer--dismissible{left:0;right:initial;display:none;position:absolute}[dir=rtl] .mdc-drawer--dismissible,.mdc-drawer--dismissible[dir=rtl]{left:initial;right:0}.mdc-drawer--dismissible.mdc-drawer--open{display:flex}.mdc-drawer-app-content{margin-left:0;margin-right:0;position:relative}[dir=rtl] .mdc-drawer-app-content,.mdc-drawer-app-content[dir=rtl]{margin-left:0;margin-right:0}.mdc-drawer--modal{box-shadow:0px 8px 10px -5px rgba(0, 0, 0, 0.2),0px 16px 24px 2px rgba(0, 0, 0, 0.14),0px 6px 30px 5px rgba(0,0,0,.12);left:0;right:initial;display:none;position:fixed}.mdc-drawer--modal+.mdc-drawer-scrim{background-color:rgba(0,0,0,.32)}[dir=rtl] .mdc-drawer--modal,.mdc-drawer--modal[dir=rtl]{left:initial;right:0}.mdc-drawer--modal.mdc-drawer--open{display:flex}.mdc-drawer-scrim{display:none;position:fixed;top:0;left:0;width:100%;height:100%;z-index:5;transition-property:opacity;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1)}.mdc-drawer--open+.mdc-drawer-scrim{display:block}.mdc-drawer--animate+.mdc-drawer-scrim{opacity:0}.mdc-drawer--opening+.mdc-drawer-scrim{transition-duration:250ms;opacity:1}.mdc-drawer--closing+.mdc-drawer-scrim{transition-duration:200ms;opacity:0}.mdc-elevation--z0{box-shadow:0px 0px 0px 0px rgba(0, 0, 0, 0.2),0px 0px 0px 0px rgba(0, 0, 0, 0.14),0px 0px 0px 0px rgba(0,0,0,.12)}.mdc-elevation--z1{box-shadow:0px 2px 1px -1px rgba(0, 0, 0, 0.2),0px 1px 1px 0px rgba(0, 0, 0, 0.14),0px 1px 3px 0px rgba(0,0,0,.12)}.mdc-elevation--z2{box-shadow:0px 3px 1px -2px rgba(0, 0, 0, 0.2),0px 2px 2px 0px rgba(0, 0, 0, 0.14),0px 1px 5px 0px rgba(0,0,0,.12)}.mdc-elevation--z3{box-shadow:0px 3px 3px -2px rgba(0, 0, 0, 0.2),0px 3px 4px 0px rgba(0, 0, 0, 0.14),0px 1px 8px 0px rgba(0,0,0,.12)}.mdc-elevation--z4{box-shadow:0px 2px 4px -1px rgba(0, 0, 0, 0.2),0px 4px 5px 0px rgba(0, 0, 0, 0.14),0px 1px 10px 0px rgba(0,0,0,.12)}.mdc-elevation--z5{box-shadow:0px 3px 5px -1px rgba(0, 0, 0, 0.2),0px 5px 8px 0px rgba(0, 0, 0, 0.14),0px 1px 14px 0px rgba(0,0,0,.12)}.mdc-elevation--z6{box-shadow:0px 3px 5px -1px rgba(0, 0, 0, 0.2),0px 6px 10px 0px rgba(0, 0, 0, 0.14),0px 1px 18px 0px rgba(0,0,0,.12)}.mdc-elevation--z7{box-shadow:0px 4px 5px -2px rgba(0, 0, 0, 0.2),0px 7px 10px 1px rgba(0, 0, 0, 0.14),0px 2px 16px 1px rgba(0,0,0,.12)}.mdc-elevation--z8{box-shadow:0px 5px 5px -3px rgba(0, 0, 0, 0.2),0px 8px 10px 1px rgba(0, 0, 0, 0.14),0px 3px 14px 2px rgba(0,0,0,.12)}.mdc-elevation--z9{box-shadow:0px 5px 6px -3px rgba(0, 0, 0, 0.2),0px 9px 12px 1px rgba(0, 0, 0, 0.14),0px 3px 16px 2px rgba(0,0,0,.12)}.mdc-elevation--z10{box-shadow:0px 6px 6px -3px rgba(0, 0, 0, 0.2),0px 10px 14px 1px rgba(0, 0, 0, 0.14),0px 4px 18px 3px rgba(0,0,0,.12)}.mdc-elevation--z11{box-shadow:0px 6px 7px -4px rgba(0, 0, 0, 0.2),0px 11px 15px 1px rgba(0, 0, 0, 0.14),0px 4px 20px 3px rgba(0,0,0,.12)}.mdc-elevation--z12{box-shadow:0px 7px 8px -4px rgba(0, 0, 0, 0.2),0px 12px 17px 2px rgba(0, 0, 0, 0.14),0px 5px 22px 4px rgba(0,0,0,.12)}.mdc-elevation--z13{box-shadow:0px 7px 8px -4px rgba(0, 0, 0, 0.2),0px 13px 19px 2px rgba(0, 0, 0, 0.14),0px 5px 24px 4px rgba(0,0,0,.12)}.mdc-elevation--z14{box-shadow:0px 7px 9px -4px rgba(0, 0, 0, 0.2),0px 14px 21px 2px rgba(0, 0, 0, 0.14),0px 5px 26px 4px rgba(0,0,0,.12)}.mdc-elevation--z15{box-shadow:0px 8px 9px -5px rgba(0, 0, 0, 0.2),0px 15px 22px 2px rgba(0, 0, 0, 0.14),0px 6px 28px 5px rgba(0,0,0,.12)}.mdc-elevation--z16{box-shadow:0px 8px 10px -5px rgba(0, 0, 0, 0.2),0px 16px 24px 2px rgba(0, 0, 0, 0.14),0px 6px 30px 5px rgba(0,0,0,.12)}.mdc-elevation--z17{box-shadow:0px 8px 11px -5px rgba(0, 0, 0, 0.2),0px 17px 26px 2px rgba(0, 0, 0, 0.14),0px 6px 32px 5px rgba(0,0,0,.12)}.mdc-elevation--z18{box-shadow:0px 9px 11px -5px rgba(0, 0, 0, 0.2),0px 18px 28px 2px rgba(0, 0, 0, 0.14),0px 7px 34px 6px rgba(0,0,0,.12)}.mdc-elevation--z19{box-shadow:0px 9px 12px -6px rgba(0, 0, 0, 0.2),0px 19px 29px 2px rgba(0, 0, 0, 0.14),0px 7px 36px 6px rgba(0,0,0,.12)}.mdc-elevation--z20{box-shadow:0px 10px 13px -6px rgba(0, 0, 0, 0.2),0px 20px 31px 3px rgba(0, 0, 0, 0.14),0px 8px 38px 7px rgba(0,0,0,.12)}.mdc-elevation--z21{box-shadow:0px 10px 13px -6px rgba(0, 0, 0, 0.2),0px 21px 33px 3px rgba(0, 0, 0, 0.14),0px 8px 40px 7px rgba(0,0,0,.12)}.mdc-elevation--z22{box-shadow:0px 10px 14px -6px rgba(0, 0, 0, 0.2),0px 22px 35px 3px rgba(0, 0, 0, 0.14),0px 8px 42px 7px rgba(0,0,0,.12)}.mdc-elevation--z23{box-shadow:0px 11px 14px -7px rgba(0, 0, 0, 0.2),0px 23px 36px 3px rgba(0, 0, 0, 0.14),0px 9px 44px 8px rgba(0,0,0,.12)}.mdc-elevation--z24{box-shadow:0px 11px 15px -7px rgba(0, 0, 0, 0.2),0px 24px 38px 3px rgba(0, 0, 0, 0.14),0px 9px 46px 8px rgba(0,0,0,.12)}.mdc-elevation-transition{transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);will-change:box-shadow}.mdc-fab{position:relative;box-shadow:0px 3px 5px -1px rgba(0, 0, 0, 0.2),0px 6px 10px 0px rgba(0, 0, 0, 0.14),0px 1px 18px 0px rgba(0,0,0,.12);display:inline-flex;position:relative;align-items:center;justify-content:center;box-sizing:border-box;width:56px;height:56px;padding:0;border:none;fill:currentColor;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-moz-appearance:none;-webkit-appearance:none;overflow:visible;transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1),opacity 15ms linear 30ms,-webkit-transform 270ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1),opacity 15ms linear 30ms,transform 270ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1),opacity 15ms linear 30ms,transform 270ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 270ms 0ms cubic-bezier(0, 0, 0.2, 1);background-color:#018786;background-color:var(--mdc-theme-secondary, #018786);color:#fff;color:var(--mdc-theme-on-secondary, #fff)}.mdc-fab .mdc-elevation-overlay{width:100%;height:100%;top:0;left:0}.mdc-fab:not(.mdc-fab--extended){border-radius:50%}.mdc-fab:not(.mdc-fab--extended) .mdc-fab__ripple{border-radius:50%}.mdc-fab::-moz-focus-inner{padding:0;border:0}.mdc-fab:hover,.mdc-fab:focus{box-shadow:0px 5px 5px -3px rgba(0, 0, 0, 0.2),0px 8px 10px 1px rgba(0, 0, 0, 0.14),0px 3px 14px 2px rgba(0,0,0,.12)}.mdc-fab:active{box-shadow:0px 7px 8px -4px rgba(0, 0, 0, 0.2),0px 12px 17px 2px rgba(0, 0, 0, 0.14),0px 5px 22px 4px rgba(0,0,0,.12)}.mdc-fab:active,.mdc-fab:focus{outline:none}.mdc-fab:hover{cursor:pointer}.mdc-fab>svg{width:100%}.mdc-fab .mdc-fab__icon{width:24px;height:24px;font-size:24px}.mdc-fab--mini{width:40px;height:40px}.mdc-fab--extended{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-button-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-button-font-size, 0.875rem);line-height:2.25rem;line-height:var(--mdc-typography-button-line-height, 2.25rem);font-weight:500;font-weight:var(--mdc-typography-button-font-weight, 500);letter-spacing:.0892857143em;letter-spacing:var(--mdc-typography-button-letter-spacing, 0.0892857143em);text-decoration:none;-webkit-text-decoration:var(--mdc-typography-button-text-decoration, none);text-decoration:var(--mdc-typography-button-text-decoration, none);text-transform:uppercase;text-transform:var(--mdc-typography-button-text-transform, uppercase);border-radius:24px;padding:0 20px;width:auto;max-width:100%;height:48px;line-height:normal}.mdc-fab--extended .mdc-fab__ripple{border-radius:24px}.mdc-fab--extended .mdc-fab__icon{margin-left:-8px;margin-right:12px}[dir=rtl] .mdc-fab--extended .mdc-fab__icon,.mdc-fab--extended .mdc-fab__icon[dir=rtl]{margin-left:12px;margin-right:-8px}.mdc-fab--extended .mdc-fab__label+.mdc-fab__icon{margin-left:12px;margin-right:-8px}[dir=rtl] .mdc-fab--extended .mdc-fab__label+.mdc-fab__icon,.mdc-fab--extended .mdc-fab__label+.mdc-fab__icon[dir=rtl]{margin-left:-8px;margin-right:12px}.mdc-fab--touch{margin-top:4px;margin-bottom:4px;margin-right:4px;margin-left:4px}.mdc-fab--touch .mdc-fab__touch{position:absolute;top:50%;right:0;height:48px;left:50%;width:48px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%)}.mdc-fab::before{position:absolute;box-sizing:border-box;width:100%;height:100%;top:0;left:0;border:1px solid transparent;border-radius:inherit;content:""}.mdc-fab__label{justify-content:flex-start;text-overflow:ellipsis;white-space:nowrap;overflow-x:hidden;overflow-y:visible}.mdc-fab__icon{transition:-webkit-transform 180ms 90ms cubic-bezier(0, 0, 0.2, 1);transition:transform 180ms 90ms cubic-bezier(0, 0, 0.2, 1);transition:transform 180ms 90ms cubic-bezier(0, 0, 0.2, 1), -webkit-transform 180ms 90ms cubic-bezier(0, 0, 0.2, 1);fill:currentColor;will-change:transform}.mdc-fab .mdc-fab__icon{display:inline-flex;align-items:center;justify-content:center}.mdc-fab--exited{-webkit-transform:scale(0);transform:scale(0);opacity:0;transition:opacity 15ms linear 150ms,-webkit-transform 180ms 0ms cubic-bezier(0.4, 0, 1, 1);transition:opacity 15ms linear 150ms,transform 180ms 0ms cubic-bezier(0.4, 0, 1, 1);transition:opacity 15ms linear 150ms,transform 180ms 0ms cubic-bezier(0.4, 0, 1, 1),-webkit-transform 180ms 0ms cubic-bezier(0.4, 0, 1, 1)}.mdc-fab--exited .mdc-fab__icon{-webkit-transform:scale(0);transform:scale(0);transition:-webkit-transform 135ms 0ms cubic-bezier(0.4, 0, 1, 1);transition:transform 135ms 0ms cubic-bezier(0.4, 0, 1, 1);transition:transform 135ms 0ms cubic-bezier(0.4, 0, 1, 1), -webkit-transform 135ms 0ms cubic-bezier(0.4, 0, 1, 1)}.mdc-fab{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mdc-fab .mdc-fab__ripple::before,.mdc-fab .mdc-fab__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-fab .mdc-fab__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-fab.mdc-ripple-upgraded .mdc-fab__ripple::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-fab.mdc-ripple-upgraded .mdc-fab__ripple::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}.mdc-fab.mdc-ripple-upgraded--unbounded .mdc-fab__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-fab.mdc-ripple-upgraded--foreground-activation .mdc-fab__ripple::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-fab.mdc-ripple-upgraded--foreground-deactivation .mdc-fab__ripple::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-fab .mdc-fab__ripple::before,.mdc-fab .mdc-fab__ripple::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-fab.mdc-ripple-upgraded .mdc-fab__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-fab .mdc-fab__ripple::before,.mdc-fab .mdc-fab__ripple::after{background-color:#fff;background-color:var(--mdc-theme-on-secondary, #fff)}.mdc-fab:hover .mdc-fab__ripple::before{opacity:.08}.mdc-fab.mdc-ripple-upgraded--background-focused .mdc-fab__ripple::before,.mdc-fab:not(.mdc-ripple-upgraded):focus .mdc-fab__ripple::before{transition-duration:75ms;opacity:.24}.mdc-fab:not(.mdc-ripple-upgraded) .mdc-fab__ripple::after{transition:opacity 150ms linear}.mdc-fab:not(.mdc-ripple-upgraded):active .mdc-fab__ripple::after{transition-duration:75ms;opacity:.24}.mdc-fab.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.24}.mdc-fab .mdc-fab__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden}.mdc-floating-label{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);position:absolute;left:0;-webkit-transform-origin:left top;transform-origin:left top;line-height:1.15rem;text-align:left;text-overflow:ellipsis;white-space:nowrap;cursor:text;overflow:hidden;will-change:transform;transition:color 150ms cubic-bezier(0.4, 0, 0.2, 1),-webkit-transform 150ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 150ms cubic-bezier(0.4, 0, 0.2, 1),color 150ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 150ms cubic-bezier(0.4, 0, 0.2, 1),color 150ms cubic-bezier(0.4, 0, 0.2, 1),-webkit-transform 150ms cubic-bezier(0.4, 0, 0.2, 1)}[dir=rtl] .mdc-floating-label,.mdc-floating-label[dir=rtl]{right:0;left:auto;-webkit-transform-origin:right top;transform-origin:right top;text-align:right}.mdc-floating-label--float-above{cursor:auto}.mdc-floating-label--float-above{-webkit-transform:translateY(-106%) scale(0.75);transform:translateY(-106%) scale(0.75)}.mdc-floating-label--shake{-webkit-animation:mdc-floating-label-shake-float-above-standard 250ms 1;animation:mdc-floating-label-shake-float-above-standard 250ms 1}@-webkit-keyframes mdc-floating-label-shake-float-above-standard{0%{-webkit-transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75);transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - 0%)) translateY(-106%) scale(0.75);transform:translateX(calc(4% - 0%)) translateY(-106%) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - 0%)) translateY(-106%) scale(0.75);transform:translateX(calc(-4% - 0%)) translateY(-106%) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75);transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}}@keyframes mdc-floating-label-shake-float-above-standard{0%{-webkit-transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75);transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - 0%)) translateY(-106%) scale(0.75);transform:translateX(calc(4% - 0%)) translateY(-106%) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - 0%)) translateY(-106%) scale(0.75);transform:translateX(calc(-4% - 0%)) translateY(-106%) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75);transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}}.mdc-form-field{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);color:rgba(0,0,0,.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));display:inline-flex;align-items:center;vertical-align:middle}.mdc-form-field>label{margin-left:0;margin-right:auto;padding-left:4px;padding-right:0;order:0}[dir=rtl] .mdc-form-field>label,.mdc-form-field>label[dir=rtl]{margin-left:auto;margin-right:0}[dir=rtl] .mdc-form-field>label,.mdc-form-field>label[dir=rtl]{padding-left:0;padding-right:4px}.mdc-form-field--nowrap>label{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.mdc-form-field--align-end>label{margin-left:auto;margin-right:0;padding-left:0;padding-right:4px;order:-1}[dir=rtl] .mdc-form-field--align-end>label,.mdc-form-field--align-end>label[dir=rtl]{margin-left:0;margin-right:auto}[dir=rtl] .mdc-form-field--align-end>label,.mdc-form-field--align-end>label[dir=rtl]{padding-left:4px;padding-right:0}.mdc-form-field--space-between{justify-content:space-between}.mdc-form-field--space-between>label{margin:0}[dir=rtl] .mdc-form-field--space-between>label,.mdc-form-field--space-between>label[dir=rtl]{margin:0}.mdc-icon-button{display:inline-block;position:relative;box-sizing:border-box;border:none;outline:none;background-color:transparent;fill:currentColor;color:inherit;font-size:24px;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:48px;height:48px;padding:12px}.mdc-icon-button svg,.mdc-icon-button img{width:24px;height:24px}.mdc-icon-button:disabled{color:rgba(0,0,0,.38);color:var(--mdc-theme-text-disabled-on-light, rgba(0, 0, 0, 0.38))}.mdc-icon-button:disabled{cursor:default;pointer-events:none}.mdc-icon-button__icon{display:inline-block}.mdc-icon-button__icon.mdc-icon-button__icon--on{display:none}.mdc-icon-button--on .mdc-icon-button__icon{display:none}.mdc-icon-button--on .mdc-icon-button__icon.mdc-icon-button__icon--on{display:inline-block}.mdc-icon-button{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mdc-icon-button::before,.mdc-icon-button::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-icon-button::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-icon-button.mdc-ripple-upgraded::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-icon-button.mdc-ripple-upgraded::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}.mdc-icon-button.mdc-ripple-upgraded--unbounded::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-icon-button.mdc-ripple-upgraded--foreground-activation::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-icon-button.mdc-ripple-upgraded--foreground-deactivation::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-icon-button::before,.mdc-icon-button::after{top:calc(50% - 50%);left:calc(50% - 50%);width:100%;height:100%}.mdc-icon-button.mdc-ripple-upgraded::before,.mdc-icon-button.mdc-ripple-upgraded::after{top:var(--mdc-ripple-top, calc(50% - 50%));left:var(--mdc-ripple-left, calc(50% - 50%));width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-icon-button.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-icon-button::before,.mdc-icon-button::after{background-color:#000}.mdc-icon-button:hover::before{opacity:.04}.mdc-icon-button.mdc-ripple-upgraded--background-focused::before,.mdc-icon-button:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.12}.mdc-icon-button:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-icon-button:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.12}.mdc-icon-button.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-image-list{display:flex;flex-wrap:wrap;margin:0 auto;padding:0}.mdc-image-list__item,.mdc-image-list__image-aspect-container{position:relative;box-sizing:border-box}.mdc-image-list__item{list-style-type:none}.mdc-image-list__image{width:100%}.mdc-image-list__image-aspect-container .mdc-image-list__image{position:absolute;top:0;right:0;bottom:0;left:0;height:100%;background-repeat:no-repeat;background-position:center;background-size:cover}.mdc-image-list__image-aspect-container{padding-bottom:calc(100% / 1)}.mdc-image-list__image{border-radius:0}.mdc-image-list--with-text-protection .mdc-image-list__supporting{border-radius:0 0 0 0}.mdc-image-list__supporting{color:rgba(0,0,0,.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));display:flex;align-items:center;justify-content:space-between;box-sizing:border-box;padding:8px 0;line-height:24px}.mdc-image-list__label{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);line-height:1.75rem;line-height:var(--mdc-typography-subtitle1-line-height, 1.75rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.mdc-image-list--with-text-protection .mdc-image-list__supporting{position:absolute;bottom:0;width:100%;height:48px;padding:0 16px;background:rgba(0,0,0,.6);color:#fff}.mdc-image-list--masonry{display:block}.mdc-image-list--masonry .mdc-image-list__item{-webkit-column-break-inside:avoid;break-inside:avoid-column}.mdc-image-list--masonry .mdc-image-list__image{display:block;height:auto}:root{--mdc-layout-grid-margin-desktop: 24px;--mdc-layout-grid-gutter-desktop: 24px;--mdc-layout-grid-column-width-desktop: 72px;--mdc-layout-grid-margin-tablet: 16px;--mdc-layout-grid-gutter-tablet: 16px;--mdc-layout-grid-column-width-tablet: 72px;--mdc-layout-grid-margin-phone: 16px;--mdc-layout-grid-gutter-phone: 16px;--mdc-layout-grid-column-width-phone: 72px}@media(min-width: 840px){.mdc-layout-grid{box-sizing:border-box;margin:0 auto;padding:24px;padding:var(--mdc-layout-grid-margin-desktop, 24px)}}@media(min-width: 600px)and (max-width: 839px){.mdc-layout-grid{box-sizing:border-box;margin:0 auto;padding:16px;padding:var(--mdc-layout-grid-margin-tablet, 16px)}}@media(max-width: 599px){.mdc-layout-grid{box-sizing:border-box;margin:0 auto;padding:16px;padding:var(--mdc-layout-grid-margin-phone, 16px)}}@media(min-width: 840px){.mdc-layout-grid__inner{display:flex;flex-flow:row wrap;align-items:stretch;margin:-12px;margin:calc(var(--mdc-layout-grid-gutter-desktop, 24px) / 2 * -1)}@supports(display: grid){.mdc-layout-grid__inner{display:grid;margin:0;grid-gap:24px;grid-gap:var(--mdc-layout-grid-gutter-desktop, 24px);grid-template-columns:repeat(12, minmax(0, 1fr))}}}@media(min-width: 600px)and (max-width: 839px){.mdc-layout-grid__inner{display:flex;flex-flow:row wrap;align-items:stretch;margin:-8px;margin:calc(var(--mdc-layout-grid-gutter-tablet, 16px) / 2 * -1)}@supports(display: grid){.mdc-layout-grid__inner{display:grid;margin:0;grid-gap:16px;grid-gap:var(--mdc-layout-grid-gutter-tablet, 16px);grid-template-columns:repeat(8, minmax(0, 1fr))}}}@media(max-width: 599px){.mdc-layout-grid__inner{display:flex;flex-flow:row wrap;align-items:stretch;margin:-8px;margin:calc(var(--mdc-layout-grid-gutter-phone, 16px) / 2 * -1)}@supports(display: grid){.mdc-layout-grid__inner{display:grid;margin:0;grid-gap:16px;grid-gap:var(--mdc-layout-grid-gutter-phone, 16px);grid-template-columns:repeat(4, minmax(0, 1fr))}}}@media(min-width: 840px){.mdc-layout-grid__cell{width:calc(33.3333333333% - 24px);width:calc(33.3333333333% - var(--mdc-layout-grid-gutter-desktop, 24px));box-sizing:border-box;margin:12px;margin:calc(var(--mdc-layout-grid-gutter-desktop, 24px) / 2)}@supports(display: grid){.mdc-layout-grid__cell{width:auto;grid-column-end:span 4}}@supports(display: grid){.mdc-layout-grid__cell{margin:0}}.mdc-layout-grid__cell--span-1,.mdc-layout-grid__cell--span-1-desktop{width:calc(8.3333333333% - 24px);width:calc(8.3333333333% - var(--mdc-layout-grid-gutter-desktop, 24px))}@supports(display: grid){.mdc-layout-grid__cell--span-1,.mdc-layout-grid__cell--span-1-desktop{width:auto;grid-column-end:span 1}}.mdc-layout-grid__cell--span-2,.mdc-layout-grid__cell--span-2-desktop{width:calc(16.6666666667% - 24px);width:calc(16.6666666667% - var(--mdc-layout-grid-gutter-desktop, 24px))}@supports(display: grid){.mdc-layout-grid__cell--span-2,.mdc-layout-grid__cell--span-2-desktop{width:auto;grid-column-end:span 2}}.mdc-layout-grid__cell--span-3,.mdc-layout-grid__cell--span-3-desktop{width:calc(25% - 24px);width:calc(25% - var(--mdc-layout-grid-gutter-desktop, 24px))}@supports(display: grid){.mdc-layout-grid__cell--span-3,.mdc-layout-grid__cell--span-3-desktop{width:auto;grid-column-end:span 3}}.mdc-layout-grid__cell--span-4,.mdc-layout-grid__cell--span-4-desktop{width:calc(33.3333333333% - 24px);width:calc(33.3333333333% - var(--mdc-layout-grid-gutter-desktop, 24px))}@supports(display: grid){.mdc-layout-grid__cell--span-4,.mdc-layout-grid__cell--span-4-desktop{width:auto;grid-column-end:span 4}}.mdc-layout-grid__cell--span-5,.mdc-layout-grid__cell--span-5-desktop{width:calc(41.6666666667% - 24px);width:calc(41.6666666667% - var(--mdc-layout-grid-gutter-desktop, 24px))}@supports(display: grid){.mdc-layout-grid__cell--span-5,.mdc-layout-grid__cell--span-5-desktop{width:auto;grid-column-end:span 5}}.mdc-layout-grid__cell--span-6,.mdc-layout-grid__cell--span-6-desktop{width:calc(50% - 24px);width:calc(50% - var(--mdc-layout-grid-gutter-desktop, 24px))}@supports(display: grid){.mdc-layout-grid__cell--span-6,.mdc-layout-grid__cell--span-6-desktop{width:auto;grid-column-end:span 6}}.mdc-layout-grid__cell--span-7,.mdc-layout-grid__cell--span-7-desktop{width:calc(58.3333333333% - 24px);width:calc(58.3333333333% - var(--mdc-layout-grid-gutter-desktop, 24px))}@supports(display: grid){.mdc-layout-grid__cell--span-7,.mdc-layout-grid__cell--span-7-desktop{width:auto;grid-column-end:span 7}}.mdc-layout-grid__cell--span-8,.mdc-layout-grid__cell--span-8-desktop{width:calc(66.6666666667% - 24px);width:calc(66.6666666667% - var(--mdc-layout-grid-gutter-desktop, 24px))}@supports(display: grid){.mdc-layout-grid__cell--span-8,.mdc-layout-grid__cell--span-8-desktop{width:auto;grid-column-end:span 8}}.mdc-layout-grid__cell--span-9,.mdc-layout-grid__cell--span-9-desktop{width:calc(75% - 24px);width:calc(75% - var(--mdc-layout-grid-gutter-desktop, 24px))}@supports(display: grid){.mdc-layout-grid__cell--span-9,.mdc-layout-grid__cell--span-9-desktop{width:auto;grid-column-end:span 9}}.mdc-layout-grid__cell--span-10,.mdc-layout-grid__cell--span-10-desktop{width:calc(83.3333333333% - 24px);width:calc(83.3333333333% - var(--mdc-layout-grid-gutter-desktop, 24px))}@supports(display: grid){.mdc-layout-grid__cell--span-10,.mdc-layout-grid__cell--span-10-desktop{width:auto;grid-column-end:span 10}}.mdc-layout-grid__cell--span-11,.mdc-layout-grid__cell--span-11-desktop{width:calc(91.6666666667% - 24px);width:calc(91.6666666667% - var(--mdc-layout-grid-gutter-desktop, 24px))}@supports(display: grid){.mdc-layout-grid__cell--span-11,.mdc-layout-grid__cell--span-11-desktop{width:auto;grid-column-end:span 11}}.mdc-layout-grid__cell--span-12,.mdc-layout-grid__cell--span-12-desktop{width:calc(100% - 24px);width:calc(100% - var(--mdc-layout-grid-gutter-desktop, 24px))}@supports(display: grid){.mdc-layout-grid__cell--span-12,.mdc-layout-grid__cell--span-12-desktop{width:auto;grid-column-end:span 12}}}@media(min-width: 600px)and (max-width: 839px){.mdc-layout-grid__cell{width:calc(50% - 16px);width:calc(50% - var(--mdc-layout-grid-gutter-tablet, 16px));box-sizing:border-box;margin:8px;margin:calc(var(--mdc-layout-grid-gutter-tablet, 16px) / 2)}@supports(display: grid){.mdc-layout-grid__cell{width:auto;grid-column-end:span 4}}@supports(display: grid){.mdc-layout-grid__cell{margin:0}}.mdc-layout-grid__cell--span-1,.mdc-layout-grid__cell--span-1-tablet{width:calc(12.5% - 16px);width:calc(12.5% - var(--mdc-layout-grid-gutter-tablet, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-1,.mdc-layout-grid__cell--span-1-tablet{width:auto;grid-column-end:span 1}}.mdc-layout-grid__cell--span-2,.mdc-layout-grid__cell--span-2-tablet{width:calc(25% - 16px);width:calc(25% - var(--mdc-layout-grid-gutter-tablet, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-2,.mdc-layout-grid__cell--span-2-tablet{width:auto;grid-column-end:span 2}}.mdc-layout-grid__cell--span-3,.mdc-layout-grid__cell--span-3-tablet{width:calc(37.5% - 16px);width:calc(37.5% - var(--mdc-layout-grid-gutter-tablet, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-3,.mdc-layout-grid__cell--span-3-tablet{width:auto;grid-column-end:span 3}}.mdc-layout-grid__cell--span-4,.mdc-layout-grid__cell--span-4-tablet{width:calc(50% - 16px);width:calc(50% - var(--mdc-layout-grid-gutter-tablet, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-4,.mdc-layout-grid__cell--span-4-tablet{width:auto;grid-column-end:span 4}}.mdc-layout-grid__cell--span-5,.mdc-layout-grid__cell--span-5-tablet{width:calc(62.5% - 16px);width:calc(62.5% - var(--mdc-layout-grid-gutter-tablet, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-5,.mdc-layout-grid__cell--span-5-tablet{width:auto;grid-column-end:span 5}}.mdc-layout-grid__cell--span-6,.mdc-layout-grid__cell--span-6-tablet{width:calc(75% - 16px);width:calc(75% - var(--mdc-layout-grid-gutter-tablet, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-6,.mdc-layout-grid__cell--span-6-tablet{width:auto;grid-column-end:span 6}}.mdc-layout-grid__cell--span-7,.mdc-layout-grid__cell--span-7-tablet{width:calc(87.5% - 16px);width:calc(87.5% - var(--mdc-layout-grid-gutter-tablet, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-7,.mdc-layout-grid__cell--span-7-tablet{width:auto;grid-column-end:span 7}}.mdc-layout-grid__cell--span-8,.mdc-layout-grid__cell--span-8-tablet{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-tablet, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-8,.mdc-layout-grid__cell--span-8-tablet{width:auto;grid-column-end:span 8}}.mdc-layout-grid__cell--span-9,.mdc-layout-grid__cell--span-9-tablet{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-tablet, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-9,.mdc-layout-grid__cell--span-9-tablet{width:auto;grid-column-end:span 8}}.mdc-layout-grid__cell--span-10,.mdc-layout-grid__cell--span-10-tablet{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-tablet, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-10,.mdc-layout-grid__cell--span-10-tablet{width:auto;grid-column-end:span 8}}.mdc-layout-grid__cell--span-11,.mdc-layout-grid__cell--span-11-tablet{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-tablet, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-11,.mdc-layout-grid__cell--span-11-tablet{width:auto;grid-column-end:span 8}}.mdc-layout-grid__cell--span-12,.mdc-layout-grid__cell--span-12-tablet{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-tablet, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-12,.mdc-layout-grid__cell--span-12-tablet{width:auto;grid-column-end:span 8}}}@media(max-width: 599px){.mdc-layout-grid__cell{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-phone, 16px));box-sizing:border-box;margin:8px;margin:calc(var(--mdc-layout-grid-gutter-phone, 16px) / 2)}@supports(display: grid){.mdc-layout-grid__cell{width:auto;grid-column-end:span 4}}@supports(display: grid){.mdc-layout-grid__cell{margin:0}}.mdc-layout-grid__cell--span-1,.mdc-layout-grid__cell--span-1-phone{width:calc(25% - 16px);width:calc(25% - var(--mdc-layout-grid-gutter-phone, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-1,.mdc-layout-grid__cell--span-1-phone{width:auto;grid-column-end:span 1}}.mdc-layout-grid__cell--span-2,.mdc-layout-grid__cell--span-2-phone{width:calc(50% - 16px);width:calc(50% - var(--mdc-layout-grid-gutter-phone, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-2,.mdc-layout-grid__cell--span-2-phone{width:auto;grid-column-end:span 2}}.mdc-layout-grid__cell--span-3,.mdc-layout-grid__cell--span-3-phone{width:calc(75% - 16px);width:calc(75% - var(--mdc-layout-grid-gutter-phone, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-3,.mdc-layout-grid__cell--span-3-phone{width:auto;grid-column-end:span 3}}.mdc-layout-grid__cell--span-4,.mdc-layout-grid__cell--span-4-phone{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-phone, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-4,.mdc-layout-grid__cell--span-4-phone{width:auto;grid-column-end:span 4}}.mdc-layout-grid__cell--span-5,.mdc-layout-grid__cell--span-5-phone{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-phone, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-5,.mdc-layout-grid__cell--span-5-phone{width:auto;grid-column-end:span 4}}.mdc-layout-grid__cell--span-6,.mdc-layout-grid__cell--span-6-phone{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-phone, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-6,.mdc-layout-grid__cell--span-6-phone{width:auto;grid-column-end:span 4}}.mdc-layout-grid__cell--span-7,.mdc-layout-grid__cell--span-7-phone{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-phone, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-7,.mdc-layout-grid__cell--span-7-phone{width:auto;grid-column-end:span 4}}.mdc-layout-grid__cell--span-8,.mdc-layout-grid__cell--span-8-phone{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-phone, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-8,.mdc-layout-grid__cell--span-8-phone{width:auto;grid-column-end:span 4}}.mdc-layout-grid__cell--span-9,.mdc-layout-grid__cell--span-9-phone{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-phone, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-9,.mdc-layout-grid__cell--span-9-phone{width:auto;grid-column-end:span 4}}.mdc-layout-grid__cell--span-10,.mdc-layout-grid__cell--span-10-phone{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-phone, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-10,.mdc-layout-grid__cell--span-10-phone{width:auto;grid-column-end:span 4}}.mdc-layout-grid__cell--span-11,.mdc-layout-grid__cell--span-11-phone{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-phone, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-11,.mdc-layout-grid__cell--span-11-phone{width:auto;grid-column-end:span 4}}.mdc-layout-grid__cell--span-12,.mdc-layout-grid__cell--span-12-phone{width:calc(100% - 16px);width:calc(100% - var(--mdc-layout-grid-gutter-phone, 16px))}@supports(display: grid){.mdc-layout-grid__cell--span-12,.mdc-layout-grid__cell--span-12-phone{width:auto;grid-column-end:span 4}}}.mdc-layout-grid__cell--order-1{order:1}.mdc-layout-grid__cell--order-2{order:2}.mdc-layout-grid__cell--order-3{order:3}.mdc-layout-grid__cell--order-4{order:4}.mdc-layout-grid__cell--order-5{order:5}.mdc-layout-grid__cell--order-6{order:6}.mdc-layout-grid__cell--order-7{order:7}.mdc-layout-grid__cell--order-8{order:8}.mdc-layout-grid__cell--order-9{order:9}.mdc-layout-grid__cell--order-10{order:10}.mdc-layout-grid__cell--order-11{order:11}.mdc-layout-grid__cell--order-12{order:12}.mdc-layout-grid__cell--align-top{align-self:flex-start}@supports(display: grid){.mdc-layout-grid__cell--align-top{align-self:start}}.mdc-layout-grid__cell--align-middle{align-self:center}.mdc-layout-grid__cell--align-bottom{align-self:flex-end}@supports(display: grid){.mdc-layout-grid__cell--align-bottom{align-self:end}}@media(min-width: 840px){.mdc-layout-grid--fixed-column-width{width:1176px;width:calc( var(--mdc-layout-grid-column-width-desktop, 72px) * 12 + var(--mdc-layout-grid-gutter-desktop, 24px) * 11 + var(--mdc-layout-grid-margin-desktop, 24px) * 2 )}}@media(min-width: 600px)and (max-width: 839px){.mdc-layout-grid--fixed-column-width{width:720px;width:calc( var(--mdc-layout-grid-column-width-tablet, 72px) * 8 + var(--mdc-layout-grid-gutter-tablet, 16px) * 7 + var(--mdc-layout-grid-margin-tablet, 16px) * 2 )}}@media(max-width: 599px){.mdc-layout-grid--fixed-column-width{width:368px;width:calc( var(--mdc-layout-grid-column-width-phone, 72px) * 4 + var(--mdc-layout-grid-gutter-phone, 16px) * 3 + var(--mdc-layout-grid-margin-phone, 16px) * 2 )}}.mdc-layout-grid--align-left{margin-right:auto;margin-left:0}.mdc-layout-grid--align-right{margin-right:0;margin-left:auto}.mdc-line-ripple::before,.mdc-line-ripple::after{position:absolute;bottom:0;left:0;width:100%;border-bottom-style:solid;content:""}.mdc-line-ripple::before{border-bottom-width:1px;z-index:1}.mdc-line-ripple::after{-webkit-transform:scaleX(0);transform:scaleX(0);border-bottom-width:2px;opacity:0;z-index:2}.mdc-line-ripple::after{transition:opacity 180ms cubic-bezier(0.4, 0, 0.2, 1),-webkit-transform 180ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 180ms cubic-bezier(0.4, 0, 0.2, 1),opacity 180ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 180ms cubic-bezier(0.4, 0, 0.2, 1),opacity 180ms cubic-bezier(0.4, 0, 0.2, 1),-webkit-transform 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-line-ripple--active::after{-webkit-transform:scaleX(1);transform:scaleX(1);opacity:1}.mdc-line-ripple--deactivating::after{opacity:0}@-webkit-keyframes mdc-linear-progress-primary-indeterminate-translate{0%{-webkit-transform:translateX(0);transform:translateX(0)}20%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(0);transform:translateX(0)}59.15%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(83.67142%);transform:translateX(83.67142%)}100%{-webkit-transform:translateX(200.611057%);transform:translateX(200.611057%)}}@keyframes mdc-linear-progress-primary-indeterminate-translate{0%{-webkit-transform:translateX(0);transform:translateX(0)}20%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(0);transform:translateX(0)}59.15%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(83.67142%);transform:translateX(83.67142%)}100%{-webkit-transform:translateX(200.611057%);transform:translateX(200.611057%)}}@-webkit-keyframes mdc-linear-progress-primary-indeterminate-scale{0%{-webkit-transform:scaleX(0.08);transform:scaleX(0.08)}36.65%{-webkit-animation-timing-function:cubic-bezier(0.334731, 0.12482, 0.785844, 1);animation-timing-function:cubic-bezier(0.334731, 0.12482, 0.785844, 1);-webkit-transform:scaleX(0.08);transform:scaleX(0.08)}69.15%{-webkit-animation-timing-function:cubic-bezier(0.06, 0.11, 0.6, 1);animation-timing-function:cubic-bezier(0.06, 0.11, 0.6, 1);-webkit-transform:scaleX(0.661479);transform:scaleX(0.661479)}100%{-webkit-transform:scaleX(0.08);transform:scaleX(0.08)}}@keyframes mdc-linear-progress-primary-indeterminate-scale{0%{-webkit-transform:scaleX(0.08);transform:scaleX(0.08)}36.65%{-webkit-animation-timing-function:cubic-bezier(0.334731, 0.12482, 0.785844, 1);animation-timing-function:cubic-bezier(0.334731, 0.12482, 0.785844, 1);-webkit-transform:scaleX(0.08);transform:scaleX(0.08)}69.15%{-webkit-animation-timing-function:cubic-bezier(0.06, 0.11, 0.6, 1);animation-timing-function:cubic-bezier(0.06, 0.11, 0.6, 1);-webkit-transform:scaleX(0.661479);transform:scaleX(0.661479)}100%{-webkit-transform:scaleX(0.08);transform:scaleX(0.08)}}@-webkit-keyframes mdc-linear-progress-secondary-indeterminate-translate{0%{-webkit-animation-timing-function:cubic-bezier(0.15, 0, 0.515058, 0.409685);animation-timing-function:cubic-bezier(0.15, 0, 0.515058, 0.409685);-webkit-transform:translateX(0);transform:translateX(0)}25%{-webkit-animation-timing-function:cubic-bezier(0.31033, 0.284058, 0.8, 0.733712);animation-timing-function:cubic-bezier(0.31033, 0.284058, 0.8, 0.733712);-webkit-transform:translateX(37.651913%);transform:translateX(37.651913%)}48.35%{-webkit-animation-timing-function:cubic-bezier(0.4, 0.627035, 0.6, 0.902026);animation-timing-function:cubic-bezier(0.4, 0.627035, 0.6, 0.902026);-webkit-transform:translateX(84.386165%);transform:translateX(84.386165%)}100%{-webkit-transform:translateX(160.277782%);transform:translateX(160.277782%)}}@keyframes mdc-linear-progress-secondary-indeterminate-translate{0%{-webkit-animation-timing-function:cubic-bezier(0.15, 0, 0.515058, 0.409685);animation-timing-function:cubic-bezier(0.15, 0, 0.515058, 0.409685);-webkit-transform:translateX(0);transform:translateX(0)}25%{-webkit-animation-timing-function:cubic-bezier(0.31033, 0.284058, 0.8, 0.733712);animation-timing-function:cubic-bezier(0.31033, 0.284058, 0.8, 0.733712);-webkit-transform:translateX(37.651913%);transform:translateX(37.651913%)}48.35%{-webkit-animation-timing-function:cubic-bezier(0.4, 0.627035, 0.6, 0.902026);animation-timing-function:cubic-bezier(0.4, 0.627035, 0.6, 0.902026);-webkit-transform:translateX(84.386165%);transform:translateX(84.386165%)}100%{-webkit-transform:translateX(160.277782%);transform:translateX(160.277782%)}}@-webkit-keyframes mdc-linear-progress-secondary-indeterminate-scale{0%{-webkit-animation-timing-function:cubic-bezier(0.205028, 0.057051, 0.57661, 0.453971);animation-timing-function:cubic-bezier(0.205028, 0.057051, 0.57661, 0.453971);-webkit-transform:scaleX(0.08);transform:scaleX(0.08)}19.15%{-webkit-animation-timing-function:cubic-bezier(0.152313, 0.196432, 0.648374, 1.004315);animation-timing-function:cubic-bezier(0.152313, 0.196432, 0.648374, 1.004315);-webkit-transform:scaleX(0.457104);transform:scaleX(0.457104)}44.15%{-webkit-animation-timing-function:cubic-bezier(0.257759, -0.003163, 0.211762, 1.38179);animation-timing-function:cubic-bezier(0.257759, -0.003163, 0.211762, 1.38179);-webkit-transform:scaleX(0.72796);transform:scaleX(0.72796)}100%{-webkit-transform:scaleX(0.08);transform:scaleX(0.08)}}@keyframes mdc-linear-progress-secondary-indeterminate-scale{0%{-webkit-animation-timing-function:cubic-bezier(0.205028, 0.057051, 0.57661, 0.453971);animation-timing-function:cubic-bezier(0.205028, 0.057051, 0.57661, 0.453971);-webkit-transform:scaleX(0.08);transform:scaleX(0.08)}19.15%{-webkit-animation-timing-function:cubic-bezier(0.152313, 0.196432, 0.648374, 1.004315);animation-timing-function:cubic-bezier(0.152313, 0.196432, 0.648374, 1.004315);-webkit-transform:scaleX(0.457104);transform:scaleX(0.457104)}44.15%{-webkit-animation-timing-function:cubic-bezier(0.257759, -0.003163, 0.211762, 1.38179);animation-timing-function:cubic-bezier(0.257759, -0.003163, 0.211762, 1.38179);-webkit-transform:scaleX(0.72796);transform:scaleX(0.72796)}100%{-webkit-transform:scaleX(0.08);transform:scaleX(0.08)}}@-webkit-keyframes mdc-linear-progress-buffering{from{-webkit-transform:rotate(180deg) translateX(-10px);transform:rotate(180deg) translateX(-10px)}}@keyframes mdc-linear-progress-buffering{from{-webkit-transform:rotate(180deg) translateX(-10px);transform:rotate(180deg) translateX(-10px)}}@-webkit-keyframes mdc-linear-progress-primary-indeterminate-translate-reverse{0%{-webkit-transform:translateX(0);transform:translateX(0)}20%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(0);transform:translateX(0)}59.15%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(-83.67142%);transform:translateX(-83.67142%)}100%{-webkit-transform:translateX(-200.611057%);transform:translateX(-200.611057%)}}@keyframes mdc-linear-progress-primary-indeterminate-translate-reverse{0%{-webkit-transform:translateX(0);transform:translateX(0)}20%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(0);transform:translateX(0)}59.15%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(-83.67142%);transform:translateX(-83.67142%)}100%{-webkit-transform:translateX(-200.611057%);transform:translateX(-200.611057%)}}@-webkit-keyframes mdc-linear-progress-secondary-indeterminate-translate-reverse{0%{-webkit-animation-timing-function:cubic-bezier(0.15, 0, 0.515058, 0.409685);animation-timing-function:cubic-bezier(0.15, 0, 0.515058, 0.409685);-webkit-transform:translateX(0);transform:translateX(0)}25%{-webkit-animation-timing-function:cubic-bezier(0.31033, 0.284058, 0.8, 0.733712);animation-timing-function:cubic-bezier(0.31033, 0.284058, 0.8, 0.733712);-webkit-transform:translateX(-37.651913%);transform:translateX(-37.651913%)}48.35%{-webkit-animation-timing-function:cubic-bezier(0.4, 0.627035, 0.6, 0.902026);animation-timing-function:cubic-bezier(0.4, 0.627035, 0.6, 0.902026);-webkit-transform:translateX(-84.386165%);transform:translateX(-84.386165%)}100%{-webkit-transform:translateX(-160.277782%);transform:translateX(-160.277782%)}}@keyframes mdc-linear-progress-secondary-indeterminate-translate-reverse{0%{-webkit-animation-timing-function:cubic-bezier(0.15, 0, 0.515058, 0.409685);animation-timing-function:cubic-bezier(0.15, 0, 0.515058, 0.409685);-webkit-transform:translateX(0);transform:translateX(0)}25%{-webkit-animation-timing-function:cubic-bezier(0.31033, 0.284058, 0.8, 0.733712);animation-timing-function:cubic-bezier(0.31033, 0.284058, 0.8, 0.733712);-webkit-transform:translateX(-37.651913%);transform:translateX(-37.651913%)}48.35%{-webkit-animation-timing-function:cubic-bezier(0.4, 0.627035, 0.6, 0.902026);animation-timing-function:cubic-bezier(0.4, 0.627035, 0.6, 0.902026);-webkit-transform:translateX(-84.386165%);transform:translateX(-84.386165%)}100%{-webkit-transform:translateX(-160.277782%);transform:translateX(-160.277782%)}}@-webkit-keyframes mdc-linear-progress-buffering-reverse{from{-webkit-transform:translateX(-10px);transform:translateX(-10px)}}@keyframes mdc-linear-progress-buffering-reverse{from{-webkit-transform:translateX(-10px);transform:translateX(-10px)}}.mdc-linear-progress{position:relative;width:100%;height:4px;-webkit-transform:translateZ(0);transform:translateZ(0);outline:1px solid transparent;overflow:hidden;transition:opacity 250ms 0ms cubic-bezier(0.4, 0, 0.6, 1)}.mdc-linear-progress__bar{position:absolute;width:100%;height:100%;-webkit-animation:none;animation:none;-webkit-transform-origin:top left;transform-origin:top left;transition:-webkit-transform 250ms 0ms cubic-bezier(0.4, 0, 0.6, 1);transition:transform 250ms 0ms cubic-bezier(0.4, 0, 0.6, 1);transition:transform 250ms 0ms cubic-bezier(0.4, 0, 0.6, 1), -webkit-transform 250ms 0ms cubic-bezier(0.4, 0, 0.6, 1)}.mdc-linear-progress__bar-inner{display:inline-block;position:absolute;width:100%;-webkit-animation:none;animation:none;border-top:4px solid}.mdc-linear-progress__buffer{display:flex;position:absolute;width:100%;height:100%}.mdc-linear-progress__buffer-dots{background-repeat:repeat-x;background-size:10px 4px;flex:auto;-webkit-transform:rotate(180deg);transform:rotate(180deg);-webkit-animation:mdc-linear-progress-buffering 250ms infinite linear;animation:mdc-linear-progress-buffering 250ms infinite linear}.mdc-linear-progress__buffer-bar{flex:0 1 100%;transition:flex-basis 250ms 0ms cubic-bezier(0.4, 0, 0.6, 1)}.mdc-linear-progress__primary-bar{-webkit-transform:scaleX(0);transform:scaleX(0)}.mdc-linear-progress__secondary-bar{visibility:hidden}.mdc-linear-progress--indeterminate .mdc-linear-progress__bar{transition:none}.mdc-linear-progress--indeterminate .mdc-linear-progress__primary-bar{left:-145.166611%;-webkit-animation:mdc-linear-progress-primary-indeterminate-translate 2s infinite linear;animation:mdc-linear-progress-primary-indeterminate-translate 2s infinite linear}.mdc-linear-progress--indeterminate .mdc-linear-progress__primary-bar>.mdc-linear-progress__bar-inner{-webkit-animation:mdc-linear-progress-primary-indeterminate-scale 2s infinite linear;animation:mdc-linear-progress-primary-indeterminate-scale 2s infinite linear}.mdc-linear-progress--indeterminate .mdc-linear-progress__secondary-bar{left:-54.888891%;visibility:visible;-webkit-animation:mdc-linear-progress-secondary-indeterminate-translate 2s infinite linear;animation:mdc-linear-progress-secondary-indeterminate-translate 2s infinite linear}.mdc-linear-progress--indeterminate .mdc-linear-progress__secondary-bar>.mdc-linear-progress__bar-inner{-webkit-animation:mdc-linear-progress-secondary-indeterminate-scale 2s infinite linear;animation:mdc-linear-progress-secondary-indeterminate-scale 2s infinite linear}.mdc-linear-progress--reversed .mdc-linear-progress__bar{right:0;-webkit-transform-origin:center right;transform-origin:center right}.mdc-linear-progress--reversed .mdc-linear-progress__primary-bar{-webkit-animation-name:mdc-linear-progress-primary-indeterminate-translate-reverse;animation-name:mdc-linear-progress-primary-indeterminate-translate-reverse}.mdc-linear-progress--reversed .mdc-linear-progress__secondary-bar{-webkit-animation-name:mdc-linear-progress-secondary-indeterminate-translate-reverse;animation-name:mdc-linear-progress-secondary-indeterminate-translate-reverse}.mdc-linear-progress--reversed .mdc-linear-progress__buffer-dots{-webkit-animation:mdc-linear-progress-buffering-reverse 250ms infinite linear;animation:mdc-linear-progress-buffering-reverse 250ms infinite linear;order:0;-webkit-transform:rotate(0);transform:rotate(0)}.mdc-linear-progress--reversed .mdc-linear-progress__buffer-bar{order:1}.mdc-linear-progress--closed{opacity:0;-webkit-animation:none;animation:none}.mdc-linear-progress__bar-inner{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-linear-progress__buffer-dots{background-image:url("data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' enable-background='new 0 0 5 2' xml:space='preserve' viewBox='0 0 5 2' preserveAspectRatio='none slice'%3E%3Ccircle cx='1' cy='1' r='1' fill='%23e6e6e6'/%3E%3C/svg%3E")}.mdc-linear-progress__buffer-bar{background-color:#e6e6e6}.mdc-linear-progress--indeterminate.mdc-linear-progress--reversed .mdc-linear-progress__primary-bar{right:-145.166611%;left:auto}.mdc-linear-progress--indeterminate.mdc-linear-progress--reversed .mdc-linear-progress__secondary-bar{right:-54.888891%;left:auto}.mdc-list{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);line-height:1.75rem;line-height:var(--mdc-typography-subtitle1-line-height, 1.75rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);line-height:1.5rem;margin:0;padding:8px 0;list-style-type:none;color:rgba(0,0,0,.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87))}.mdc-list:focus{outline:none}.mdc-list-item{height:48px}.mdc-list-item__secondary-text{color:rgba(0,0,0,.54);color:var(--mdc-theme-text-secondary-on-background, rgba(0, 0, 0, 0.54))}.mdc-list-item__graphic{background-color:transparent}.mdc-list-item__graphic{color:rgba(0,0,0,.38);color:var(--mdc-theme-text-icon-on-background, rgba(0, 0, 0, 0.38))}.mdc-list-item__meta{color:rgba(0,0,0,.38);color:var(--mdc-theme-text-hint-on-background, rgba(0, 0, 0, 0.38))}.mdc-list-group__subheader{color:rgba(0,0,0,.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87))}.mdc-list-item--disabled .mdc-list-item__text{opacity:.38}.mdc-list-item--disabled .mdc-list-item__text,.mdc-list-item--disabled .mdc-list-item__primary-text,.mdc-list-item--disabled .mdc-list-item__secondary-text{color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-list--dense{padding-top:4px;padding-bottom:4px;font-size:.812rem}.mdc-list-item{display:flex;position:relative;align-items:center;justify-content:flex-start;padding:0 16px;overflow:hidden}.mdc-list-item:focus{outline:none}.mdc-list-item--selected,.mdc-list-item--activated{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}.mdc-list-item--selected .mdc-list-item__graphic,.mdc-list-item--activated .mdc-list-item__graphic{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}.mdc-list-item__graphic{margin-left:0;margin-right:32px;width:24px;height:24px;flex-shrink:0;align-items:center;justify-content:center;fill:currentColor}.mdc-list-item[dir=rtl] .mdc-list-item__graphic,[dir=rtl] .mdc-list-item .mdc-list-item__graphic{margin-left:32px;margin-right:0}.mdc-list .mdc-list-item__graphic{display:inline-flex}.mdc-list-item__meta{margin-left:auto;margin-right:0}.mdc-list-item__meta:not(.material-icons){-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit)}.mdc-list-item[dir=rtl] .mdc-list-item__meta,[dir=rtl] .mdc-list-item .mdc-list-item__meta{margin-left:0;margin-right:auto}.mdc-list-item__text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.mdc-list-item__text[for]{pointer-events:none}.mdc-list-item__primary-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block;margin-top:0;line-height:normal;margin-bottom:-20px}.mdc-list-item__primary-text::before{display:inline-block;width:0;height:32px;content:"";vertical-align:0}.mdc-list-item__primary-text::after{display:inline-block;width:0;height:20px;content:"";vertical-align:-20px}.mdc-list--dense .mdc-list-item__primary-text{display:block;margin-top:0;line-height:normal;margin-bottom:-20px}.mdc-list--dense .mdc-list-item__primary-text::before{display:inline-block;width:0;height:24px;content:"";vertical-align:0}.mdc-list--dense .mdc-list-item__primary-text::after{display:inline-block;width:0;height:20px;content:"";vertical-align:-20px}.mdc-list-item__secondary-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block;margin-top:0;line-height:normal}.mdc-list-item__secondary-text::before{display:inline-block;width:0;height:20px;content:"";vertical-align:0}.mdc-list--dense .mdc-list-item__secondary-text{font-size:inherit}.mdc-list--dense .mdc-list-item{height:40px}.mdc-list--dense .mdc-list-item__graphic{margin-left:0;margin-right:36px;width:20px;height:20px}.mdc-list-item[dir=rtl] .mdc-list--dense .mdc-list-item__graphic,[dir=rtl] .mdc-list-item .mdc-list--dense .mdc-list-item__graphic{margin-left:36px;margin-right:0}.mdc-list--avatar-list .mdc-list-item{height:56px}.mdc-list--avatar-list .mdc-list-item__graphic{margin-left:0;margin-right:16px;width:40px;height:40px;border-radius:50%}.mdc-list-item[dir=rtl] .mdc-list--avatar-list .mdc-list-item__graphic,[dir=rtl] .mdc-list-item .mdc-list--avatar-list .mdc-list-item__graphic{margin-left:16px;margin-right:0}.mdc-list--two-line .mdc-list-item__text{align-self:flex-start}.mdc-list--two-line .mdc-list-item{height:72px}.mdc-list--two-line.mdc-list--dense .mdc-list-item,.mdc-list--avatar-list.mdc-list--dense .mdc-list-item{height:60px}.mdc-list--avatar-list.mdc-list--dense .mdc-list-item__graphic{margin-left:0;margin-right:20px;width:36px;height:36px}.mdc-list-item[dir=rtl] .mdc-list--avatar-list.mdc-list--dense .mdc-list-item__graphic,[dir=rtl] .mdc-list-item .mdc-list--avatar-list.mdc-list--dense .mdc-list-item__graphic{margin-left:20px;margin-right:0}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item{cursor:pointer}a.mdc-list-item{color:inherit;text-decoration:none}.mdc-list-divider{height:0;margin:0;border:none;border-bottom-width:1px;border-bottom-style:solid}.mdc-list-divider{border-bottom-color:rgba(0,0,0,.12)}.mdc-list-divider--padded{margin:0 16px}.mdc-list-divider--inset{margin-left:72px;margin-right:0;width:calc(100% - 72px)}.mdc-list-group[dir=rtl] .mdc-list-divider--inset,[dir=rtl] .mdc-list-group .mdc-list-divider--inset{margin-left:0;margin-right:72px}.mdc-list-divider--inset.mdc-list-divider--padded{width:calc(100% - 72px - 16px)}.mdc-list-group .mdc-list{padding:0}.mdc-list-group__subheader{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);line-height:1.75rem;line-height:var(--mdc-typography-subtitle1-line-height, 1.75rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);margin:calc((3rem - 1.5rem) / 2) 16px}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0)}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item::before,:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item.mdc-ripple-upgraded::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item.mdc-ripple-upgraded::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item.mdc-ripple-upgraded--unbounded::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item.mdc-ripple-upgraded--foreground-activation::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item.mdc-ripple-upgraded--foreground-deactivation::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item::before,:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item::before,:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item::after{background-color:#000}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item:hover::before{opacity:.04}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item.mdc-ripple-upgraded--background-focused::before,:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.12}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.12}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--activated::before{opacity:.12}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--activated::before,:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--activated::after{background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--activated:hover::before{opacity:.16}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--activated.mdc-ripple-upgraded--background-focused::before,:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--activated:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.24}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--activated:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--activated:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.24}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--activated.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.24}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--selected::before{opacity:.08}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--selected::before,:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--selected::after{background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--selected:hover::before{opacity:.12}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--selected.mdc-ripple-upgraded--background-focused::before,:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--selected:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.2}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--selected:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--selected:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.2}:not(.mdc-list--non-interactive)>:not(.mdc-list-item--disabled).mdc-list-item--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.2}:not(.mdc-list--non-interactive)>.mdc-list-item--disabled{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0)}:not(.mdc-list--non-interactive)>.mdc-list-item--disabled::before,:not(.mdc-list--non-interactive)>.mdc-list-item--disabled::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}:not(.mdc-list--non-interactive)>.mdc-list-item--disabled::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}:not(.mdc-list--non-interactive)>.mdc-list-item--disabled.mdc-ripple-upgraded::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}:not(.mdc-list--non-interactive)>.mdc-list-item--disabled.mdc-ripple-upgraded::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}:not(.mdc-list--non-interactive)>.mdc-list-item--disabled.mdc-ripple-upgraded--unbounded::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}:not(.mdc-list--non-interactive)>.mdc-list-item--disabled.mdc-ripple-upgraded--foreground-activation::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}:not(.mdc-list--non-interactive)>.mdc-list-item--disabled.mdc-ripple-upgraded--foreground-deactivation::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}:not(.mdc-list--non-interactive)>.mdc-list-item--disabled::before,:not(.mdc-list--non-interactive)>.mdc-list-item--disabled::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}:not(.mdc-list--non-interactive)>.mdc-list-item--disabled.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}:not(.mdc-list--non-interactive)>.mdc-list-item--disabled::before,:not(.mdc-list--non-interactive)>.mdc-list-item--disabled::after{background-color:#000}:not(.mdc-list--non-interactive)>.mdc-list-item--disabled.mdc-ripple-upgraded--background-focused::before,:not(.mdc-list--non-interactive)>.mdc-list-item--disabled:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.12}.mdc-menu{min-width:112px}.mdc-menu .mdc-list-item__meta{color:rgba(0,0,0,.87)}.mdc-menu .mdc-list-item__graphic{color:rgba(0,0,0,.87)}.mdc-menu .mdc-list{color:rgba(0,0,0,.87);position:relative}.mdc-menu .mdc-list .mdc-elevation-overlay{width:100%;height:100%;top:0;left:0}.mdc-menu .mdc-list-divider{margin:8px 0}.mdc-menu .mdc-list-item{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdc-menu .mdc-list-item--disabled{cursor:auto}.mdc-menu a.mdc-list-item .mdc-list-item__text,.mdc-menu a.mdc-list-item .mdc-list-item__graphic{pointer-events:none}.mdc-menu__selection-group{padding:0;fill:currentColor}.mdc-menu__selection-group .mdc-list-item{padding-left:56px;padding-right:16px}[dir=rtl] .mdc-menu__selection-group .mdc-list-item,.mdc-menu__selection-group .mdc-list-item[dir=rtl]{padding-left:16px;padding-right:56px}.mdc-menu__selection-group .mdc-menu__selection-group-icon{left:16px;right:initial;display:none;position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}[dir=rtl] .mdc-menu__selection-group .mdc-menu__selection-group-icon,.mdc-menu__selection-group .mdc-menu__selection-group-icon[dir=rtl]{left:initial;right:16px}.mdc-menu-item--selected .mdc-menu__selection-group-icon{display:inline}.mdc-menu-surface{display:none;position:absolute;box-sizing:border-box;max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);margin:0;padding:0;-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:top left;transform-origin:top left;opacity:0;overflow:auto;will-change:transform,opacity;z-index:8;transition:opacity .03s linear,-webkit-transform .12s cubic-bezier(0, 0, 0.2, 1);transition:opacity .03s linear,transform .12s cubic-bezier(0, 0, 0.2, 1);transition:opacity .03s linear,transform .12s cubic-bezier(0, 0, 0.2, 1),-webkit-transform .12s cubic-bezier(0, 0, 0.2, 1);box-shadow:0px 5px 5px -3px rgba(0, 0, 0, 0.2),0px 8px 10px 1px rgba(0, 0, 0, 0.14),0px 3px 14px 2px rgba(0,0,0,.12);background-color:#fff;background-color:var(--mdc-theme-surface, #fff);color:#000;color:var(--mdc-theme-on-surface, #000);border-radius:4px;transform-origin-left:top left;transform-origin-right:top right}.mdc-menu-surface:focus{outline:none}.mdc-menu-surface--open{display:inline-block;-webkit-transform:scale(1);transform:scale(1);opacity:1}.mdc-menu-surface--animating-open{display:inline-block;-webkit-transform:scale(0.8);transform:scale(0.8);opacity:0}.mdc-menu-surface--animating-closed{display:inline-block;opacity:0;transition:opacity .075s linear}[dir=rtl] .mdc-menu-surface,.mdc-menu-surface[dir=rtl]{transform-origin-left:top right;transform-origin-right:top left}.mdc-menu-surface--anchor{position:relative;overflow:visible}.mdc-menu-surface--fixed{position:fixed}.mdc-notched-outline{display:flex;position:absolute;top:0;right:0;left:0;box-sizing:border-box;width:100%;max-width:100%;height:100%;text-align:left;pointer-events:none}[dir=rtl] .mdc-notched-outline,.mdc-notched-outline[dir=rtl]{text-align:right}.mdc-notched-outline__leading,.mdc-notched-outline__notch,.mdc-notched-outline__trailing{box-sizing:border-box;height:100%;border-top:1px solid;border-bottom:1px solid;pointer-events:none}.mdc-notched-outline__leading{border-left:1px solid;border-right:none;width:12px}[dir=rtl] .mdc-notched-outline__leading,.mdc-notched-outline__leading[dir=rtl]{border-left:none;border-right:1px solid}.mdc-notched-outline__trailing{border-left:none;border-right:1px solid;flex-grow:1}[dir=rtl] .mdc-notched-outline__trailing,.mdc-notched-outline__trailing[dir=rtl]{border-left:1px solid;border-right:none}.mdc-notched-outline__notch{flex:0 0 auto;width:auto;max-width:calc(100% - 12px * 2)}.mdc-notched-outline .mdc-floating-label{display:inline-block;position:relative;max-width:100%}.mdc-notched-outline .mdc-floating-label--float-above{text-overflow:clip}.mdc-notched-outline--upgraded .mdc-floating-label--float-above{max-width:calc(100% / .75)}.mdc-notched-outline--notched .mdc-notched-outline__notch{padding-left:0;padding-right:8px;border-top:none}[dir=rtl] .mdc-notched-outline--notched .mdc-notched-outline__notch,.mdc-notched-outline--notched .mdc-notched-outline__notch[dir=rtl]{padding-left:8px;padding-right:0}.mdc-notched-outline--no-label .mdc-notched-outline__notch{padding:0}.mdc-radio{padding:10px;display:inline-block;position:relative;flex:0 0 auto;box-sizing:content-box;width:20px;height:20px;cursor:pointer;will-change:opacity,transform,border-color,color}.mdc-radio .mdc-radio__native-control:enabled:not(:checked)+.mdc-radio__background .mdc-radio__outer-circle{border-color:rgba(0,0,0,.54)}.mdc-radio .mdc-radio__native-control:enabled:checked+.mdc-radio__background .mdc-radio__outer-circle{border-color:#018786;border-color:var(--mdc-theme-secondary, #018786)}.mdc-radio .mdc-radio__native-control:enabled+.mdc-radio__background .mdc-radio__inner-circle{border-color:#018786;border-color:var(--mdc-theme-secondary, #018786)}.mdc-radio [aria-disabled=true] .mdc-radio__native-control:not(:checked)+.mdc-radio__background .mdc-radio__outer-circle,.mdc-radio .mdc-radio__native-control:disabled:not(:checked)+.mdc-radio__background .mdc-radio__outer-circle{border-color:rgba(0,0,0,.38)}.mdc-radio [aria-disabled=true] .mdc-radio__native-control:checked+.mdc-radio__background .mdc-radio__outer-circle,.mdc-radio .mdc-radio__native-control:disabled:checked+.mdc-radio__background .mdc-radio__outer-circle{border-color:rgba(0,0,0,.38)}.mdc-radio [aria-disabled=true] .mdc-radio__native-control+.mdc-radio__background .mdc-radio__inner-circle,.mdc-radio .mdc-radio__native-control:disabled+.mdc-radio__background .mdc-radio__inner-circle{border-color:rgba(0,0,0,.38)}.mdc-radio .mdc-radio__background::before{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-radio .mdc-radio__background::before{top:-10px;left:-10px;width:40px;height:40px}.mdc-radio .mdc-radio__native-control{top:0px;right:0px;left:0px;width:40px;height:40px}.mdc-radio__background{display:inline-block;position:relative;box-sizing:border-box;width:20px;height:20px}.mdc-radio__background::before{position:absolute;-webkit-transform:scale(0, 0);transform:scale(0, 0);border-radius:50%;opacity:0;pointer-events:none;content:"";transition:opacity 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1),-webkit-transform 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1);transition:opacity 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1),transform 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1);transition:opacity 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1),transform 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1),-webkit-transform 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1)}.mdc-radio__outer-circle{position:absolute;top:0;left:0;box-sizing:border-box;width:100%;height:100%;border-width:2px;border-style:solid;border-radius:50%;transition:border-color 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1)}.mdc-radio__inner-circle{position:absolute;top:0;left:0;box-sizing:border-box;width:100%;height:100%;-webkit-transform:scale(0, 0);transform:scale(0, 0);border-width:10px;border-style:solid;border-radius:50%;transition:border-color 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1),-webkit-transform 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1);transition:transform 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1),border-color 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1);transition:transform 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1),border-color 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1),-webkit-transform 120ms 0ms cubic-bezier(0.4, 0, 0.6, 1)}.mdc-radio__native-control{position:absolute;margin:0;padding:0;opacity:0;cursor:inherit;z-index:1}.mdc-radio--touch{margin-top:4px;margin-bottom:4px;margin-right:4px;margin-left:4px}.mdc-radio--touch .mdc-radio__native-control{top:-4px;right:-4px;left:-4px;width:48px;height:48px}.mdc-radio__native-control:checked+.mdc-radio__background,.mdc-radio__native-control:disabled+.mdc-radio__background{transition:opacity 120ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:opacity 120ms 0ms cubic-bezier(0, 0, 0.2, 1),transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:opacity 120ms 0ms cubic-bezier(0, 0, 0.2, 1),transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1)}.mdc-radio__native-control:checked+.mdc-radio__background .mdc-radio__outer-circle,.mdc-radio__native-control:disabled+.mdc-radio__background .mdc-radio__outer-circle{transition:border-color 120ms 0ms cubic-bezier(0, 0, 0.2, 1)}.mdc-radio__native-control:checked+.mdc-radio__background .mdc-radio__inner-circle,.mdc-radio__native-control:disabled+.mdc-radio__background .mdc-radio__inner-circle{transition:border-color 120ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1),border-color 120ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1),border-color 120ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1)}.mdc-radio--disabled{cursor:default;pointer-events:none}.mdc-radio__native-control:checked+.mdc-radio__background .mdc-radio__inner-circle{-webkit-transform:scale(0.5);transform:scale(0.5);transition:border-color 120ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1),border-color 120ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1),border-color 120ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1)}.mdc-radio__native-control:disabled+.mdc-radio__background,[aria-disabled=true] .mdc-radio__native-control+.mdc-radio__background{cursor:default}.mdc-radio__native-control:focus+.mdc-radio__background::before{-webkit-transform:scale(1);transform:scale(1);opacity:.12;transition:opacity 120ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:opacity 120ms 0ms cubic-bezier(0, 0, 0.2, 1),transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:opacity 120ms 0ms cubic-bezier(0, 0, 0.2, 1),transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 120ms 0ms cubic-bezier(0, 0, 0.2, 1)}.mdc-radio{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mdc-radio .mdc-radio__ripple::before,.mdc-radio .mdc-radio__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-radio .mdc-radio__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-radio.mdc-ripple-upgraded .mdc-radio__ripple::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-radio.mdc-ripple-upgraded .mdc-radio__ripple::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}.mdc-radio.mdc-ripple-upgraded--unbounded .mdc-radio__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-radio.mdc-ripple-upgraded--foreground-activation .mdc-radio__ripple::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-radio.mdc-ripple-upgraded--foreground-deactivation .mdc-radio__ripple::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-radio .mdc-radio__ripple::before,.mdc-radio .mdc-radio__ripple::after{top:calc(50% - 50%);left:calc(50% - 50%);width:100%;height:100%}.mdc-radio.mdc-ripple-upgraded .mdc-radio__ripple::before,.mdc-radio.mdc-ripple-upgraded .mdc-radio__ripple::after{top:var(--mdc-ripple-top, calc(50% - 50%));left:var(--mdc-ripple-left, calc(50% - 50%));width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-radio.mdc-ripple-upgraded .mdc-radio__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-radio .mdc-radio__ripple::before,.mdc-radio .mdc-radio__ripple::after{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-radio:hover .mdc-radio__ripple::before{opacity:.04}.mdc-radio.mdc-ripple-upgraded--background-focused .mdc-radio__ripple::before,.mdc-radio:not(.mdc-ripple-upgraded):focus .mdc-radio__ripple::before{transition-duration:75ms;opacity:.12}.mdc-radio:not(.mdc-ripple-upgraded) .mdc-radio__ripple::after{transition:opacity 150ms linear}.mdc-radio:not(.mdc-ripple-upgraded):active .mdc-radio__ripple::after{transition-duration:75ms;opacity:.12}.mdc-radio.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-radio.mdc-ripple-upgraded--background-focused .mdc-radio__background::before{content:none}.mdc-radio__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.mdc-ripple-surface{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);position:relative;outline:none;overflow:hidden}.mdc-ripple-surface::before,.mdc-ripple-surface::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-ripple-surface::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-ripple-surface.mdc-ripple-upgraded::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-ripple-surface.mdc-ripple-upgraded::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}.mdc-ripple-surface.mdc-ripple-upgraded--unbounded::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-ripple-surface.mdc-ripple-upgraded--foreground-activation::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-ripple-surface.mdc-ripple-upgraded--foreground-deactivation::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-ripple-surface::before,.mdc-ripple-surface::after{background-color:#000}.mdc-ripple-surface:hover::before{opacity:.04}.mdc-ripple-surface.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.12}.mdc-ripple-surface:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.12}.mdc-ripple-surface.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-ripple-surface::before,.mdc-ripple-surface::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-ripple-surface.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-ripple-surface[data-mdc-ripple-is-unbounded]{overflow:visible}.mdc-ripple-surface[data-mdc-ripple-is-unbounded]::before,.mdc-ripple-surface[data-mdc-ripple-is-unbounded]::after{top:calc(50% - 50%);left:calc(50% - 50%);width:100%;height:100%}.mdc-ripple-surface[data-mdc-ripple-is-unbounded].mdc-ripple-upgraded::before,.mdc-ripple-surface[data-mdc-ripple-is-unbounded].mdc-ripple-upgraded::after{top:var(--mdc-ripple-top, calc(50% - 50%));left:var(--mdc-ripple-left, calc(50% - 50%));width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-ripple-surface[data-mdc-ripple-is-unbounded].mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-ripple-surface--primary::before,.mdc-ripple-surface--primary::after{background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}.mdc-ripple-surface--primary:hover::before{opacity:.04}.mdc-ripple-surface--primary.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--primary:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.12}.mdc-ripple-surface--primary:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--primary:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.12}.mdc-ripple-surface--primary.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-ripple-surface--accent::before,.mdc-ripple-surface--accent::after{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-ripple-surface--accent:hover::before{opacity:.04}.mdc-ripple-surface--accent.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--accent:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.12}.mdc-ripple-surface--accent:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--accent:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.12}.mdc-ripple-surface--accent.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-select-helper-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit);display:block;margin-top:0;line-height:normal;margin:0;opacity:0;will-change:opacity;transition:opacity 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-select-helper-text::before{display:inline-block;width:0;height:16px;content:"";vertical-align:0}.mdc-select-helper-text--persistent{transition:none;opacity:1;will-change:initial}.mdc-select--with-leading-icon .mdc-select__icon{display:inline-block;box-sizing:border-box;width:24px;height:24px;border:none;opacity:.54;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;flex-shrink:0;align-self:center;background-color:transparent;fill:currentColor}.mdc-select--with-leading-icon .mdc-select__icon{margin-left:12px;margin-right:12px}[dir=rtl] .mdc-select--with-leading-icon .mdc-select__icon,.mdc-select--with-leading-icon .mdc-select__icon[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select--with-leading-icon:not(.mdc-select--disabled) .mdc-select__icon{color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-select__icon:not([tabindex]),.mdc-select__icon[tabindex="-1"]{cursor:default;pointer-events:none}.mdc-select__anchor{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-select__anchor .mdc-select__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}.mdc-select__anchor.mdc-ripple-upgraded--unbounded .mdc-select__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-select__anchor.mdc-ripple-upgraded--foreground-activation .mdc-select__ripple::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-select__anchor.mdc-ripple-upgraded--foreground-deactivation .mdc-select__ripple::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{background-color:rgba(0,0,0,.87)}.mdc-select__anchor:hover .mdc-select__ripple::before{opacity:.04}.mdc-select__anchor.mdc-ripple-upgraded--background-focused .mdc-select__ripple::before,.mdc-select__anchor:not(.mdc-ripple-upgraded):focus .mdc-select__ripple::before{transition-duration:75ms;opacity:.12}.mdc-select__anchor .mdc-select__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.mdc-select__menu .mdc-list .mdc-list-item--selected::before,.mdc-select__menu .mdc-list .mdc-list-item--selected::after{background-color:#000;background-color:var(--mdc-theme-on-surface, #000)}.mdc-select__menu .mdc-list .mdc-list-item--selected:hover::before{opacity:.04}.mdc-select__menu .mdc-list .mdc-list-item--selected.mdc-ripple-upgraded--background-focused::before,.mdc-select__menu .mdc-list .mdc-list-item--selected:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.12}.mdc-select__menu .mdc-list .mdc-list-item--selected:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-select__menu .mdc-list .mdc-list-item--selected:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.12}.mdc-select__menu .mdc-list .mdc-list-item--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-select{position:relative}.mdc-select:not(.mdc-select--disabled) .mdc-select__anchor{background-color:#f5f5f5}.mdc-select:not(.mdc-select--disabled) .mdc-select__selected-text{color:rgba(0,0,0,.87)}.mdc-select:not(.mdc-select--disabled) .mdc-floating-label{color:rgba(0,0,0,.6)}.mdc-select:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0,0,0,.42)}.mdc-select:not(.mdc-select--disabled) .mdc-select__anchor+.mdc-select-helper-text{color:rgba(0,0,0,.6)}.mdc-select:not(.mdc-select--disabled).mdc-select--focused .mdc-line-ripple::after{border-bottom-color:#6200ee;border-bottom-color:var(--mdc-theme-primary, #6200ee)}.mdc-select:not(.mdc-select--disabled).mdc-select--focused .mdc-floating-label{color:rgba(98,0,238,.87)}.mdc-select:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0,0,0,.87)}.mdc-select .mdc-floating-label{left:16px;right:initial;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);pointer-events:none}[dir=rtl] .mdc-select .mdc-floating-label,.mdc-select .mdc-floating-label[dir=rtl]{left:initial;right:16px}.mdc-select.mdc-select--outlined .mdc-floating-label{left:4px;right:initial}[dir=rtl] .mdc-select.mdc-select--outlined .mdc-floating-label,.mdc-select.mdc-select--outlined .mdc-floating-label[dir=rtl]{left:initial;right:4px}.mdc-select .mdc-select__anchor{border-radius:4px 4px 0 0}.mdc-select .mdc-select__anchor{padding-left:16px;padding-right:0}[dir=rtl] .mdc-select .mdc-select__anchor,.mdc-select .mdc-select__anchor[dir=rtl]{padding-left:0;padding-right:16px}.mdc-select.mdc-select--with-leading-icon .mdc-select__anchor{padding-left:0;padding-right:0}[dir=rtl] .mdc-select.mdc-select--with-leading-icon .mdc-select__anchor,.mdc-select.mdc-select--with-leading-icon .mdc-select__anchor[dir=rtl]{padding-left:0;padding-right:0}.mdc-select__dropdown-icon{background:url("data:image/svg+xml,%3Csvg%20width%3D%2210px%22%20height%3D%225px%22%20viewBox%3D%227%2010%2010%205%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%0A%20%20%20%20%3Cpolygon%20id%3D%22Shape%22%20stroke%3D%22none%22%20fill%3D%22%23000%22%20fill-rule%3D%22evenodd%22%20opacity%3D%220.54%22%20points%3D%227%2010%2012%2015%2017%2010%22%3E%3C%2Fpolygon%3E%0A%3C%2Fsvg%3E") no-repeat center;margin-left:12px;margin-right:12px;width:24px;height:24px;align-self:center;flex-shrink:0;pointer-events:none;transition:-webkit-transform 150ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 150ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 150ms cubic-bezier(0.4, 0, 0.2, 1), -webkit-transform 150ms cubic-bezier(0.4, 0, 0.2, 1)}[dir=rtl] .mdc-select__dropdown-icon,.mdc-select__dropdown-icon[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select--focused .mdc-select__dropdown-icon{background:url("data:image/svg+xml,%3Csvg%20width%3D%2210px%22%20height%3D%225px%22%20viewBox%3D%227%2010%2010%205%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%0A%20%20%20%20%3Cpolygon%20id%3D%22Shape%22%20stroke%3D%22none%22%20fill%3D%22%236200ee%22%20fill-rule%3D%22evenodd%22%20opacity%3D%221%22%20points%3D%227%2010%2012%2015%2017%2010%22%3E%3C%2Fpolygon%3E%0A%3C%2Fsvg%3E") no-repeat center}.mdc-select--activated .mdc-select__dropdown-icon{-webkit-transform:rotate(180deg) translateY(-5px);transform:rotate(180deg) translateY(-5px);transition:-webkit-transform 150ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 150ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 150ms cubic-bezier(0.4, 0, 0.2, 1), -webkit-transform 150ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-select__anchor{height:56px;display:inline-flex;align-items:baseline;display:inline-flex;position:relative;box-sizing:border-box;overflow:hidden;outline:none;cursor:pointer;min-width:200px}.mdc-select__anchor::before{display:inline-block;width:0;height:40px;content:"";vertical-align:0}.mdc-select--outlined .mdc-select__anchor .mdc-select__selected-text,.mdc-select--no-label .mdc-select__anchor .mdc-select__selected-text{height:100%}.mdc-select--outlined .mdc-select__anchor::before,.mdc-select--no-label .mdc-select__anchor::before{display:none}.mdc-select__anchor .mdc-floating-label--float-above{-webkit-transform:translateY(-106%) scale(0.75);transform:translateY(-106%) scale(0.75)}.mdc-select__anchor.mdc-select--focused .mdc-line-ripple::after{-webkit-transform:scale(1, 2);transform:scale(1, 2);opacity:1}.mdc-select__anchor+.mdc-select-helper-text{margin-right:16px;margin-left:16px}.mdc-select--focused .mdc-select__anchor+.mdc-select-helper-text:not(.mdc-select-helper-text--validation-msg){opacity:1}.mdc-select__selected-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);line-height:1.75rem;line-height:var(--mdc-typography-subtitle1-line-height, 1.75rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);box-sizing:border-box;width:0;flex-grow:1;height:28px;border:none;outline:none;padding:0;white-space:nowrap;-webkit-appearance:none;-moz-appearance:none;appearance:none;pointer-events:none;text-overflow:ellipsis;background-color:transparent;color:inherit}.mdc-select__selected-text::-ms-expand{display:none}.mdc-select__selected-text::-ms-value{background-color:transparent;color:inherit}.mdc-select--outlined{border:none}.mdc-select--outlined:not(.mdc-select--disabled) .mdc-select__anchor{background-color:transparent}.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__trailing{border-color:rgba(0,0,0,.38)}.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:rgba(0,0,0,.87)}.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-width:2px}.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading{border-radius:4px 0 0 4px}[dir=rtl] .mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading[dir=rtl]{border-radius:0 4px 4px 0}.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing{border-radius:0 4px 4px 0}[dir=rtl] .mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing,.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing[dir=rtl]{border-radius:4px 0 0 4px}.mdc-select--outlined .mdc-select__selected-text{border-radius:4px}.mdc-select--outlined:not(.mdc-select--disabled) .mdc-select__anchor{background-color:transparent}.mdc-select--outlined .mdc-select__anchor{overflow:visible}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--shake{-webkit-animation:mdc-floating-label-shake-float-above-select-outlined 250ms 1;animation:mdc-floating-label-shake-float-above-select-outlined 250ms 1}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{-webkit-transform:translateY(-37.25px) scale(1);transform:translateY(-37.25px) scale(1)}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{-webkit-transform:translateY(-34.75px) scale(0.75);transform:translateY(-34.75px) scale(0.75)}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:1px}.mdc-select--outlined .mdc-select__selected-text{display:flex;border:none;z-index:1;background-color:transparent}.mdc-select--outlined .mdc-select__icon{z-index:2}.mdc-select--outlined .mdc-floating-label{line-height:1.15rem;pointer-events:auto}.mdc-select--outlined.mdc-select--focused .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:2px}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-floating-label{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-line-ripple::after{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-floating-label{color:#b00020}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--invalid .mdc-select__anchor+.mdc-select-helper-text--validation-msg{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__leading,.mdc-select--invalid.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__notch,.mdc-select--invalid.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--invalid.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--invalid.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--invalid.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--invalid.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-width:2px}.mdc-select--invalid.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--invalid.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--invalid.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid .mdc-select__dropdown-icon{background:url("data:image/svg+xml,%3Csvg%20width%3D%2210px%22%20height%3D%225px%22%20viewBox%3D%227%2010%2010%205%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%0A%20%20%20%20%3Cpolygon%20id%3D%22Shape%22%20stroke%3D%22none%22%20fill%3D%22%23b00020%22%20fill-rule%3D%22evenodd%22%20opacity%3D%221%22%20points%3D%227%2010%2012%2015%2017%2010%22%3E%3C%2Fpolygon%3E%0A%3C%2Fsvg%3E") no-repeat center}.mdc-select--invalid+.mdc-select-helper-text--validation-msg{opacity:1}.mdc-select--required .mdc-floating-label::after{content:"*"}.mdc-select--disabled{cursor:default;pointer-events:none}.mdc-select--disabled .mdc-select__anchor{background-color:#fafafa}.mdc-select--disabled .mdc-floating-label{color:rgba(0,0,0,.38)}.mdc-select--disabled .mdc-select__dropdown-icon{background:url("data:image/svg+xml,%3Csvg%20width%3D%2210px%22%20height%3D%225px%22%20viewBox%3D%227%2010%2010%205%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%0A%20%20%20%20%3Cpolygon%20id%3D%22Shape%22%20stroke%3D%22none%22%20fill%3D%22%23000%22%20fill-rule%3D%22evenodd%22%20opacity%3D%220.38%22%20points%3D%227%2010%2012%2015%2017%2010%22%3E%3C%2Fpolygon%3E%0A%3C%2Fsvg%3E") no-repeat center}.mdc-select--disabled .mdc-line-ripple::before{border-bottom-color:rgba(0,0,0,.38)}.mdc-select--disabled .mdc-line-ripple::before{border-bottom-style:dotted}.mdc-select--disabled .mdc-select__icon{color:rgba(0,0,0,.38)}.mdc-select--disabled .mdc-select__selected-text{color:rgba(0,0,0,.38);pointer-events:none}.mdc-select--disabled.mdc-select--outlined .mdc-select__anchor{background-color:transparent}.mdc-select--disabled.mdc-select--outlined .mdc-notched-outline__leading,.mdc-select--disabled.mdc-select--outlined .mdc-notched-outline__notch,.mdc-select--disabled.mdc-select--outlined .mdc-notched-outline__trailing{border-color:rgba(0,0,0,.16)}.mdc-select--with-leading-icon .mdc-floating-label{left:48px;right:initial}[dir=rtl] .mdc-select--with-leading-icon .mdc-floating-label,.mdc-select--with-leading-icon .mdc-floating-label[dir=rtl]{left:initial;right:48px}.mdc-select--with-leading-icon.mdc-select--outlined .mdc-floating-label{left:36px;right:initial}[dir=rtl] .mdc-select--with-leading-icon.mdc-select--outlined .mdc-floating-label,.mdc-select--with-leading-icon.mdc-select--outlined .mdc-floating-label[dir=rtl]{left:initial;right:36px}.mdc-select--with-leading-icon.mdc-select--outlined .mdc-floating-label--float-above{left:36px;right:initial}[dir=rtl] .mdc-select--with-leading-icon.mdc-select--outlined .mdc-floating-label--float-above,.mdc-select--with-leading-icon.mdc-select--outlined .mdc-floating-label--float-above[dir=rtl]{left:initial;right:36px}.mdc-select--with-leading-icon.mdc-select--outlined .mdc-floating-label--float-above{-webkit-transform:translateY(-37.25px) translateX(-32px) scale(1);transform:translateY(-37.25px) translateX(-32px) scale(1)}[dir=rtl] .mdc-select--with-leading-icon.mdc-select--outlined .mdc-floating-label--float-above,.mdc-select--with-leading-icon.mdc-select--outlined .mdc-floating-label--float-above[dir=rtl]{-webkit-transform:translateY(-37.25px) translateX(32px) scale(1);transform:translateY(-37.25px) translateX(32px) scale(1)}.mdc-select--with-leading-icon.mdc-select--outlined .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--with-leading-icon.mdc-select--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--with-leading-icon.mdc-select--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{-webkit-transform:translateY(-34.75px) translateX(-32px) scale(0.75);transform:translateY(-34.75px) translateX(-32px) scale(0.75)}[dir=rtl] .mdc-select--with-leading-icon.mdc-select--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--with-leading-icon.mdc-select--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl],[dir=rtl] .mdc-select--with-leading-icon.mdc-select--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--with-leading-icon.mdc-select--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl]{-webkit-transform:translateY(-34.75px) translateX(32px) scale(0.75);transform:translateY(-34.75px) translateX(32px) scale(0.75)}.mdc-select--with-leading-icon.mdc-select--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--with-leading-icon.mdc-select--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--with-leading-icon.mdc-select--outlined .mdc-floating-label--shake{-webkit-animation:mdc-floating-label-shake-float-above-select-outlined-leading-icon 250ms 1;animation:mdc-floating-label-shake-float-above-select-outlined-leading-icon 250ms 1}[dir=rtl] .mdc-select--with-leading-icon.mdc-select--outlined .mdc-floating-label--shake,.mdc-select--with-leading-icon.mdc-select--outlined[dir=rtl] .mdc-floating-label--shake{-webkit-animation:mdc-floating-label-shake-float-above-select-outlined-leading-icon-rtl 250ms 1;animation:mdc-floating-label-shake-float-above-select-outlined-leading-icon-rtl 250ms 1}.mdc-select--with-leading-icon.mdc-select__menu .mdc-list-item__text{padding-left:32px;padding-right:32px}[dir=rtl] .mdc-select--with-leading-icon.mdc-select__menu .mdc-list-item__text,.mdc-select--with-leading-icon.mdc-select__menu .mdc-list-item__text[dir=rtl]{padding-left:32px;padding-right:32px}.mdc-select__menu .mdc-list .mdc-list-item--selected{color:#000;color:var(--mdc-theme-on-surface, #000)}@-webkit-keyframes mdc-floating-label-shake-float-above-select-outlined-leading-icon{0%{-webkit-transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}}@keyframes mdc-floating-label-shake-float-above-select-outlined-leading-icon{0%{-webkit-transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}}@-webkit-keyframes mdc-floating-label-shake-float-above-select-outlined-leading-icon-rtl{0%{-webkit-transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}}@keyframes mdc-floating-label-shake-float-above-select-outlined-leading-icon-rtl{0%{-webkit-transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}}@-webkit-keyframes mdc-slider-emphasize{0%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;-webkit-transform:scale(0.85);transform:scale(0.85)}100%{-webkit-transform:scale(0.571);transform:scale(0.571)}}@keyframes mdc-slider-emphasize{0%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;-webkit-transform:scale(0.85);transform:scale(0.85)}100%{-webkit-transform:scale(0.571);transform:scale(0.571)}}.mdc-slider{position:relative;width:100%;height:48px;cursor:pointer;touch-action:pan-x;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mdc-slider:not(.mdc-slider--disabled) .mdc-slider__track{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-slider:not(.mdc-slider--disabled) .mdc-slider__track-container::after{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786);opacity:.26}.mdc-slider:not(.mdc-slider--disabled) .mdc-slider__track-marker-container{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-slider:not(.mdc-slider--disabled) .mdc-slider__thumb{fill:#018786;fill:var(--mdc-theme-secondary, #018786);stroke:#018786;stroke:var(--mdc-theme-secondary, #018786)}.mdc-slider:not(.mdc-slider--disabled) .mdc-slider__focus-ring{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-slider:not(.mdc-slider--disabled) .mdc-slider__pin{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-slider:not(.mdc-slider--disabled) .mdc-slider__pin{color:#fff;color:var(--mdc-theme-text-primary-on-dark, white)}.mdc-slider--disabled{cursor:auto}.mdc-slider--disabled .mdc-slider__track{background-color:#9a9a9a}.mdc-slider--disabled .mdc-slider__track-container::after{background-color:#9a9a9a;opacity:.26}.mdc-slider--disabled .mdc-slider__track-marker-container{background-color:#9a9a9a}.mdc-slider--disabled .mdc-slider__thumb{fill:#9a9a9a;stroke:#9a9a9a}.mdc-slider--disabled .mdc-slider__thumb{stroke:#fff;stroke:var(--mdc-slider-bg-color-behind-component, white)}.mdc-slider:focus{outline:none}.mdc-slider__track-container{position:absolute;top:50%;width:100%;height:2px;overflow:hidden}.mdc-slider__track-container::after{position:absolute;top:0;left:0;display:block;width:100%;height:100%;content:""}.mdc-slider__track{position:absolute;width:100%;height:100%;-webkit-transform-origin:left top;transform-origin:left top;will-change:transform}.mdc-slider[dir=rtl] .mdc-slider__track,[dir=rtl] .mdc-slider .mdc-slider__track{-webkit-transform-origin:right top;transform-origin:right top}.mdc-slider__track-marker-container{display:flex;margin-right:0;margin-left:-1px;visibility:hidden}.mdc-slider[dir=rtl] .mdc-slider__track-marker-container,[dir=rtl] .mdc-slider .mdc-slider__track-marker-container{margin-right:-1px;margin-left:0}.mdc-slider__track-marker-container::after{display:block;width:2px;height:2px;content:""}.mdc-slider__track-marker{flex:1}.mdc-slider__track-marker::after{display:block;width:2px;height:2px;content:""}.mdc-slider__track-marker:first-child::after{width:3px}.mdc-slider__thumb-container{position:absolute;top:15px;left:0;width:21px;height:100%;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;will-change:transform}.mdc-slider__thumb{position:absolute;top:0;left:0;-webkit-transform:scale(0.571);transform:scale(0.571);stroke-width:3.5;transition:fill 100ms ease-out,stroke 100ms ease-out,-webkit-transform 100ms ease-out;transition:transform 100ms ease-out,fill 100ms ease-out,stroke 100ms ease-out;transition:transform 100ms ease-out,fill 100ms ease-out,stroke 100ms ease-out,-webkit-transform 100ms ease-out}.mdc-slider__focus-ring{width:21px;height:21px;border-radius:50%;opacity:0;transition:opacity 266.67ms ease-out,background-color 266.67ms ease-out,-webkit-transform 266.67ms ease-out;transition:transform 266.67ms ease-out,opacity 266.67ms ease-out,background-color 266.67ms ease-out;transition:transform 266.67ms ease-out,opacity 266.67ms ease-out,background-color 266.67ms ease-out,-webkit-transform 266.67ms ease-out}.mdc-slider__pin{display:flex;position:absolute;top:0;left:0;align-items:center;justify-content:center;width:26px;height:26px;margin-top:-2px;margin-left:-2px;-webkit-transform:rotate(-45deg) scale(0) translate(0, 0);transform:rotate(-45deg) scale(0) translate(0, 0);border-radius:50% 50% 50% 0%;z-index:1;transition:-webkit-transform 100ms ease-out;transition:transform 100ms ease-out;transition:transform 100ms ease-out, -webkit-transform 100ms ease-out}.mdc-slider__pin-value-marker{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);-webkit-transform:rotate(45deg);transform:rotate(45deg)}.mdc-slider--active .mdc-slider__thumb{-webkit-transform:scale3d(1, 1, 1);transform:scale3d(1, 1, 1)}.mdc-slider--focus .mdc-slider__thumb{-webkit-animation:mdc-slider-emphasize 266.67ms linear;animation:mdc-slider-emphasize 266.67ms linear}.mdc-slider--focus .mdc-slider__focus-ring{-webkit-transform:scale3d(1.55, 1.55, 1.55);transform:scale3d(1.55, 1.55, 1.55);opacity:.25}.mdc-slider--in-transit .mdc-slider__thumb{transition-delay:140ms}.mdc-slider--in-transit .mdc-slider__thumb-container,.mdc-slider--in-transit .mdc-slider__track,.mdc-slider:focus:not(.mdc-slider--active) .mdc-slider__thumb-container,.mdc-slider:focus:not(.mdc-slider--active) .mdc-slider__track{transition:-webkit-transform 80ms ease;transition:transform 80ms ease;transition:transform 80ms ease, -webkit-transform 80ms ease}.mdc-slider--discrete.mdc-slider--active .mdc-slider__thumb{-webkit-transform:scale(calc(12 / 21));transform:scale(calc(12 / 21))}.mdc-slider--discrete.mdc-slider--active .mdc-slider__pin{-webkit-transform:rotate(-45deg) scale(1) translate(19px, -20px);transform:rotate(-45deg) scale(1) translate(19px, -20px)}.mdc-slider--discrete.mdc-slider--focus .mdc-slider__thumb{-webkit-animation:none;animation:none}.mdc-slider--discrete.mdc-slider--display-markers .mdc-slider__track-marker-container{visibility:visible}.mdc-snackbar{z-index:8;margin:8px;display:none;position:fixed;right:0;bottom:0;left:0;align-items:center;justify-content:center;box-sizing:border-box;pointer-events:none;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mdc-snackbar__surface{background-color:#333}.mdc-snackbar__label{color:rgba(255,255,255,.87)}.mdc-snackbar__surface{min-width:344px}@media(max-width: 480px),(max-width: 344px){.mdc-snackbar__surface{min-width:100%}}.mdc-snackbar__surface{max-width:672px}.mdc-snackbar__surface{box-shadow:0px 3px 5px -1px rgba(0, 0, 0, 0.2),0px 6px 10px 0px rgba(0, 0, 0, 0.14),0px 1px 18px 0px rgba(0,0,0,.12)}.mdc-snackbar__surface{border-radius:4px}.mdc-snackbar--opening,.mdc-snackbar--open,.mdc-snackbar--closing{display:flex}.mdc-snackbar--leading{justify-content:flex-start}.mdc-snackbar--stacked .mdc-snackbar__label{padding-left:16px;padding-right:0;padding-bottom:12px}[dir=rtl] .mdc-snackbar--stacked .mdc-snackbar__label,.mdc-snackbar--stacked .mdc-snackbar__label[dir=rtl]{padding-left:0;padding-right:16px}.mdc-snackbar--stacked .mdc-snackbar__surface{flex-direction:column;align-items:flex-start}.mdc-snackbar--stacked .mdc-snackbar__actions{align-self:flex-end;margin-bottom:8px}.mdc-snackbar__surface{padding-left:0;padding-right:8px;display:flex;align-items:center;justify-content:flex-start;box-sizing:border-box;-webkit-transform:scale(0.8);transform:scale(0.8);opacity:0}[dir=rtl] .mdc-snackbar__surface,.mdc-snackbar__surface[dir=rtl]{padding-left:8px;padding-right:0}.mdc-snackbar--open .mdc-snackbar__surface{-webkit-transform:scale(1);transform:scale(1);opacity:1;pointer-events:auto;transition:opacity 150ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 150ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:opacity 150ms 0ms cubic-bezier(0, 0, 0.2, 1),transform 150ms 0ms cubic-bezier(0, 0, 0.2, 1);transition:opacity 150ms 0ms cubic-bezier(0, 0, 0.2, 1),transform 150ms 0ms cubic-bezier(0, 0, 0.2, 1),-webkit-transform 150ms 0ms cubic-bezier(0, 0, 0.2, 1)}.mdc-snackbar--closing .mdc-snackbar__surface{-webkit-transform:scale(1);transform:scale(1);transition:opacity 75ms 0ms cubic-bezier(0.4, 0, 1, 1)}.mdc-snackbar__label{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);padding-left:16px;padding-right:8px;width:100%;flex-grow:1;box-sizing:border-box;margin:0;padding-top:14px;padding-bottom:14px}[dir=rtl] .mdc-snackbar__label,.mdc-snackbar__label[dir=rtl]{padding-left:8px;padding-right:16px}.mdc-snackbar__label::before{display:inline;content:attr(data-mdc-snackbar-label-text)}.mdc-snackbar__actions{display:flex;flex-shrink:0;align-items:center;box-sizing:border-box}.mdc-snackbar__action:not(:disabled){color:#bb86fc}.mdc-snackbar__action::before,.mdc-snackbar__action::after{background-color:#bb86fc}.mdc-snackbar__action:hover::before{opacity:.08}.mdc-snackbar__action.mdc-ripple-upgraded--background-focused::before,.mdc-snackbar__action:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.24}.mdc-snackbar__action:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-snackbar__action:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.24}.mdc-snackbar__action.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.24}.mdc-snackbar__dismiss{color:rgba(255,255,255,.87)}.mdc-snackbar__dismiss::before,.mdc-snackbar__dismiss::after{background-color:rgba(255,255,255,.87)}.mdc-snackbar__dismiss:hover::before{opacity:.08}.mdc-snackbar__dismiss.mdc-ripple-upgraded--background-focused::before,.mdc-snackbar__dismiss:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.24}.mdc-snackbar__dismiss:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-snackbar__dismiss:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.24}.mdc-snackbar__dismiss.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.24}.mdc-snackbar__dismiss.mdc-snackbar__dismiss{width:36px;height:36px;padding:9px;font-size:18px}.mdc-snackbar__dismiss.mdc-snackbar__dismiss svg,.mdc-snackbar__dismiss.mdc-snackbar__dismiss img{width:18px;height:18px}.mdc-snackbar__action+.mdc-snackbar__dismiss{margin-left:8px;margin-right:0}[dir=rtl] .mdc-snackbar__action+.mdc-snackbar__dismiss,.mdc-snackbar__action+.mdc-snackbar__dismiss[dir=rtl]{margin-left:0;margin-right:8px}.mdc-switch__thumb-underlay{left:-18px;right:initial;top:-17px;width:48px;height:48px}[dir=rtl] .mdc-switch__thumb-underlay,.mdc-switch__thumb-underlay[dir=rtl]{left:initial;right:-18px}.mdc-switch__native-control{width:68px;height:48px}.mdc-switch{display:inline-block;position:relative;outline:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdc-switch.mdc-switch--checked .mdc-switch__track{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-switch.mdc-switch--checked .mdc-switch__thumb{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786);border-color:#018786;border-color:var(--mdc-theme-secondary, #018786)}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track{background-color:#000;background-color:var(--mdc-theme-on-surface, #000)}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb{background-color:#fff;background-color:var(--mdc-theme-surface, #fff);border-color:#fff;border-color:var(--mdc-theme-surface, #fff)}.mdc-switch__native-control{left:0;right:initial;position:absolute;top:0;margin:0;opacity:0;cursor:pointer;pointer-events:auto;transition:-webkit-transform 90ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 90ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 90ms cubic-bezier(0.4, 0, 0.2, 1), -webkit-transform 90ms cubic-bezier(0.4, 0, 0.2, 1)}[dir=rtl] .mdc-switch__native-control,.mdc-switch__native-control[dir=rtl]{left:initial;right:0}.mdc-switch__track{box-sizing:border-box;width:32px;height:14px;border:1px solid transparent;border-radius:7px;opacity:.38;transition:opacity 90ms cubic-bezier(0.4, 0, 0.2, 1),background-color 90ms cubic-bezier(0.4, 0, 0.2, 1),border-color 90ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-switch__thumb-underlay{display:flex;position:absolute;align-items:center;justify-content:center;-webkit-transform:translateX(0);transform:translateX(0);transition:background-color 90ms cubic-bezier(0.4, 0, 0.2, 1),border-color 90ms cubic-bezier(0.4, 0, 0.2, 1),-webkit-transform 90ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 90ms cubic-bezier(0.4, 0, 0.2, 1),background-color 90ms cubic-bezier(0.4, 0, 0.2, 1),border-color 90ms cubic-bezier(0.4, 0, 0.2, 1);transition:transform 90ms cubic-bezier(0.4, 0, 0.2, 1),background-color 90ms cubic-bezier(0.4, 0, 0.2, 1),border-color 90ms cubic-bezier(0.4, 0, 0.2, 1),-webkit-transform 90ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-switch__thumb{box-shadow:0px 3px 1px -2px rgba(0, 0, 0, 0.2),0px 2px 2px 0px rgba(0, 0, 0, 0.14),0px 1px 5px 0px rgba(0,0,0,.12);box-sizing:border-box;width:20px;height:20px;border:10px solid;border-radius:50%;pointer-events:none;z-index:1}.mdc-switch--checked .mdc-switch__track{opacity:.54}.mdc-switch--checked .mdc-switch__thumb-underlay{-webkit-transform:translateX(20px);transform:translateX(20px)}[dir=rtl] .mdc-switch--checked .mdc-switch__thumb-underlay,.mdc-switch--checked .mdc-switch__thumb-underlay[dir=rtl]{-webkit-transform:translateX(-20px);transform:translateX(-20px)}.mdc-switch--checked .mdc-switch__native-control{-webkit-transform:translateX(-20px);transform:translateX(-20px)}[dir=rtl] .mdc-switch--checked .mdc-switch__native-control,.mdc-switch--checked .mdc-switch__native-control[dir=rtl]{-webkit-transform:translateX(20px);transform:translateX(20px)}.mdc-switch--disabled{opacity:.38;pointer-events:none}.mdc-switch--disabled .mdc-switch__thumb{border-width:1px}.mdc-switch--disabled .mdc-switch__native-control{cursor:default;pointer-events:none}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb-underlay::before,.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb-underlay::after{background-color:#9e9e9e}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb-underlay:hover::before{opacity:.08}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb-underlay.mdc-ripple-upgraded--background-focused::before,.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb-underlay:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.24}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb-underlay:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb-underlay:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.24}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb-underlay.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.24}.mdc-switch__thumb-underlay{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mdc-switch__thumb-underlay::before,.mdc-switch__thumb-underlay::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-switch__thumb-underlay::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-switch__thumb-underlay.mdc-ripple-upgraded::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-switch__thumb-underlay.mdc-ripple-upgraded::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}.mdc-switch__thumb-underlay.mdc-ripple-upgraded--unbounded::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-switch__thumb-underlay.mdc-ripple-upgraded--foreground-activation::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-switch__thumb-underlay.mdc-ripple-upgraded--foreground-deactivation::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-switch__thumb-underlay::before,.mdc-switch__thumb-underlay::after{top:calc(50% - 50%);left:calc(50% - 50%);width:100%;height:100%}.mdc-switch__thumb-underlay.mdc-ripple-upgraded::before,.mdc-switch__thumb-underlay.mdc-ripple-upgraded::after{top:var(--mdc-ripple-top, calc(50% - 50%));left:var(--mdc-ripple-left, calc(50% - 50%));width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-switch__thumb-underlay.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-switch__thumb-underlay::before,.mdc-switch__thumb-underlay::after{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-switch__thumb-underlay:hover::before{opacity:.04}.mdc-switch__thumb-underlay.mdc-ripple-upgraded--background-focused::before,.mdc-switch__thumb-underlay:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.12}.mdc-switch__thumb-underlay:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-switch__thumb-underlay:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.12}.mdc-switch__thumb-underlay.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-tab{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-button-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-button-font-size, 0.875rem);line-height:2.25rem;line-height:var(--mdc-typography-button-line-height, 2.25rem);font-weight:500;font-weight:var(--mdc-typography-button-font-weight, 500);letter-spacing:.0892857143em;letter-spacing:var(--mdc-typography-button-letter-spacing, 0.0892857143em);text-decoration:none;-webkit-text-decoration:var(--mdc-typography-button-text-decoration, none);text-decoration:var(--mdc-typography-button-text-decoration, none);text-transform:uppercase;text-transform:var(--mdc-typography-button-text-transform, uppercase);padding-right:24px;padding-left:24px;position:relative;display:flex;flex:1 0 auto;justify-content:center;box-sizing:border-box;margin:0;padding-top:0;padding-bottom:0;border:none;outline:none;background:none;text-align:center;white-space:nowrap;cursor:pointer;-webkit-appearance:none;z-index:1}.mdc-tab .mdc-tab__text-label{color:rgba(0,0,0,.6)}.mdc-tab .mdc-tab__icon{color:rgba(0,0,0,.54);fill:currentColor}.mdc-tab::-moz-focus-inner{padding:0;border:0}.mdc-tab--min-width{flex:0 1 auto}.mdc-tab__content{position:relative;display:flex;align-items:center;justify-content:center;height:inherit;pointer-events:none}.mdc-tab__text-label{transition:150ms color linear;display:inline-block;line-height:1;z-index:2}.mdc-tab__icon{transition:150ms color linear;width:24px;height:24px;font-size:24px;z-index:2}.mdc-tab--stacked .mdc-tab__content{flex-direction:column;align-items:center;justify-content:center}.mdc-tab--stacked .mdc-tab__text-label{padding-top:6px;padding-bottom:4px}.mdc-tab--active .mdc-tab__text-label{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}.mdc-tab--active .mdc-tab__icon{color:#6200ee;color:var(--mdc-theme-primary, #6200ee);fill:currentColor}.mdc-tab--active .mdc-tab__text-label,.mdc-tab--active .mdc-tab__icon{transition-delay:100ms}.mdc-tab:not(.mdc-tab--stacked) .mdc-tab__icon+.mdc-tab__text-label{padding-left:8px;padding-right:0}[dir=rtl] .mdc-tab:not(.mdc-tab--stacked) .mdc-tab__icon+.mdc-tab__text-label,.mdc-tab:not(.mdc-tab--stacked) .mdc-tab__icon+.mdc-tab__text-label[dir=rtl]{padding-left:0;padding-right:8px}.mdc-tab__ripple{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden}.mdc-tab__ripple::before,.mdc-tab__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-tab__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-tab__ripple.mdc-ripple-upgraded::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-tab__ripple.mdc-ripple-upgraded::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}.mdc-tab__ripple.mdc-ripple-upgraded--unbounded::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-tab__ripple.mdc-ripple-upgraded--foreground-activation::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-tab__ripple.mdc-ripple-upgraded--foreground-deactivation::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-tab__ripple::before,.mdc-tab__ripple::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-tab__ripple.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-tab__ripple::before,.mdc-tab__ripple::after{background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee)}.mdc-tab__ripple:hover::before{opacity:.04}.mdc-tab__ripple.mdc-ripple-upgraded--background-focused::before,.mdc-tab__ripple:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.12}.mdc-tab__ripple:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-tab__ripple:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.12}.mdc-tab__ripple.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.12}.mdc-tab-bar{width:100%}.mdc-tab{height:48px}.mdc-tab--stacked{height:72px}.mdc-tab-indicator{display:flex;position:absolute;top:0;left:0;justify-content:center;width:100%;height:100%;pointer-events:none;z-index:1}.mdc-tab-indicator .mdc-tab-indicator__content--underline{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-tab-indicator .mdc-tab-indicator__content--icon{color:#018786;color:var(--mdc-theme-secondary, #018786)}.mdc-tab-indicator .mdc-tab-indicator__content--underline{border-top-width:2px}.mdc-tab-indicator .mdc-tab-indicator__content--icon{height:34px;font-size:34px}.mdc-tab-indicator__content{-webkit-transform-origin:left;transform-origin:left;opacity:0}.mdc-tab-indicator__content--underline{align-self:flex-end;box-sizing:border-box;width:100%;border-top-style:solid}.mdc-tab-indicator__content--icon{align-self:center;margin:0 auto}.mdc-tab-indicator--active .mdc-tab-indicator__content{opacity:1}.mdc-tab-indicator .mdc-tab-indicator__content{transition:250ms -webkit-transform cubic-bezier(0.4, 0, 0.2, 1);transition:250ms transform cubic-bezier(0.4, 0, 0.2, 1);transition:250ms transform cubic-bezier(0.4, 0, 0.2, 1), 250ms -webkit-transform cubic-bezier(0.4, 0, 0.2, 1)}.mdc-tab-indicator--no-transition .mdc-tab-indicator__content{transition:none}.mdc-tab-indicator--fade .mdc-tab-indicator__content{transition:150ms opacity linear}.mdc-tab-indicator--active.mdc-tab-indicator--fade .mdc-tab-indicator__content{transition-delay:100ms}.mdc-tab-scroller{overflow-y:hidden}.mdc-tab-scroller.mdc-tab-scroller--animating .mdc-tab-scroller__scroll-content{transition:250ms -webkit-transform cubic-bezier(0.4, 0, 0.2, 1);transition:250ms transform cubic-bezier(0.4, 0, 0.2, 1);transition:250ms transform cubic-bezier(0.4, 0, 0.2, 1), 250ms -webkit-transform cubic-bezier(0.4, 0, 0.2, 1)}.mdc-tab-scroller__test{position:absolute;top:-9999px;width:100px;height:100px;overflow-x:scroll}.mdc-tab-scroller__scroll-area{-webkit-overflow-scrolling:touch;display:flex;overflow-x:hidden}.mdc-tab-scroller__scroll-area::-webkit-scrollbar,.mdc-tab-scroller__test::-webkit-scrollbar{display:none}.mdc-tab-scroller__scroll-area--scroll{overflow-x:scroll}.mdc-tab-scroller__scroll-content{position:relative;display:flex;flex:1 0 auto;-webkit-transform:none;transform:none;will-change:transform}.mdc-tab-scroller--align-start .mdc-tab-scroller__scroll-content{justify-content:flex-start}.mdc-tab-scroller--align-end .mdc-tab-scroller__scroll-content{justify-content:flex-end}.mdc-tab-scroller--align-center .mdc-tab-scroller__scroll-content{justify-content:center}.mdc-tab-scroller--animating .mdc-tab-scroller__scroll-area{-webkit-overflow-scrolling:auto}.mdc-text-field-helper-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit);display:block;margin-top:0;line-height:normal;margin:0;opacity:0;will-change:opacity;transition:opacity 150ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-text-field-helper-text::before{display:inline-block;width:0;height:16px;content:"";vertical-align:0}.mdc-text-field-helper-text--persistent{transition:none;opacity:1;will-change:initial}.mdc-text-field-character-counter{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit);display:block;margin-top:0;line-height:normal;margin-left:auto;margin-right:0;padding-left:16px;padding-right:0;white-space:nowrap}.mdc-text-field-character-counter::before{display:inline-block;width:0;height:16px;content:"";vertical-align:0}[dir=rtl] .mdc-text-field-character-counter,.mdc-text-field-character-counter[dir=rtl]{margin-left:0;margin-right:auto}[dir=rtl] .mdc-text-field-character-counter,.mdc-text-field-character-counter[dir=rtl]{padding-left:0;padding-right:16px}.mdc-text-field__icon{align-self:center;cursor:pointer}.mdc-text-field__icon:not([tabindex]),.mdc-text-field__icon[tabindex="-1"]{cursor:default;pointer-events:none}.mdc-text-field__icon--leading{margin-left:16px;margin-right:8px}[dir=rtl] .mdc-text-field__icon--leading,.mdc-text-field__icon--leading[dir=rtl]{margin-left:8px;margin-right:16px}.mdc-text-field__icon--trailing{margin-left:12px;margin-right:12px}[dir=rtl] .mdc-text-field__icon--trailing,.mdc-text-field__icon--trailing[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-text-field--filled{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mdc-text-field--filled .mdc-text-field__ripple::before,.mdc-text-field--filled .mdc-text-field__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-text-field--filled .mdc-text-field__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-text-field--filled.mdc-ripple-upgraded .mdc-text-field__ripple::before{-webkit-transform:scale(var(--mdc-ripple-fg-scale, 1));transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-text-field--filled.mdc-ripple-upgraded .mdc-text-field__ripple::after{top:0;left:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:center center;transform-origin:center center}.mdc-text-field--filled.mdc-ripple-upgraded--unbounded .mdc-text-field__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-text-field--filled.mdc-ripple-upgraded--foreground-activation .mdc-text-field__ripple::after{-webkit-animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards;animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-text-field--filled.mdc-ripple-upgraded--foreground-deactivation .mdc-text-field__ripple::after{-webkit-animation:mdc-ripple-fg-opacity-out 150ms;animation:mdc-ripple-fg-opacity-out 150ms;-webkit-transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1));transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-text-field--filled .mdc-text-field__ripple::before,.mdc-text-field--filled .mdc-text-field__ripple::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-text-field--filled.mdc-ripple-upgraded .mdc-text-field__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-text-field__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.mdc-text-field{border-radius:4px 4px 0 0;padding:0 16px;display:inline-flex;align-items:baseline;position:relative;box-sizing:border-box;overflow:hidden;will-change:opacity,transform,color}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-floating-label{color:rgba(0,0,0,.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input{color:rgba(0,0,0,.87)}@media all{.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input::-webkit-input-placeholder{color:rgba(0,0,0,.54)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input:-ms-input-placeholder{color:rgba(0,0,0,.54)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input::-ms-input-placeholder{color:rgba(0,0,0,.54)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input::placeholder{color:rgba(0,0,0,.54)}}@media all{.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input:-ms-input-placeholder{color:rgba(0,0,0,.54)}}.mdc-text-field .mdc-text-field__input{caret-color:#6200ee;caret-color:var(--mdc-theme-primary, #6200ee)}.mdc-text-field:not(.mdc-text-field--disabled)+.mdc-text-field-helper-line .mdc-text-field-helper-text{color:rgba(0,0,0,.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field-character-counter,.mdc-text-field:not(.mdc-text-field--disabled)+.mdc-text-field-helper-line .mdc-text-field-character-counter{color:rgba(0,0,0,.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon--leading{color:rgba(0,0,0,.54)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon--trailing{color:rgba(0,0,0,.54)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__affix--prefix{color:rgba(0,0,0,.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__affix--suffix{color:rgba(0,0,0,.6)}.mdc-text-field .mdc-floating-label{top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);pointer-events:none}.mdc-text-field.mdc-text-field--with-leading-icon{padding-left:0;padding-right:16px}[dir=rtl] .mdc-text-field.mdc-text-field--with-leading-icon,.mdc-text-field.mdc-text-field--with-leading-icon[dir=rtl]{padding-left:16px;padding-right:0}.mdc-text-field.mdc-text-field--with-trailing-icon{padding-left:16px;padding-right:0}[dir=rtl] .mdc-text-field.mdc-text-field--with-trailing-icon,.mdc-text-field.mdc-text-field--with-trailing-icon[dir=rtl]{padding-left:0;padding-right:16px}.mdc-text-field.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon{padding-left:0;padding-right:0}[dir=rtl] .mdc-text-field.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon,.mdc-text-field.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon[dir=rtl]{padding-left:0;padding-right:0}.mdc-text-field__input{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);height:28px;transition:opacity 150ms cubic-bezier(0.4, 0, 0.2, 1);width:100%;min-width:0;border:none;border-radius:0;background:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0}.mdc-text-field__input::-ms-clear{display:none}.mdc-text-field__input:focus{outline:none}.mdc-text-field__input:invalid{box-shadow:none}.mdc-text-field__input:-webkit-autofill{z-index:auto !important}@media all{.mdc-text-field__input::-webkit-input-placeholder{transition:opacity 67ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0}.mdc-text-field__input:-ms-input-placeholder{transition:opacity 67ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0}.mdc-text-field__input::-ms-input-placeholder{transition:opacity 67ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0}.mdc-text-field__input::placeholder{transition:opacity 67ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0}}@media all{.mdc-text-field__input:-ms-input-placeholder{transition:opacity 67ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0}}@media all{.mdc-text-field--fullwidth .mdc-text-field__input::-webkit-input-placeholder,.mdc-text-field--no-label .mdc-text-field__input::-webkit-input-placeholder,.mdc-text-field--focused .mdc-text-field__input::-webkit-input-placeholder{transition-delay:40ms;transition-duration:110ms;opacity:1}.mdc-text-field--fullwidth .mdc-text-field__input:-ms-input-placeholder,.mdc-text-field--no-label .mdc-text-field__input:-ms-input-placeholder,.mdc-text-field--focused .mdc-text-field__input:-ms-input-placeholder{transition-delay:40ms;transition-duration:110ms;opacity:1}.mdc-text-field--fullwidth .mdc-text-field__input::-ms-input-placeholder,.mdc-text-field--no-label .mdc-text-field__input::-ms-input-placeholder,.mdc-text-field--focused .mdc-text-field__input::-ms-input-placeholder{transition-delay:40ms;transition-duration:110ms;opacity:1}.mdc-text-field--fullwidth .mdc-text-field__input::placeholder,.mdc-text-field--no-label .mdc-text-field__input::placeholder,.mdc-text-field--focused .mdc-text-field__input::placeholder{transition-delay:40ms;transition-duration:110ms;opacity:1}}@media all{.mdc-text-field--fullwidth .mdc-text-field__input:-ms-input-placeholder,.mdc-text-field--no-label .mdc-text-field__input:-ms-input-placeholder,.mdc-text-field--focused .mdc-text-field__input:-ms-input-placeholder{transition-delay:40ms;transition-duration:110ms;opacity:1}}.mdc-text-field__affix{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);height:28px;transition:opacity 150ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0;white-space:nowrap}.mdc-text-field--label-floating .mdc-text-field__affix,.mdc-text-field--no-label .mdc-text-field__affix{opacity:1}.mdc-text-field__affix--prefix{padding-left:0;padding-right:2px}[dir=rtl] .mdc-text-field__affix--prefix,.mdc-text-field__affix--prefix[dir=rtl]{padding-left:2px;padding-right:0}.mdc-text-field--end-aligned .mdc-text-field__affix--prefix{padding-left:0;padding-right:12px}[dir=rtl] .mdc-text-field--end-aligned .mdc-text-field__affix--prefix,.mdc-text-field--end-aligned .mdc-text-field__affix--prefix[dir=rtl]{padding-left:12px;padding-right:0}.mdc-text-field__affix--suffix{padding-left:12px;padding-right:0}[dir=rtl] .mdc-text-field__affix--suffix,.mdc-text-field__affix--suffix[dir=rtl]{padding-left:0;padding-right:12px}.mdc-text-field--end-aligned .mdc-text-field__affix--suffix{padding-left:2px;padding-right:0}[dir=rtl] .mdc-text-field--end-aligned .mdc-text-field__affix--suffix,.mdc-text-field--end-aligned .mdc-text-field__affix--suffix[dir=rtl]{padding-left:0;padding-right:2px}.mdc-text-field__input:-webkit-autofill+.mdc-floating-label{-webkit-transform:translateY(-50%) scale(0.75);transform:translateY(-50%) scale(0.75);cursor:auto}.mdc-text-field--filled{height:56px}.mdc-text-field--filled .mdc-text-field__ripple::before,.mdc-text-field--filled .mdc-text-field__ripple::after{background-color:rgba(0,0,0,.87)}.mdc-text-field--filled:hover .mdc-text-field__ripple::before{opacity:.04}.mdc-text-field--filled.mdc-ripple-upgraded--background-focused .mdc-text-field__ripple::before,.mdc-text-field--filled:not(.mdc-ripple-upgraded):focus .mdc-text-field__ripple::before{transition-duration:75ms;opacity:.12}.mdc-text-field--filled::before{display:inline-block;width:0;height:40px;content:"";vertical-align:0}.mdc-text-field--filled:not(.mdc-text-field--disabled){background-color:#f5f5f5}.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0,0,0,.42)}.mdc-text-field--filled:not(.mdc-text-field--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0,0,0,.87)}.mdc-text-field--filled .mdc-line-ripple::after{border-bottom-color:#6200ee;border-bottom-color:var(--mdc-theme-primary, #6200ee)}.mdc-text-field--filled .mdc-floating-label{left:16px;right:initial}[dir=rtl] .mdc-text-field--filled .mdc-floating-label,.mdc-text-field--filled .mdc-floating-label[dir=rtl]{left:initial;right:16px}.mdc-text-field--filled .mdc-floating-label--float-above{-webkit-transform:translateY(-106%) scale(0.75);transform:translateY(-106%) scale(0.75)}.mdc-text-field--filled.mdc-text-field--no-label .mdc-text-field__input{height:100%}.mdc-text-field--filled.mdc-text-field--no-label .mdc-floating-label{display:none}.mdc-text-field--filled.mdc-text-field--no-label::before{display:none}.mdc-text-field--outlined{height:56px;overflow:visible}.mdc-text-field--outlined .mdc-floating-label--float-above{-webkit-transform:translateY(-37.25px) scale(1);transform:translateY(-37.25px) scale(1)}.mdc-text-field--outlined .mdc-floating-label--float-above{font-size:.75rem}.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{-webkit-transform:translateY(-34.75px) scale(0.75);transform:translateY(-34.75px) scale(0.75)}.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-text-field--outlined .mdc-floating-label--shake{-webkit-animation:mdc-floating-label-shake-float-above-text-field-outlined 250ms 1;animation:mdc-floating-label-shake-float-above-text-field-outlined 250ms 1}@-webkit-keyframes mdc-floating-label-shake-float-above-text-field-outlined{0%{-webkit-transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - 0%)) translateY(-34.75px) scale(0.75);transform:translateX(calc(4% - 0%)) translateY(-34.75px) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - 0%)) translateY(-34.75px) scale(0.75);transform:translateX(calc(-4% - 0%)) translateY(-34.75px) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}}@keyframes mdc-floating-label-shake-float-above-text-field-outlined{0%{-webkit-transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - 0%)) translateY(-34.75px) scale(0.75);transform:translateX(calc(4% - 0%)) translateY(-34.75px) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - 0%)) translateY(-34.75px) scale(0.75);transform:translateX(calc(-4% - 0%)) translateY(-34.75px) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}}.mdc-text-field--outlined .mdc-text-field__input{height:100%}.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__leading,.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__notch,.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__trailing{border-color:rgba(0,0,0,.38)}.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:rgba(0,0,0,.87)}.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__leading,.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__notch,.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__trailing{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading{border-radius:4px 0 0 4px}[dir=rtl] .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading[dir=rtl]{border-radius:0 4px 4px 0}.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing{border-radius:0 4px 4px 0}[dir=rtl] .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing,.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing[dir=rtl]{border-radius:4px 0 0 4px}.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:1px}.mdc-text-field--outlined .mdc-text-field__ripple::before,.mdc-text-field--outlined .mdc-text-field__ripple::after{content:none}.mdc-text-field--outlined .mdc-floating-label{left:4px;right:initial}[dir=rtl] .mdc-text-field--outlined .mdc-floating-label,.mdc-text-field--outlined .mdc-floating-label[dir=rtl]{left:initial;right:4px}.mdc-text-field--outlined .mdc-text-field__input{display:flex;border:none !important;background-color:transparent;z-index:1}.mdc-text-field--outlined .mdc-text-field__icon{z-index:2}.mdc-text-field--outlined.mdc-text-field--focused .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:2px}.mdc-text-field--textarea{align-items:center;width:auto;height:auto;padding:0;overflow:visible;transition:none}.mdc-text-field--textarea:not(.mdc-text-field--disabled) .mdc-notched-outline__leading,.mdc-text-field--textarea:not(.mdc-text-field--disabled) .mdc-notched-outline__notch,.mdc-text-field--textarea:not(.mdc-text-field--disabled) .mdc-notched-outline__trailing{border-color:rgba(0,0,0,.38)}.mdc-text-field--textarea:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--textarea:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-text-field--textarea:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:rgba(0,0,0,.87)}.mdc-text-field--textarea:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__leading,.mdc-text-field--textarea:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__notch,.mdc-text-field--textarea:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__trailing{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-text-field--textarea .mdc-notched-outline .mdc-notched-outline__leading{border-radius:4px 0 0 4px}[dir=rtl] .mdc-text-field--textarea .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--textarea .mdc-notched-outline .mdc-notched-outline__leading[dir=rtl]{border-radius:0 4px 4px 0}.mdc-text-field--textarea .mdc-notched-outline .mdc-notched-outline__trailing{border-radius:0 4px 4px 0}[dir=rtl] .mdc-text-field--textarea .mdc-notched-outline .mdc-notched-outline__trailing,.mdc-text-field--textarea .mdc-notched-outline .mdc-notched-outline__trailing[dir=rtl]{border-radius:4px 0 0 4px}.mdc-text-field--textarea .mdc-text-field__ripple::before,.mdc-text-field--textarea .mdc-text-field__ripple::after{content:none}.mdc-text-field--textarea:not(.mdc-text-field--disabled){background-color:transparent}.mdc-text-field--textarea .mdc-text-field-character-counter{left:initial;right:16px;position:absolute;bottom:13px}[dir=rtl] .mdc-text-field--textarea .mdc-text-field-character-counter,.mdc-text-field--textarea .mdc-text-field-character-counter[dir=rtl]{left:16px;right:initial}.mdc-text-field--textarea .mdc-floating-label{left:4px;right:initial;top:17px;width:auto}[dir=rtl] .mdc-text-field--textarea .mdc-floating-label,.mdc-text-field--textarea .mdc-floating-label[dir=rtl]{left:initial;right:4px}.mdc-text-field--textarea .mdc-floating-label:not(.mdc-floating-label--float-above){-webkit-transform:none;transform:none}.mdc-text-field--textarea .mdc-floating-label--float-above{-webkit-transform:translateY(-144%) scale(1);transform:translateY(-144%) scale(1)}.mdc-text-field--textarea .mdc-floating-label--float-above{font-size:.75rem}.mdc-text-field--textarea.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--textarea .mdc-notched-outline--upgraded .mdc-floating-label--float-above{-webkit-transform:translateY(-130%) scale(0.75);transform:translateY(-130%) scale(0.75)}.mdc-text-field--textarea.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--textarea .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-text-field--textarea .mdc-floating-label--shake{-webkit-animation:mdc-floating-label-shake-float-above-textarea 250ms 1;animation:mdc-floating-label-shake-float-above-textarea 250ms 1}@-webkit-keyframes mdc-floating-label-shake-float-above-textarea{0%{-webkit-transform:translateX(calc(0 - 0%)) translateY(-130%) scale(0.75);transform:translateX(calc(0 - 0%)) translateY(-130%) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - 0%)) translateY(-130%) scale(0.75);transform:translateX(calc(4% - 0%)) translateY(-130%) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - 0%)) translateY(-130%) scale(0.75);transform:translateX(calc(-4% - 0%)) translateY(-130%) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - 0%)) translateY(-130%) scale(0.75);transform:translateX(calc(0 - 0%)) translateY(-130%) scale(0.75)}}@keyframes mdc-floating-label-shake-float-above-textarea{0%{-webkit-transform:translateX(calc(0 - 0%)) translateY(-130%) scale(0.75);transform:translateX(calc(0 - 0%)) translateY(-130%) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - 0%)) translateY(-130%) scale(0.75);transform:translateX(calc(4% - 0%)) translateY(-130%) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - 0%)) translateY(-130%) scale(0.75);transform:translateX(calc(-4% - 0%)) translateY(-130%) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - 0%)) translateY(-130%) scale(0.75);transform:translateX(calc(0 - 0%)) translateY(-130%) scale(0.75)}}.mdc-text-field--textarea .mdc-text-field__input{height:auto;align-self:stretch;box-sizing:border-box;margin-top:8px;margin-bottom:1px;margin-left:0;margin-right:1px;padding:0 16px 16px;line-height:1.75rem}[dir=rtl] .mdc-text-field--textarea .mdc-text-field__input,.mdc-text-field--textarea .mdc-text-field__input[dir=rtl]{margin-left:1px;margin-right:0}.mdc-text-field--textarea .mdc-text-field-character-counter+.mdc-text-field__input{margin-bottom:28px;padding-bottom:0}.mdc-text-field--fullwidth{padding:0;width:100%}.mdc-text-field--fullwidth:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0,0,0,.42)}.mdc-text-field--fullwidth.mdc-text-field--disabled .mdc-line-ripple::before{border-bottom-color:rgba(0,0,0,.42)}.mdc-text-field--fullwidth:not(.mdc-text-field--textarea){display:flex}.mdc-text-field--fullwidth:not(.mdc-text-field--textarea) .mdc-text-field__input{height:100%}.mdc-text-field--fullwidth:not(.mdc-text-field--textarea) .mdc-floating-label{display:none}.mdc-text-field--fullwidth:not(.mdc-text-field--textarea)::before{display:none}.mdc-text-field--fullwidth:not(.mdc-text-field--textarea) .mdc-text-field__ripple::before,.mdc-text-field--fullwidth:not(.mdc-text-field--textarea) .mdc-text-field__ripple::after{content:none}.mdc-text-field--fullwidth:not(.mdc-text-field--textarea):not(.mdc-text-field--disabled){background-color:transparent}.mdc-text-field--fullwidth.mdc-text-field--textarea .mdc-text-field__input{resize:vertical}.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label{max-width:calc(100% - 48px);left:48px;right:initial}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label,.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label[dir=rtl]{left:initial;right:48px}.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 64px / 0.75)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label{left:36px;right:initial}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label[dir=rtl]{left:initial;right:36px}.mdc-text-field--with-leading-icon.mdc-text-field--outlined :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 60px)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above{-webkit-transform:translateY(-37.25px) translateX(-32px) scale(1);transform:translateY(-37.25px) translateX(-32px) scale(1)}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above[dir=rtl]{-webkit-transform:translateY(-37.25px) translateX(32px) scale(1);transform:translateY(-37.25px) translateX(32px) scale(1)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above{font-size:.75rem}.mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{-webkit-transform:translateY(-34.75px) translateX(-32px) scale(0.75);transform:translateY(-34.75px) translateX(-32px) scale(0.75)}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl],[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl]{-webkit-transform:translateY(-34.75px) translateX(32px) scale(0.75);transform:translateY(-34.75px) translateX(32px) scale(0.75)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--shake{-webkit-animation:mdc-floating-label-shake-float-above-text-field-outlined-leading-icon 250ms 1;animation:mdc-floating-label-shake-float-above-text-field-outlined-leading-icon 250ms 1}@-webkit-keyframes mdc-floating-label-shake-float-above-text-field-outlined-leading-icon{0%{-webkit-transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}}@keyframes mdc-floating-label-shake-float-above-text-field-outlined-leading-icon{0%{-webkit-transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--shake,.mdc-text-field--with-leading-icon.mdc-text-field--outlined[dir=rtl] .mdc-floating-label--shake{-webkit-animation:mdc-floating-label-shake-float-above-text-field-outlined-leading-icon 250ms 1;animation:mdc-floating-label-shake-float-above-text-field-outlined-leading-icon 250ms 1}@-webkit-keyframes mdc-floating-label-shake-float-above-text-field-outlined-leading-icon-rtl{0%{-webkit-transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}}@keyframes mdc-floating-label-shake-float-above-text-field-outlined-leading-icon-rtl{0%{-webkit-transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}33%{-webkit-animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);-webkit-transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75)}66%{-webkit-animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);-webkit-transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75)}100%{-webkit-transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75);transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}}.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label{max-width:calc(100% - 64px)}.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 64px / 0.75)}.mdc-text-field--with-trailing-icon.mdc-text-field--outlined :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 60px)}.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label{max-width:calc(100% - 96px)}.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 96px / 0.75)}.mdc-text-field__input:required~.mdc-floating-label::after,.mdc-text-field__input:required~.mdc-notched-outline .mdc-floating-label::after{margin-left:1px;content:"*"}.mdc-text-field-helper-line{display:flex;justify-content:space-between;box-sizing:border-box}.mdc-text-field+.mdc-text-field-helper-line{padding-right:16px;padding-left:16px}.mdc-form-field>.mdc-text-field+label{align-self:flex-start}.mdc-text-field--focused:not(.mdc-text-field--disabled) .mdc-floating-label{color:rgba(98,0,238,.87)}.mdc-text-field--focused .mdc-notched-outline__leading,.mdc-text-field--focused .mdc-notched-outline__notch,.mdc-text-field--focused .mdc-notched-outline__trailing{border-width:2px}.mdc-text-field--focused+.mdc-text-field-helper-line .mdc-text-field-helper-text:not(.mdc-text-field-helper-text--validation-msg){opacity:1}.mdc-text-field--invalid:not(.mdc-text-field--disabled):hover .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::after{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-floating-label{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--invalid+.mdc-text-field-helper-line .mdc-text-field-helper-text--validation-msg{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid .mdc-text-field__input{caret-color:#b00020;caret-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-text-field__icon--trailing{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-notched-outline__leading,.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-notched-outline__notch,.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--invalid:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-text-field--invalid:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__leading,.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__notch,.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid+.mdc-text-field-helper-line .mdc-text-field-helper-text--validation-msg{opacity:1}.mdc-text-field--disabled{pointer-events:none}.mdc-text-field--disabled .mdc-text-field__input{color:rgba(0,0,0,.38)}@media all{.mdc-text-field--disabled .mdc-text-field__input::-webkit-input-placeholder{color:rgba(0,0,0,.38)}.mdc-text-field--disabled .mdc-text-field__input:-ms-input-placeholder{color:rgba(0,0,0,.38)}.mdc-text-field--disabled .mdc-text-field__input::-ms-input-placeholder{color:rgba(0,0,0,.38)}.mdc-text-field--disabled .mdc-text-field__input::placeholder{color:rgba(0,0,0,.38)}}@media all{.mdc-text-field--disabled .mdc-text-field__input:-ms-input-placeholder{color:rgba(0,0,0,.38)}}.mdc-text-field--disabled .mdc-floating-label{color:rgba(0,0,0,.38)}.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-helper-text{color:rgba(0,0,0,.38)}.mdc-text-field--disabled .mdc-text-field-character-counter,.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-character-counter{color:rgba(0,0,0,.38)}.mdc-text-field--disabled .mdc-text-field__icon--leading{color:rgba(0,0,0,.3)}.mdc-text-field--disabled .mdc-text-field__icon--trailing{color:rgba(0,0,0,.3)}.mdc-text-field--disabled .mdc-text-field__affix--prefix{color:rgba(0,0,0,.38)}.mdc-text-field--disabled .mdc-text-field__affix--suffix{color:rgba(0,0,0,.38)}.mdc-text-field--disabled .mdc-line-ripple::before{border-bottom-color:rgba(0,0,0,.06)}.mdc-text-field--disabled .mdc-notched-outline__leading,.mdc-text-field--disabled .mdc-notched-outline__notch,.mdc-text-field--disabled .mdc-notched-outline__trailing{border-color:rgba(0,0,0,.06)}@media screen and (-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__input::-webkit-input-placeholder{color:GrayText}.mdc-text-field--disabled .mdc-text-field__input:-ms-input-placeholder{color:GrayText}.mdc-text-field--disabled .mdc-text-field__input::-ms-input-placeholder{color:GrayText}.mdc-text-field--disabled .mdc-text-field__input::placeholder{color:GrayText}}@media screen and (-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__input:-ms-input-placeholder{color:GrayText}}@media screen and (-ms-high-contrast: active){.mdc-text-field--disabled .mdc-floating-label{color:GrayText}}@media screen and (-ms-high-contrast: active){.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-helper-text{color:GrayText}}@media screen and (-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field-character-counter,.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-character-counter{color:GrayText}}@media screen and (-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__icon--leading{color:GrayText}}@media screen and (-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__icon--trailing{color:GrayText}}@media screen and (-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__affix--prefix{color:GrayText}}@media screen and (-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__affix--suffix{color:GrayText}}@media screen and (-ms-high-contrast: active){.mdc-text-field--disabled .mdc-line-ripple::before{border-bottom-color:GrayText}}@media screen and (-ms-high-contrast: active){.mdc-text-field--disabled .mdc-notched-outline__leading,.mdc-text-field--disabled .mdc-notched-outline__notch,.mdc-text-field--disabled .mdc-notched-outline__trailing{border-color:GrayText}}.mdc-text-field--disabled .mdc-floating-label{cursor:default}.mdc-text-field--disabled.mdc-text-field--filled{background-color:#fafafa}.mdc-text-field--end-aligned .mdc-text-field__input{text-align:right}[dir=rtl] .mdc-text-field--end-aligned .mdc-text-field__input,.mdc-text-field--end-aligned .mdc-text-field__input[dir=rtl]{text-align:left}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__input,[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__input,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix{direction:ltr}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--prefix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--prefix{padding-left:0;padding-right:2px}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--suffix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--suffix{padding-left:12px;padding-right:0}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__icon--leading,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__icon--leading{order:1}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--suffix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--suffix{order:2}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__input,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__input{order:3}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--prefix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--prefix{order:4}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__icon--trailing,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__icon--trailing{order:5}[dir=rtl] .mdc-text-field--ltr-text.mdc-text-field--end-aligned .mdc-text-field__input,.mdc-text-field--ltr-text.mdc-text-field--end-aligned[dir=rtl] .mdc-text-field__input{text-align:right}[dir=rtl] .mdc-text-field--ltr-text.mdc-text-field--end-aligned .mdc-text-field__affix--prefix,.mdc-text-field--ltr-text.mdc-text-field--end-aligned[dir=rtl] .mdc-text-field__affix--prefix{padding-right:12px}[dir=rtl] .mdc-text-field--ltr-text.mdc-text-field--end-aligned .mdc-text-field__affix--suffix,.mdc-text-field--ltr-text.mdc-text-field--end-aligned[dir=rtl] .mdc-text-field__affix--suffix{padding-left:2px}:root{--mdc-theme-primary: #6200ee;--mdc-theme-secondary: #018786;--mdc-theme-background: #fff;--mdc-theme-surface: #fff;--mdc-theme-error: #b00020;--mdc-theme-on-primary: #fff;--mdc-theme-on-secondary: #fff;--mdc-theme-on-surface: #000;--mdc-theme-on-error: #fff;--mdc-theme-text-primary-on-background: rgba(0, 0, 0, 0.87);--mdc-theme-text-secondary-on-background: rgba(0, 0, 0, 0.54);--mdc-theme-text-hint-on-background: rgba(0, 0, 0, 0.38);--mdc-theme-text-disabled-on-background: rgba(0, 0, 0, 0.38);--mdc-theme-text-icon-on-background: rgba(0, 0, 0, 0.38);--mdc-theme-text-primary-on-light: rgba(0, 0, 0, 0.87);--mdc-theme-text-secondary-on-light: rgba(0, 0, 0, 0.54);--mdc-theme-text-hint-on-light: rgba(0, 0, 0, 0.38);--mdc-theme-text-disabled-on-light: rgba(0, 0, 0, 0.38);--mdc-theme-text-icon-on-light: rgba(0, 0, 0, 0.38);--mdc-theme-text-primary-on-dark: white;--mdc-theme-text-secondary-on-dark: rgba(255, 255, 255, 0.7);--mdc-theme-text-hint-on-dark: rgba(255, 255, 255, 0.5);--mdc-theme-text-disabled-on-dark: rgba(255, 255, 255, 0.5);--mdc-theme-text-icon-on-dark: rgba(255, 255, 255, 0.5)}.mdc-theme--primary{color:#6200ee !important;color:var(--mdc-theme-primary, #6200ee) !important}.mdc-theme--secondary{color:#018786 !important;color:var(--mdc-theme-secondary, #018786) !important}.mdc-theme--background{background-color:#fff;background-color:var(--mdc-theme-background, #fff)}.mdc-theme--surface{background-color:#fff;background-color:var(--mdc-theme-surface, #fff)}.mdc-theme--error{color:#b00020 !important;color:var(--mdc-theme-error, #b00020) !important}.mdc-theme--on-primary{color:#fff !important;color:var(--mdc-theme-on-primary, #fff) !important}.mdc-theme--on-secondary{color:#fff !important;color:var(--mdc-theme-on-secondary, #fff) !important}.mdc-theme--on-surface{color:#000 !important;color:var(--mdc-theme-on-surface, #000) !important}.mdc-theme--on-error{color:#fff !important;color:var(--mdc-theme-on-error, #fff) !important}.mdc-theme--text-primary-on-background{color:rgba(0,0,0,.87) !important;color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87)) !important}.mdc-theme--text-secondary-on-background{color:rgba(0,0,0,.54) !important;color:var(--mdc-theme-text-secondary-on-background, rgba(0, 0, 0, 0.54)) !important}.mdc-theme--text-hint-on-background{color:rgba(0,0,0,.38) !important;color:var(--mdc-theme-text-hint-on-background, rgba(0, 0, 0, 0.38)) !important}.mdc-theme--text-disabled-on-background{color:rgba(0,0,0,.38) !important;color:var(--mdc-theme-text-disabled-on-background, rgba(0, 0, 0, 0.38)) !important}.mdc-theme--text-icon-on-background{color:rgba(0,0,0,.38) !important;color:var(--mdc-theme-text-icon-on-background, rgba(0, 0, 0, 0.38)) !important}.mdc-theme--text-primary-on-light{color:rgba(0,0,0,.87) !important;color:var(--mdc-theme-text-primary-on-light, rgba(0, 0, 0, 0.87)) !important}.mdc-theme--text-secondary-on-light{color:rgba(0,0,0,.54) !important;color:var(--mdc-theme-text-secondary-on-light, rgba(0, 0, 0, 0.54)) !important}.mdc-theme--text-hint-on-light{color:rgba(0,0,0,.38) !important;color:var(--mdc-theme-text-hint-on-light, rgba(0, 0, 0, 0.38)) !important}.mdc-theme--text-disabled-on-light{color:rgba(0,0,0,.38) !important;color:var(--mdc-theme-text-disabled-on-light, rgba(0, 0, 0, 0.38)) !important}.mdc-theme--text-icon-on-light{color:rgba(0,0,0,.38) !important;color:var(--mdc-theme-text-icon-on-light, rgba(0, 0, 0, 0.38)) !important}.mdc-theme--text-primary-on-dark{color:#fff !important;color:var(--mdc-theme-text-primary-on-dark, white) !important}.mdc-theme--text-secondary-on-dark{color:rgba(255,255,255,.7) !important;color:var(--mdc-theme-text-secondary-on-dark, rgba(255, 255, 255, 0.7)) !important}.mdc-theme--text-hint-on-dark{color:rgba(255,255,255,.5) !important;color:var(--mdc-theme-text-hint-on-dark, rgba(255, 255, 255, 0.5)) !important}.mdc-theme--text-disabled-on-dark{color:rgba(255,255,255,.5) !important;color:var(--mdc-theme-text-disabled-on-dark, rgba(255, 255, 255, 0.5)) !important}.mdc-theme--text-icon-on-dark{color:rgba(255,255,255,.5) !important;color:var(--mdc-theme-text-icon-on-dark, rgba(255, 255, 255, 0.5)) !important}.mdc-theme--primary-bg{background-color:#6200ee !important;background-color:var(--mdc-theme-primary, #6200ee) !important}.mdc-theme--secondary-bg{background-color:#018786 !important;background-color:var(--mdc-theme-secondary, #018786) !important}.mdc-top-app-bar{background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee);color:#fff;display:flex;position:fixed;flex-direction:column;justify-content:space-between;box-sizing:border-box;width:100%;z-index:4}.mdc-top-app-bar .mdc-top-app-bar__action-item,.mdc-top-app-bar .mdc-top-app-bar__navigation-icon{color:#fff;color:var(--mdc-theme-on-primary, #fff)}.mdc-top-app-bar .mdc-top-app-bar__action-item::before,.mdc-top-app-bar .mdc-top-app-bar__action-item::after,.mdc-top-app-bar .mdc-top-app-bar__navigation-icon::before,.mdc-top-app-bar .mdc-top-app-bar__navigation-icon::after{background-color:#fff;background-color:var(--mdc-theme-on-primary, #fff)}.mdc-top-app-bar .mdc-top-app-bar__action-item:hover::before,.mdc-top-app-bar .mdc-top-app-bar__navigation-icon:hover::before{opacity:.08}.mdc-top-app-bar .mdc-top-app-bar__action-item.mdc-ripple-upgraded--background-focused::before,.mdc-top-app-bar .mdc-top-app-bar__action-item:not(.mdc-ripple-upgraded):focus::before,.mdc-top-app-bar .mdc-top-app-bar__navigation-icon.mdc-ripple-upgraded--background-focused::before,.mdc-top-app-bar .mdc-top-app-bar__navigation-icon:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:.24}.mdc-top-app-bar .mdc-top-app-bar__action-item:not(.mdc-ripple-upgraded)::after,.mdc-top-app-bar .mdc-top-app-bar__navigation-icon:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-top-app-bar .mdc-top-app-bar__action-item:not(.mdc-ripple-upgraded):active::after,.mdc-top-app-bar .mdc-top-app-bar__navigation-icon:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.24}.mdc-top-app-bar .mdc-top-app-bar__action-item.mdc-ripple-upgraded,.mdc-top-app-bar .mdc-top-app-bar__navigation-icon.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.24}.mdc-top-app-bar__row{display:flex;position:relative;box-sizing:border-box;width:100%;height:64px}.mdc-top-app-bar__section{display:inline-flex;flex:1 1 auto;align-items:center;min-width:0;padding:8px 12px;z-index:1}.mdc-top-app-bar__section--align-start{justify-content:flex-start;order:-1}.mdc-top-app-bar__section--align-end{justify-content:flex-end;order:1}.mdc-top-app-bar__title{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-headline6-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1.25rem;font-size:var(--mdc-typography-headline6-font-size, 1.25rem);line-height:2rem;line-height:var(--mdc-typography-headline6-line-height, 2rem);font-weight:500;font-weight:var(--mdc-typography-headline6-font-weight, 500);letter-spacing:.0125em;letter-spacing:var(--mdc-typography-headline6-letter-spacing, 0.0125em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-headline6-text-decoration, inherit);text-decoration:var(--mdc-typography-headline6-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-headline6-text-transform, inherit);padding-left:20px;padding-right:0;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;z-index:1}[dir=rtl] .mdc-top-app-bar__title,.mdc-top-app-bar__title[dir=rtl]{padding-left:0;padding-right:20px}.mdc-top-app-bar--short-collapsed{border-radius:0 0 24px 0}[dir=rtl] .mdc-top-app-bar--short-collapsed,.mdc-top-app-bar--short-collapsed[dir=rtl]{border-radius:0 0 0 24px}.mdc-top-app-bar--short{top:0;right:auto;left:0;width:100%;transition:width 250ms cubic-bezier(0.4, 0, 0.2, 1)}[dir=rtl] .mdc-top-app-bar--short,.mdc-top-app-bar--short[dir=rtl]{right:0;left:auto}.mdc-top-app-bar--short .mdc-top-app-bar__row{height:56px}.mdc-top-app-bar--short .mdc-top-app-bar__section{padding:4px}.mdc-top-app-bar--short .mdc-top-app-bar__title{transition:opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);opacity:1}.mdc-top-app-bar--short-collapsed{box-shadow:0px 2px 4px -1px rgba(0, 0, 0, 0.2),0px 4px 5px 0px rgba(0, 0, 0, 0.14),0px 1px 10px 0px rgba(0,0,0,.12);width:56px;transition:width 300ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-top-app-bar--short-collapsed .mdc-top-app-bar__title{display:none}.mdc-top-app-bar--short-collapsed .mdc-top-app-bar__action-item{transition:padding 150ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-top-app-bar--short-collapsed.mdc-top-app-bar--short-has-action-item{width:112px}.mdc-top-app-bar--short-collapsed.mdc-top-app-bar--short-has-action-item .mdc-top-app-bar__section--align-end{padding-left:0;padding-right:12px}[dir=rtl] .mdc-top-app-bar--short-collapsed.mdc-top-app-bar--short-has-action-item .mdc-top-app-bar__section--align-end,.mdc-top-app-bar--short-collapsed.mdc-top-app-bar--short-has-action-item .mdc-top-app-bar__section--align-end[dir=rtl]{padding-left:12px;padding-right:0}.mdc-top-app-bar--dense .mdc-top-app-bar__row{height:48px}.mdc-top-app-bar--dense .mdc-top-app-bar__section{padding:0 4px}.mdc-top-app-bar--dense .mdc-top-app-bar__title{padding-left:12px;padding-right:0}[dir=rtl] .mdc-top-app-bar--dense .mdc-top-app-bar__title,.mdc-top-app-bar--dense .mdc-top-app-bar__title[dir=rtl]{padding-left:0;padding-right:12px}.mdc-top-app-bar--prominent .mdc-top-app-bar__row{height:128px}.mdc-top-app-bar--prominent .mdc-top-app-bar__title{align-self:flex-end;padding-bottom:2px}.mdc-top-app-bar--prominent .mdc-top-app-bar__action-item,.mdc-top-app-bar--prominent .mdc-top-app-bar__navigation-icon{align-self:flex-start}.mdc-top-app-bar--fixed{transition:box-shadow 200ms linear}.mdc-top-app-bar--fixed-scrolled{box-shadow:0px 2px 4px -1px rgba(0, 0, 0, 0.2),0px 4px 5px 0px rgba(0, 0, 0, 0.14),0px 1px 10px 0px rgba(0,0,0,.12);transition:box-shadow 200ms linear}.mdc-top-app-bar--dense.mdc-top-app-bar--prominent .mdc-top-app-bar__row{height:96px}.mdc-top-app-bar--dense.mdc-top-app-bar--prominent .mdc-top-app-bar__section{padding:0 12px}.mdc-top-app-bar--dense.mdc-top-app-bar--prominent .mdc-top-app-bar__title{padding-left:20px;padding-right:0;padding-bottom:9px}[dir=rtl] .mdc-top-app-bar--dense.mdc-top-app-bar--prominent .mdc-top-app-bar__title,.mdc-top-app-bar--dense.mdc-top-app-bar--prominent .mdc-top-app-bar__title[dir=rtl]{padding-left:0;padding-right:20px}.mdc-top-app-bar--fixed-adjust{padding-top:64px}.mdc-top-app-bar--dense-fixed-adjust{padding-top:48px}.mdc-top-app-bar--short-fixed-adjust{padding-top:56px}.mdc-top-app-bar--prominent-fixed-adjust{padding-top:128px}.mdc-top-app-bar--dense-prominent-fixed-adjust{padding-top:96px}@media(max-width: 599px){.mdc-top-app-bar__row{height:56px}.mdc-top-app-bar__section{padding:4px}.mdc-top-app-bar--short{transition:width 200ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-top-app-bar--short-collapsed{transition:width 250ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-top-app-bar--short-collapsed .mdc-top-app-bar__section--align-end{padding-left:0;padding-right:12px}[dir=rtl] .mdc-top-app-bar--short-collapsed .mdc-top-app-bar__section--align-end,.mdc-top-app-bar--short-collapsed .mdc-top-app-bar__section--align-end[dir=rtl]{padding-left:12px;padding-right:0}.mdc-top-app-bar--prominent .mdc-top-app-bar__title{padding-bottom:6px}.mdc-top-app-bar--fixed-adjust{padding-top:56px}}.mdc-typography{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-font-family, Roboto, sans-serif)}.mdc-typography--headline1{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-headline1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:6rem;font-size:var(--mdc-typography-headline1-font-size, 6rem);line-height:6rem;line-height:var(--mdc-typography-headline1-line-height, 6rem);font-weight:300;font-weight:var(--mdc-typography-headline1-font-weight, 300);letter-spacing:-0.015625em;letter-spacing:var(--mdc-typography-headline1-letter-spacing, -0.015625em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-headline1-text-decoration, inherit);text-decoration:var(--mdc-typography-headline1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-headline1-text-transform, inherit)}.mdc-typography--headline2{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-headline2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:3.75rem;font-size:var(--mdc-typography-headline2-font-size, 3.75rem);line-height:3.75rem;line-height:var(--mdc-typography-headline2-line-height, 3.75rem);font-weight:300;font-weight:var(--mdc-typography-headline2-font-weight, 300);letter-spacing:-0.0083333333em;letter-spacing:var(--mdc-typography-headline2-letter-spacing, -0.0083333333em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-headline2-text-decoration, inherit);text-decoration:var(--mdc-typography-headline2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-headline2-text-transform, inherit)}.mdc-typography--headline3{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-headline3-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:3rem;font-size:var(--mdc-typography-headline3-font-size, 3rem);line-height:3.125rem;line-height:var(--mdc-typography-headline3-line-height, 3.125rem);font-weight:400;font-weight:var(--mdc-typography-headline3-font-weight, 400);letter-spacing:normal;letter-spacing:var(--mdc-typography-headline3-letter-spacing, normal);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-headline3-text-decoration, inherit);text-decoration:var(--mdc-typography-headline3-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-headline3-text-transform, inherit)}.mdc-typography--headline4{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-headline4-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:2.125rem;font-size:var(--mdc-typography-headline4-font-size, 2.125rem);line-height:2.5rem;line-height:var(--mdc-typography-headline4-line-height, 2.5rem);font-weight:400;font-weight:var(--mdc-typography-headline4-font-weight, 400);letter-spacing:.0073529412em;letter-spacing:var(--mdc-typography-headline4-letter-spacing, 0.0073529412em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-headline4-text-decoration, inherit);text-decoration:var(--mdc-typography-headline4-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-headline4-text-transform, inherit)}.mdc-typography--headline5{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-headline5-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1.5rem;font-size:var(--mdc-typography-headline5-font-size, 1.5rem);line-height:2rem;line-height:var(--mdc-typography-headline5-line-height, 2rem);font-weight:400;font-weight:var(--mdc-typography-headline5-font-weight, 400);letter-spacing:normal;letter-spacing:var(--mdc-typography-headline5-letter-spacing, normal);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-headline5-text-decoration, inherit);text-decoration:var(--mdc-typography-headline5-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-headline5-text-transform, inherit)}.mdc-typography--headline6{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-headline6-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1.25rem;font-size:var(--mdc-typography-headline6-font-size, 1.25rem);line-height:2rem;line-height:var(--mdc-typography-headline6-line-height, 2rem);font-weight:500;font-weight:var(--mdc-typography-headline6-font-weight, 500);letter-spacing:.0125em;letter-spacing:var(--mdc-typography-headline6-letter-spacing, 0.0125em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-headline6-text-decoration, inherit);text-decoration:var(--mdc-typography-headline6-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-headline6-text-transform, inherit)}.mdc-typography--subtitle1{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);line-height:1.75rem;line-height:var(--mdc-typography-subtitle1-line-height, 1.75rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit)}.mdc-typography--subtitle2{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-subtitle2-font-size, 0.875rem);line-height:1.375rem;line-height:var(--mdc-typography-subtitle2-line-height, 1.375rem);font-weight:500;font-weight:var(--mdc-typography-subtitle2-font-weight, 500);letter-spacing:.0071428571em;letter-spacing:var(--mdc-typography-subtitle2-letter-spacing, 0.0071428571em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-subtitle2-text-decoration, inherit);text-decoration:var(--mdc-typography-subtitle2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle2-text-transform, inherit)}.mdc-typography--body1{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-body1-font-size, 1rem);line-height:1.5rem;line-height:var(--mdc-typography-body1-line-height, 1.5rem);font-weight:400;font-weight:var(--mdc-typography-body1-font-weight, 400);letter-spacing:.03125em;letter-spacing:var(--mdc-typography-body1-letter-spacing, 0.03125em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-body1-text-decoration, inherit);text-decoration:var(--mdc-typography-body1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body1-text-transform, inherit)}.mdc-typography--body2{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit)}.mdc-typography--caption{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;-webkit-text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit)}.mdc-typography--button{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-button-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.875rem;font-size:var(--mdc-typography-button-font-size, 0.875rem);line-height:2.25rem;line-height:var(--mdc-typography-button-line-height, 2.25rem);font-weight:500;font-weight:var(--mdc-typography-button-font-weight, 500);letter-spacing:.0892857143em;letter-spacing:var(--mdc-typography-button-letter-spacing, 0.0892857143em);text-decoration:none;-webkit-text-decoration:var(--mdc-typography-button-text-decoration, none);text-decoration:var(--mdc-typography-button-text-decoration, none);text-transform:uppercase;text-transform:var(--mdc-typography-button-text-transform, uppercase)}.mdc-typography--overline{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-overline-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:.75rem;font-size:var(--mdc-typography-overline-font-size, 0.75rem);line-height:2rem;line-height:var(--mdc-typography-overline-line-height, 2rem);font-weight:500;font-weight:var(--mdc-typography-overline-font-weight, 500);letter-spacing:.1666666667em;letter-spacing:var(--mdc-typography-overline-letter-spacing, 0.1666666667em);text-decoration:none;-webkit-text-decoration:var(--mdc-typography-overline-text-decoration, none);text-decoration:var(--mdc-typography-overline-text-decoration, none);text-transform:uppercase;text-transform:var(--mdc-typography-overline-text-transform, uppercase)} + +/*# sourceMappingURL=material-components-web.min.css.map*/ \ No newline at end of file diff --git a/backend/src/main/resources/static/img/404.jpg b/backend/src/main/resources/static/img/404.jpg new file mode 100644 index 000000000..ab668e872 Binary files /dev/null and b/backend/src/main/resources/static/img/404.jpg differ diff --git a/backend/src/main/resources/static/img/500.gif b/backend/src/main/resources/static/img/500.gif new file mode 100644 index 000000000..bd74b3418 Binary files /dev/null and b/backend/src/main/resources/static/img/500.gif differ diff --git a/backend/src/main/resources/static/img/GitHub_Logo.png b/backend/src/main/resources/static/img/GitHub_Logo.png new file mode 100644 index 000000000..e03d8dd8b Binary files /dev/null and b/backend/src/main/resources/static/img/GitHub_Logo.png differ diff --git a/backend/src/main/resources/static/img/enbarsskar.jpg b/backend/src/main/resources/static/img/enbarsskar.jpg new file mode 100644 index 000000000..2a9892329 Binary files /dev/null and b/backend/src/main/resources/static/img/enbarsskar.jpg differ diff --git a/backend/src/main/resources/static/img/favicon.ico b/backend/src/main/resources/static/img/favicon.ico new file mode 100644 index 000000000..c873e6a04 Binary files /dev/null and b/backend/src/main/resources/static/img/favicon.ico differ diff --git a/backend/src/main/resources/static/img/itlogo.svg b/backend/src/main/resources/static/img/itlogo.svg new file mode 100644 index 000000000..7bcc301ba --- /dev/null +++ b/backend/src/main/resources/static/img/itlogo.svg @@ -0,0 +1,160 @@ + + + + itlogo + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/backend/src/main/resources/static/js/mcw.min.js b/backend/src/main/resources/static/js/mcw.min.js new file mode 100644 index 000000000..4d870d86a --- /dev/null +++ b/backend/src/main/resources/static/js/mcw.min.js @@ -0,0 +1,15878 @@ +!(function(t, e) { + "object" == typeof exports && "object" == typeof module + ? (module.exports = e()) + : "function" == typeof define && define.amd + ? define([], e) + : "object" == typeof exports + ? (exports.mdc = e()) + : (t.mdc = e()); +})(this, function() { + return ( + (i = {}), + (r.m = n = [ + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + var i = + (Object.defineProperty(r, "cssClasses", { + get: function() { + return {}; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(r, "strings", { + get: function() { + return {}; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(r, "numbers", { + get: function() { + return {}; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(r, "defaultAdapter", { + get: function() { + return {}; + }, + enumerable: !0, + configurable: !0 + }), + (r.prototype.init = function() {}), + (r.prototype.destroy = function() {}), + r); + function r(t) { + void 0 === t && (t = {}), (this.adapter_ = t); + } + (e.MDCFoundation = i), (e.default = i); + }, + function(t, e, n) { + "use strict"; + var i = + (this && this.__read) || + function(t, e) { + var n = "function" == typeof Symbol && t[Symbol.iterator]; + if (!n) return t; + var i, + r, + o = n.call(t), + s = []; + try { + for (; (void 0 === e || 0 < e--) && !(i = o.next()).done; ) + s.push(i.value); + } catch (t) { + r = { error: t }; + } finally { + try { + i && !i.done && (n = o.return) && n.call(o); + } finally { + if (r) throw r.error; + } + } + return s; + }, + r = + (this && this.__spread) || + function() { + for (var t = [], e = 0; e < arguments.length; e++) + t = t.concat(i(arguments[e])); + return t; + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var o = n(0), + s = + ((a.attachTo = function(t) { + return new a(t, new o.MDCFoundation({})); + }), + (a.prototype.initialize = function() { + for (var t = [], e = 0; e < arguments.length; e++) + t[e] = arguments[e]; + }), + (a.prototype.getDefaultFoundation = function() { + throw new Error( + "Subclasses must override getDefaultFoundation to return a properly configured foundation class" + ); + }), + (a.prototype.initialSyncWithDOM = function() {}), + (a.prototype.destroy = function() { + this.foundation_.destroy(); + }), + (a.prototype.listen = function(t, e, n) { + this.root_.addEventListener(t, e, n); + }), + (a.prototype.unlisten = function(t, e, n) { + this.root_.removeEventListener(t, e, n); + }), + (a.prototype.emit = function(t, e, n) { + var i; + void 0 === n && (n = !1), + "function" == typeof CustomEvent + ? (i = new CustomEvent(t, { bubbles: n, detail: e })) + : (i = document.createEvent("CustomEvent")).initCustomEvent( + t, + n, + !1, + e + ), + this.root_.dispatchEvent(i); + }), + a); + function a(t, e) { + for (var n = [], i = 2; i < arguments.length; i++) + n[i - 2] = arguments[i]; + (this.root_ = t), + this.initialize.apply(this, r(n)), + (this.foundation_ = void 0 === e ? this.getDefaultFoundation() : e), + this.foundation_.init(), + this.initialSyncWithDOM(); + } + (e.MDCComponent = s), (e.default = s); + }, + function(t, e, n) { + "use strict"; + function i(t, e) { + return ( + t.matches || + t.webkitMatchesSelector || + t.msMatchesSelector + ).call(t, e); + } + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.closest = function(t, e) { + if (t.closest) return t.closest(e); + for (var n = t; n; ) { + if (i(n, e)) return n; + n = n.parentElement; + } + return null; + }), + (e.matches = i), + (e.estimateScrollWidth = function(t) { + var e = t; + if (null !== e.offsetParent) return e.scrollWidth; + var n = e.cloneNode(!0); + n.style.setProperty("position", "absolute"), + n.style.setProperty("transform", "translate(-9999px, -9999px)"), + document.documentElement.appendChild(n); + var i = n.scrollWidth; + return document.documentElement.removeChild(n), i; + }); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(1), + c = n(5), + u = n(2), + l = n(4), + d = o(n(16)), + p = + ((s = a.MDCComponent), + r(_, s), + (_.attachTo = function(t, e) { + void 0 === e && (e = { isUnbounded: void 0 }); + var n = new _(t); + return ( + void 0 !== e.isUnbounded && (n.unbounded = e.isUnbounded), n + ); + }), + (_.createAdapter = function(n) { + return { + addClass: function(t) { + return n.root_.classList.add(t); + }, + browserSupportsCssVars: function() { + return d.supportsCssVariables(window); + }, + computeBoundingRect: function() { + return n.root_.getBoundingClientRect(); + }, + containsEventTarget: function(t) { + return n.root_.contains(t); + }, + deregisterDocumentInteractionHandler: function(t, e) { + return document.documentElement.removeEventListener( + t, + e, + c.applyPassive() + ); + }, + deregisterInteractionHandler: function(t, e) { + return n.root_.removeEventListener(t, e, c.applyPassive()); + }, + deregisterResizeHandler: function(t) { + return window.removeEventListener("resize", t); + }, + getWindowPageOffset: function() { + return { x: window.pageXOffset, y: window.pageYOffset }; + }, + isSurfaceActive: function() { + return u.matches(n.root_, ":active"); + }, + isSurfaceDisabled: function() { + return Boolean(n.disabled); + }, + isUnbounded: function() { + return Boolean(n.unbounded); + }, + registerDocumentInteractionHandler: function(t, e) { + return document.documentElement.addEventListener( + t, + e, + c.applyPassive() + ); + }, + registerInteractionHandler: function(t, e) { + return n.root_.addEventListener(t, e, c.applyPassive()); + }, + registerResizeHandler: function(t) { + return window.addEventListener("resize", t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + updateCssVariable: function(t, e) { + return n.root_.style.setProperty(t, e); + } + }; + }), + Object.defineProperty(_.prototype, "unbounded", { + get: function() { + return Boolean(this.unbounded_); + }, + set: function(t) { + (this.unbounded_ = Boolean(t)), this.setUnbounded_(); + }, + enumerable: !0, + configurable: !0 + }), + (_.prototype.activate = function() { + this.foundation_.activate(); + }), + (_.prototype.deactivate = function() { + this.foundation_.deactivate(); + }), + (_.prototype.layout = function() { + this.foundation_.layout(); + }), + (_.prototype.getDefaultFoundation = function() { + return new l.MDCRippleFoundation(_.createAdapter(this)); + }), + (_.prototype.initialSyncWithDOM = function() { + var t = this.root_; + this.unbounded = "mdcRippleIsUnbounded" in t.dataset; + }), + (_.prototype.setUnbounded_ = function() { + this.foundation_.setUnbounded(Boolean(this.unbounded_)); + }), + _); + function _() { + var t = (null !== s && s.apply(this, arguments)) || this; + return (t.disabled = !1), t; + } + e.MDCRipple = p; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(40), + u = n(16), + l = ["touchstart", "pointerdown", "mousedown", "keydown"], + d = ["touchend", "pointerup", "mouseup", "contextmenu"], + p = [], + _ = + ((s = a.MDCFoundation), + r(f, s), + Object.defineProperty(f, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f, "numbers", { + get: function() { + return c.numbers; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + browserSupportsCssVars: function() { + return !0; + }, + computeBoundingRect: function() { + return { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 0, + height: 0 + }; + }, + containsEventTarget: function() { + return !0; + }, + deregisterDocumentInteractionHandler: function() {}, + deregisterInteractionHandler: function() {}, + deregisterResizeHandler: function() {}, + getWindowPageOffset: function() { + return { x: 0, y: 0 }; + }, + isSurfaceActive: function() { + return !0; + }, + isSurfaceDisabled: function() { + return !0; + }, + isUnbounded: function() { + return !0; + }, + registerDocumentInteractionHandler: function() {}, + registerInteractionHandler: function() {}, + registerResizeHandler: function() {}, + removeClass: function() {}, + updateCssVariable: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (f.prototype.init = function() { + var t = this, + e = this.supportsPressRipple_(); + if ((this.registerRootHandlers_(e), e)) { + var n = f.cssClasses, + i = n.ROOT, + r = n.UNBOUNDED; + requestAnimationFrame(function() { + t.adapter_.addClass(i), + t.adapter_.isUnbounded() && + (t.adapter_.addClass(r), t.layoutInternal_()); + }); + } + }), + (f.prototype.destroy = function() { + var t = this; + if (this.supportsPressRipple_()) { + this.activationTimer_ && + (clearTimeout(this.activationTimer_), + (this.activationTimer_ = 0), + this.adapter_.removeClass(f.cssClasses.FG_ACTIVATION)), + this.fgDeactivationRemovalTimer_ && + (clearTimeout(this.fgDeactivationRemovalTimer_), + (this.fgDeactivationRemovalTimer_ = 0), + this.adapter_.removeClass(f.cssClasses.FG_DEACTIVATION)); + var e = f.cssClasses, + n = e.ROOT, + i = e.UNBOUNDED; + requestAnimationFrame(function() { + t.adapter_.removeClass(n), + t.adapter_.removeClass(i), + t.removeCssVars_(); + }); + } + this.deregisterRootHandlers_(), + this.deregisterDeactivationHandlers_(); + }), + (f.prototype.activate = function(t) { + this.activate_(t); + }), + (f.prototype.deactivate = function() { + this.deactivate_(); + }), + (f.prototype.layout = function() { + var t = this; + this.layoutFrame_ && cancelAnimationFrame(this.layoutFrame_), + (this.layoutFrame_ = requestAnimationFrame(function() { + t.layoutInternal_(), (t.layoutFrame_ = 0); + })); + }), + (f.prototype.setUnbounded = function(t) { + var e = f.cssClasses.UNBOUNDED; + t ? this.adapter_.addClass(e) : this.adapter_.removeClass(e); + }), + (f.prototype.handleFocus = function() { + var t = this; + requestAnimationFrame(function() { + return t.adapter_.addClass(f.cssClasses.BG_FOCUSED); + }); + }), + (f.prototype.handleBlur = function() { + var t = this; + requestAnimationFrame(function() { + return t.adapter_.removeClass(f.cssClasses.BG_FOCUSED); + }); + }), + (f.prototype.supportsPressRipple_ = function() { + return this.adapter_.browserSupportsCssVars(); + }), + (f.prototype.defaultActivationState_ = function() { + return { + activationEvent: void 0, + hasDeactivationUXRun: !1, + isActivated: !1, + isProgrammatic: !1, + wasActivatedByPointer: !1, + wasElementMadeActive: !1 + }; + }), + (f.prototype.registerRootHandlers_ = function(t) { + var e = this; + t && + (l.forEach(function(t) { + e.adapter_.registerInteractionHandler(t, e.activateHandler_); + }), + this.adapter_.isUnbounded() && + this.adapter_.registerResizeHandler(this.resizeHandler_)), + this.adapter_.registerInteractionHandler( + "focus", + this.focusHandler_ + ), + this.adapter_.registerInteractionHandler( + "blur", + this.blurHandler_ + ); + }), + (f.prototype.registerDeactivationHandlers_ = function(t) { + var e = this; + "keydown" === t.type + ? this.adapter_.registerInteractionHandler( + "keyup", + this.deactivateHandler_ + ) + : d.forEach(function(t) { + e.adapter_.registerDocumentInteractionHandler( + t, + e.deactivateHandler_ + ); + }); + }), + (f.prototype.deregisterRootHandlers_ = function() { + var e = this; + l.forEach(function(t) { + e.adapter_.deregisterInteractionHandler(t, e.activateHandler_); + }), + this.adapter_.deregisterInteractionHandler( + "focus", + this.focusHandler_ + ), + this.adapter_.deregisterInteractionHandler( + "blur", + this.blurHandler_ + ), + this.adapter_.isUnbounded() && + this.adapter_.deregisterResizeHandler(this.resizeHandler_); + }), + (f.prototype.deregisterDeactivationHandlers_ = function() { + var e = this; + this.adapter_.deregisterInteractionHandler( + "keyup", + this.deactivateHandler_ + ), + d.forEach(function(t) { + e.adapter_.deregisterDocumentInteractionHandler( + t, + e.deactivateHandler_ + ); + }); + }), + (f.prototype.removeCssVars_ = function() { + var e = this, + n = f.strings; + Object.keys(n).forEach(function(t) { + 0 === t.indexOf("VAR_") && + e.adapter_.updateCssVariable(n[t], null); + }); + }), + (f.prototype.activate_ = function(t) { + var e = this; + if (!this.adapter_.isSurfaceDisabled()) { + var n = this.activationState_; + if (!n.isActivated) { + var i = this.previousActivationEvent_; + (i && void 0 !== t && i.type !== t.type) || + ((n.isActivated = !0), + (n.isProgrammatic = void 0 === t), + (n.activationEvent = t), + (n.wasActivatedByPointer = + !n.isProgrammatic && + void 0 !== t && + ("mousedown" === t.type || + "touchstart" === t.type || + "pointerdown" === t.type)), + void 0 !== t && + 0 < p.length && + p.some(function(t) { + return e.adapter_.containsEventTarget(t); + }) + ? this.resetActivationState_() + : (void 0 !== t && + (p.push(t.target), + this.registerDeactivationHandlers_(t)), + (n.wasElementMadeActive = this.checkElementMadeActive_( + t + )), + n.wasElementMadeActive && this.animateActivation_(), + requestAnimationFrame(function() { + (p = []), + n.wasElementMadeActive || + void 0 === t || + (" " !== t.key && 32 !== t.keyCode) || + ((n.wasElementMadeActive = e.checkElementMadeActive_( + t + )), + n.wasElementMadeActive && e.animateActivation_()), + n.wasElementMadeActive || + (e.activationState_ = e.defaultActivationState_()); + }))); + } + } + }), + (f.prototype.checkElementMadeActive_ = function(t) { + return ( + void 0 === t || + "keydown" !== t.type || + this.adapter_.isSurfaceActive() + ); + }), + (f.prototype.animateActivation_ = function() { + var t = this, + e = f.strings, + n = e.VAR_FG_TRANSLATE_START, + i = e.VAR_FG_TRANSLATE_END, + r = f.cssClasses, + o = r.FG_DEACTIVATION, + s = r.FG_ACTIVATION, + a = f.numbers.DEACTIVATION_TIMEOUT_MS; + this.layoutInternal_(); + var c = "", + u = ""; + if (!this.adapter_.isUnbounded()) { + var l = this.getFgTranslationCoordinates_(), + d = l.startPoint, + p = l.endPoint; + (c = d.x + "px, " + d.y + "px"), + (u = p.x + "px, " + p.y + "px"); + } + this.adapter_.updateCssVariable(n, c), + this.adapter_.updateCssVariable(i, u), + clearTimeout(this.activationTimer_), + clearTimeout(this.fgDeactivationRemovalTimer_), + this.rmBoundedActivationClasses_(), + this.adapter_.removeClass(o), + this.adapter_.computeBoundingRect(), + this.adapter_.addClass(s), + (this.activationTimer_ = setTimeout(function() { + return t.activationTimerCallback_(); + }, a)); + }), + (f.prototype.getFgTranslationCoordinates_ = function() { + var t, + e = this.activationState_, + n = e.activationEvent; + return { + startPoint: (t = { + x: + (t = e.wasActivatedByPointer + ? u.getNormalizedEventCoords( + n, + this.adapter_.getWindowPageOffset(), + this.adapter_.computeBoundingRect() + ) + : { x: this.frame_.width / 2, y: this.frame_.height / 2 }) + .x - + this.initialSize_ / 2, + y: t.y - this.initialSize_ / 2 + }), + endPoint: { + x: this.frame_.width / 2 - this.initialSize_ / 2, + y: this.frame_.height / 2 - this.initialSize_ / 2 + } + }; + }), + (f.prototype.runDeactivationUXLogicIfReady_ = function() { + var t = this, + e = f.cssClasses.FG_DEACTIVATION, + n = this.activationState_, + i = n.hasDeactivationUXRun, + r = n.isActivated; + (!i && r) || + !this.activationAnimationHasEnded_ || + (this.rmBoundedActivationClasses_(), + this.adapter_.addClass(e), + (this.fgDeactivationRemovalTimer_ = setTimeout(function() { + t.adapter_.removeClass(e); + }, c.numbers.FG_DEACTIVATION_MS))); + }), + (f.prototype.rmBoundedActivationClasses_ = function() { + var t = f.cssClasses.FG_ACTIVATION; + this.adapter_.removeClass(t), + (this.activationAnimationHasEnded_ = !1), + this.adapter_.computeBoundingRect(); + }), + (f.prototype.resetActivationState_ = function() { + var t = this; + (this.previousActivationEvent_ = this.activationState_.activationEvent), + (this.activationState_ = this.defaultActivationState_()), + setTimeout(function() { + return (t.previousActivationEvent_ = void 0); + }, f.numbers.TAP_DELAY_MS); + }), + (f.prototype.deactivate_ = function() { + var t = this, + e = this.activationState_; + if (e.isActivated) { + var n = o({}, e); + e.isProgrammatic + ? (requestAnimationFrame(function() { + return t.animateDeactivation_(n); + }), + this.resetActivationState_()) + : (this.deregisterDeactivationHandlers_(), + requestAnimationFrame(function() { + (t.activationState_.hasDeactivationUXRun = !0), + t.animateDeactivation_(n), + t.resetActivationState_(); + })); + } + }), + (f.prototype.animateDeactivation_ = function(t) { + var e = t.wasActivatedByPointer, + n = t.wasElementMadeActive; + (e || n) && this.runDeactivationUXLogicIfReady_(); + }), + (f.prototype.layoutInternal_ = function() { + var t = this; + this.frame_ = this.adapter_.computeBoundingRect(); + var e = Math.max(this.frame_.height, this.frame_.width); + this.maxRadius_ = this.adapter_.isUnbounded() + ? e + : Math.sqrt( + Math.pow(t.frame_.width, 2) + Math.pow(t.frame_.height, 2) + ) + f.numbers.PADDING; + var n = Math.floor(e * f.numbers.INITIAL_ORIGIN_SCALE); + this.adapter_.isUnbounded() && n % 2 != 0 + ? (this.initialSize_ = n - 1) + : (this.initialSize_ = n), + (this.fgScale_ = "" + this.maxRadius_ / this.initialSize_), + this.updateLayoutCssVars_(); + }), + (f.prototype.updateLayoutCssVars_ = function() { + var t = f.strings, + e = t.VAR_FG_SIZE, + n = t.VAR_LEFT, + i = t.VAR_TOP, + r = t.VAR_FG_SCALE; + this.adapter_.updateCssVariable(e, this.initialSize_ + "px"), + this.adapter_.updateCssVariable(r, this.fgScale_), + this.adapter_.isUnbounded() && + ((this.unboundedCoords_ = { + left: Math.round( + this.frame_.width / 2 - this.initialSize_ / 2 + ), + top: Math.round( + this.frame_.height / 2 - this.initialSize_ / 2 + ) + }), + this.adapter_.updateCssVariable( + n, + this.unboundedCoords_.left + "px" + ), + this.adapter_.updateCssVariable( + i, + this.unboundedCoords_.top + "px" + )); + }), + f); + function f(t) { + var e = s.call(this, o(o({}, f.defaultAdapter), t)) || this; + return ( + (e.activationAnimationHasEnded_ = !1), + (e.activationTimer_ = 0), + (e.fgDeactivationRemovalTimer_ = 0), + (e.fgScale_ = "0"), + (e.frame_ = { width: 0, height: 0 }), + (e.initialSize_ = 0), + (e.layoutFrame_ = 0), + (e.maxRadius_ = 0), + (e.unboundedCoords_ = { left: 0, top: 0 }), + (e.activationState_ = e.defaultActivationState_()), + (e.activationTimerCallback_ = function() { + (e.activationAnimationHasEnded_ = !0), + e.runDeactivationUXLogicIfReady_(); + }), + (e.activateHandler_ = function(t) { + return e.activate_(t); + }), + (e.deactivateHandler_ = function() { + return e.deactivate_(); + }), + (e.focusHandler_ = function() { + return e.handleFocus(); + }), + (e.blurHandler_ = function() { + return e.handleBlur(); + }), + (e.resizeHandler_ = function() { + return e.layout(); + }), + e + ); + } + (e.MDCRippleFoundation = _), (e.default = _); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.applyPassive = function(t) { + return ( + void 0 === t && (t = window), + !!(function(t) { + void 0 === t && (t = window); + var e = !1; + try { + var n = { + get passive() { + return !(e = !0); + } + }, + i = function() {}; + t.document.addEventListener("test", i, n), + t.document.removeEventListener("test", i, n); + } catch (t) { + e = !1; + } + return e; + })(t) && { passive: !0 } + ); + }); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.cssClasses = { + ANCHOR: "mdc-menu-surface--anchor", + ANIMATING_CLOSED: "mdc-menu-surface--animating-closed", + ANIMATING_OPEN: "mdc-menu-surface--animating-open", + FIXED: "mdc-menu-surface--fixed", + IS_OPEN_BELOW: "mdc-menu-surface--is-open-below", + OPEN: "mdc-menu-surface--open", + ROOT: "mdc-menu-surface" + }; + var i = { + CLOSED_EVENT: "MDCMenuSurface:closed", + OPENED_EVENT: "MDCMenuSurface:opened", + FOCUSABLE_ELEMENTS: [ + "button:not(:disabled)", + '[href]:not([aria-disabled="true"])', + "input:not(:disabled)", + "select:not(:disabled)", + "textarea:not(:disabled)", + '[tabindex]:not([tabindex="-1"]):not([aria-disabled="true"])' + ].join(", ") + }; + e.strings = i; + var r, o, s, a; + (e.numbers = { + TRANSITION_OPEN_DURATION: 120, + TRANSITION_CLOSE_DURATION: 75, + MARGIN_TO_EDGE: 32, + ANCHOR_TO_MENU_SURFACE_WIDTH_RATIO: 0.67 + }), + ((o = r = r || {})[(o.BOTTOM = 1)] = "BOTTOM"), + (o[(o.CENTER = 2)] = "CENTER"), + (o[(o.RIGHT = 4)] = "RIGHT"), + (o[(o.FLIP_RTL = 8)] = "FLIP_RTL"), + (e.CornerBit = r), + ((a = s = s || {})[(a.TOP_LEFT = 0)] = "TOP_LEFT"), + (a[(a.TOP_RIGHT = 4)] = "TOP_RIGHT"), + (a[(a.BOTTOM_LEFT = 1)] = "BOTTOM_LEFT"), + (a[(a.BOTTOM_RIGHT = 5)] = "BOTTOM_RIGHT"), + (a[(a.TOP_START = 8)] = "TOP_START"), + (a[(a.TOP_END = 12)] = "TOP_END"), + (a[(a.BOTTOM_START = 9)] = "BOTTOM_START"), + (a[(a.BOTTOM_END = 13)] = "BOTTOM_END"), + (e.Corner = s); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.cssClasses = { + FIXED_CLASS: "mdc-top-app-bar--fixed", + FIXED_SCROLLED_CLASS: "mdc-top-app-bar--fixed-scrolled", + SHORT_CLASS: "mdc-top-app-bar--short", + SHORT_COLLAPSED_CLASS: "mdc-top-app-bar--short-collapsed", + SHORT_HAS_ACTION_ITEM_CLASS: "mdc-top-app-bar--short-has-action-item" + }; + e.numbers = { + DEBOUNCE_THROTTLE_RESIZE_TIME_MS: 100, + MAX_TOP_APP_BAR_HEIGHT: 128 + }; + e.strings = { + ACTION_ITEM_SELECTOR: ".mdc-top-app-bar__action-item", + NAVIGATION_EVENT: "MDCTopAppBar:nav", + NAVIGATION_ICON_SELECTOR: ".mdc-top-app-bar__navigation-icon", + ROOT_SELECTOR: ".mdc-top-app-bar", + TITLE_SELECTOR: ".mdc-top-app-bar__title" + }; + }, + function(t, e, n) { + "use strict"; + var i, r; + Object.defineProperty(e, "__esModule", { value: !0 }), + ((i = e.Direction || (e.Direction = {}))[(i.RIGHT = 0)] = "RIGHT"), + (i[(i.LEFT = 1)] = "LEFT"), + ((r = e.EventSource || (e.EventSource = {}))[(r.PRIMARY = 0)] = + "PRIMARY"), + (r[(r.TRAILING = 1)] = "TRAILING"), + (r[(r.NONE = 2)] = "NONE"), + (e.strings = { + ADDED_ANNOUNCEMENT_ATTRIBUTE: "data-mdc-chip-added-announcement", + ARIA_CHECKED: "aria-checked", + ARROW_DOWN_KEY: "ArrowDown", + ARROW_LEFT_KEY: "ArrowLeft", + ARROW_RIGHT_KEY: "ArrowRight", + ARROW_UP_KEY: "ArrowUp", + BACKSPACE_KEY: "Backspace", + CHECKMARK_SELECTOR: ".mdc-chip__checkmark", + DELETE_KEY: "Delete", + END_KEY: "End", + ENTER_KEY: "Enter", + ENTRY_ANIMATION_NAME: "mdc-chip-entry", + HOME_KEY: "Home", + IE_ARROW_DOWN_KEY: "Down", + IE_ARROW_LEFT_KEY: "Left", + IE_ARROW_RIGHT_KEY: "Right", + IE_ARROW_UP_KEY: "Up", + IE_DELETE_KEY: "Del", + INTERACTION_EVENT: "MDCChip:interaction", + LEADING_ICON_SELECTOR: ".mdc-chip__icon--leading", + NAVIGATION_EVENT: "MDCChip:navigation", + PRIMARY_ACTION_SELECTOR: ".mdc-chip__primary-action", + REMOVED_ANNOUNCEMENT_ATTRIBUTE: + "data-mdc-chip-removed-announcement", + REMOVAL_EVENT: "MDCChip:removal", + SELECTION_EVENT: "MDCChip:selection", + SPACEBAR_KEY: " ", + TAB_INDEX: "tabindex", + TRAILING_ACTION_SELECTOR: ".mdc-chip__trailing-action", + TRAILING_ICON_INTERACTION_EVENT: "MDCChip:trailingIconInteraction", + TRAILING_ICON_SELECTOR: ".mdc-chip__icon--trailing" + }), + (e.cssClasses = { + CHECKMARK: "mdc-chip__checkmark", + CHIP_EXIT: "mdc-chip--exit", + DELETABLE: "mdc-chip--deletable", + HIDDEN_LEADING_ICON: "mdc-chip__icon--leading-hidden", + LEADING_ICON: "mdc-chip__icon--leading", + PRIMARY_ACTION: "mdc-chip__primary-action", + PRIMARY_ACTION_FOCUSED: "mdc-chip--primary-action-focused", + SELECTED: "mdc-chip--selected", + TEXT: "mdc-chip__text", + TRAILING_ACTION: "mdc-chip__trailing-action", + TRAILING_ICON: "mdc-chip__icon--trailing" + }), + (e.navigationKeys = new Set()), + e.navigationKeys.add(e.strings.ARROW_LEFT_KEY), + e.navigationKeys.add(e.strings.ARROW_RIGHT_KEY), + e.navigationKeys.add(e.strings.ARROW_DOWN_KEY), + e.navigationKeys.add(e.strings.ARROW_UP_KEY), + e.navigationKeys.add(e.strings.END_KEY), + e.navigationKeys.add(e.strings.HOME_KEY), + e.navigationKeys.add(e.strings.IE_ARROW_LEFT_KEY), + e.navigationKeys.add(e.strings.IE_ARROW_RIGHT_KEY), + e.navigationKeys.add(e.strings.IE_ARROW_DOWN_KEY), + e.navigationKeys.add(e.strings.IE_ARROW_UP_KEY), + (e.jumpChipKeys = new Set()), + e.jumpChipKeys.add(e.strings.ARROW_UP_KEY), + e.jumpChipKeys.add(e.strings.ARROW_DOWN_KEY), + e.jumpChipKeys.add(e.strings.HOME_KEY), + e.jumpChipKeys.add(e.strings.END_KEY), + e.jumpChipKeys.add(e.strings.IE_ARROW_UP_KEY), + e.jumpChipKeys.add(e.strings.IE_ARROW_DOWN_KEY); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + var i = { + LIST_ITEM_ACTIVATED_CLASS: "mdc-list-item--activated", + LIST_ITEM_CLASS: "mdc-list-item", + LIST_ITEM_DISABLED_CLASS: "mdc-list-item--disabled", + LIST_ITEM_SELECTED_CLASS: "mdc-list-item--selected", + ROOT: "mdc-list" + }, + r = { + ACTION_EVENT: "MDCList:action", + ARIA_CHECKED: "aria-checked", + ARIA_CHECKED_CHECKBOX_SELECTOR: + '[role="checkbox"][aria-checked="true"]', + ARIA_CHECKED_RADIO_SELECTOR: '[role="radio"][aria-checked="true"]', + ARIA_CURRENT: "aria-current", + ARIA_DISABLED: "aria-disabled", + ARIA_ORIENTATION: "aria-orientation", + ARIA_ORIENTATION_HORIZONTAL: "horizontal", + ARIA_ROLE_CHECKBOX_SELECTOR: '[role="checkbox"]', + ARIA_SELECTED: "aria-selected", + CHECKBOX_RADIO_SELECTOR: + 'input[type="checkbox"], input[type="radio"]', + CHECKBOX_SELECTOR: 'input[type="checkbox"]', + CHILD_ELEMENTS_TO_TOGGLE_TABINDEX: + "\n ." + + (e.cssClasses = i).LIST_ITEM_CLASS + + " button:not(:disabled),\n ." + + i.LIST_ITEM_CLASS + + " a\n ", + FOCUSABLE_CHILD_ELEMENTS: + "\n ." + + i.LIST_ITEM_CLASS + + " button:not(:disabled),\n ." + + i.LIST_ITEM_CLASS + + " a,\n ." + + i.LIST_ITEM_CLASS + + ' input[type="radio"]:not(:disabled),\n .' + + i.LIST_ITEM_CLASS + + ' input[type="checkbox"]:not(:disabled)\n ', + RADIO_SELECTOR: 'input[type="radio"]' + }; + e.strings = r; + e.numbers = { UNSET_INDEX: -1 }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s = n(0), + f = n(9), + a = ["input", "button", "textarea", "select"]; + var c, + u = + ((c = s.MDCFoundation), + r(l, c), + Object.defineProperty(l, "strings", { + get: function() { + return f.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "cssClasses", { + get: function() { + return f.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "numbers", { + get: function() { + return f.numbers; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClassForElementIndex: function() {}, + focusItemAtIndex: function() {}, + getAttributeForElementIndex: function() { + return null; + }, + getFocusedElementIndex: function() { + return 0; + }, + getListItemCount: function() { + return 0; + }, + hasCheckboxAtIndex: function() { + return !1; + }, + hasRadioAtIndex: function() { + return !1; + }, + isCheckboxCheckedAtIndex: function() { + return !1; + }, + isFocusInsideList: function() { + return !1; + }, + isRootFocused: function() { + return !1; + }, + listItemAtIndexHasClass: function() { + return !1; + }, + notifyAction: function() {}, + removeClassForElementIndex: function() {}, + setAttributeForElementIndex: function() {}, + setCheckedCheckboxOrRadioAtIndex: function() {}, + setTabIndexForListItemChildren: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.layout = function() { + 0 !== this.adapter_.getListItemCount() && + (this.adapter_.hasCheckboxAtIndex(0) + ? (this.isCheckboxList_ = !0) + : this.adapter_.hasRadioAtIndex(0) && + (this.isRadioList_ = !0)); + }), + (l.prototype.setWrapFocus = function(t) { + this.wrapFocus_ = t; + }), + (l.prototype.setVerticalOrientation = function(t) { + this.isVertical_ = t; + }), + (l.prototype.setSingleSelection = function(t) { + this.isSingleSelectionList_ = t; + }), + (l.prototype.setUseActivatedClass = function(t) { + this.useActivatedClass_ = t; + }), + (l.prototype.getSelectedIndex = function() { + return this.selectedIndex_; + }), + (l.prototype.setSelectedIndex = function(t) { + this.isIndexValid_(t) && + (this.isCheckboxList_ + ? this.setCheckboxAtIndex_(t) + : this.isRadioList_ + ? this.setRadioAtIndex_(t) + : this.setSingleSelectionAtIndex_(t)); + }), + (l.prototype.handleFocusIn = function(t, e) { + 0 <= e && this.adapter_.setTabIndexForListItemChildren(e, "0"); + }), + (l.prototype.handleFocusOut = function(t, e) { + var n = this; + 0 <= e && this.adapter_.setTabIndexForListItemChildren(e, "-1"), + setTimeout(function() { + n.adapter_.isFocusInsideList() || + n.setTabindexToFirstSelectedItem_(); + }, 0); + }), + (l.prototype.handleKeydown = function(t, e, n) { + var i = "ArrowLeft" === t.key || 37 === t.keyCode, + r = "ArrowUp" === t.key || 38 === t.keyCode, + o = "ArrowRight" === t.key || 39 === t.keyCode, + s = "ArrowDown" === t.key || 40 === t.keyCode, + a = "Home" === t.key || 36 === t.keyCode, + c = "End" === t.key || 35 === t.keyCode, + u = "Enter" === t.key || 13 === t.keyCode, + l = "Space" === t.key || 32 === t.keyCode; + if (this.adapter_.isRootFocused()) + r || c + ? (t.preventDefault(), this.focusLastElement()) + : (s || a) && (t.preventDefault(), this.focusFirstElement()); + else { + var d = this.adapter_.getFocusedElementIndex(); + if (!(-1 === d && (d = n) < 0)) { + var p; + if ((this.isVertical_ && s) || (!this.isVertical_ && o)) + this.preventDefaultEvent_(t), + (p = this.focusNextElement(d)); + else if ((this.isVertical_ && r) || (!this.isVertical_ && i)) + this.preventDefaultEvent_(t), + (p = this.focusPrevElement(d)); + else if (a) + this.preventDefaultEvent_(t), + (p = this.focusFirstElement()); + else if (c) + this.preventDefaultEvent_(t), (p = this.focusLastElement()); + else if ((u || l) && e) { + var _ = t.target; + if (_ && "A" === _.tagName && u) return; + if ( + (this.preventDefaultEvent_(t), + this.adapter_.listItemAtIndexHasClass( + d, + f.cssClasses.LIST_ITEM_DISABLED_CLASS + )) + ) + return; + this.isSelectableList_() && + this.setSelectedIndexOnAction_(d), + this.adapter_.notifyAction(d); + } + (this.focusedItemIndex_ = d), + void 0 !== p && + (this.setTabindexAtIndex_(p), + (this.focusedItemIndex_ = p)); + } + } + }), + (l.prototype.handleClick = function(t, e) { + t !== f.numbers.UNSET_INDEX && + (this.setTabindexAtIndex_(t), + (this.focusedItemIndex_ = t), + this.adapter_.listItemAtIndexHasClass( + t, + f.cssClasses.LIST_ITEM_DISABLED_CLASS + ) || + (this.isSelectableList_() && + this.setSelectedIndexOnAction_(t, e), + this.adapter_.notifyAction(t))); + }), + (l.prototype.focusNextElement = function(t) { + var e = t + 1; + if (this.adapter_.getListItemCount() <= e) { + if (!this.wrapFocus_) return t; + e = 0; + } + return this.adapter_.focusItemAtIndex(e), e; + }), + (l.prototype.focusPrevElement = function(t) { + var e = t - 1; + if (e < 0) { + if (!this.wrapFocus_) return t; + e = this.adapter_.getListItemCount() - 1; + } + return this.adapter_.focusItemAtIndex(e), e; + }), + (l.prototype.focusFirstElement = function() { + return this.adapter_.focusItemAtIndex(0), 0; + }), + (l.prototype.focusLastElement = function() { + var t = this.adapter_.getListItemCount() - 1; + return this.adapter_.focusItemAtIndex(t), t; + }), + (l.prototype.setEnabled = function(t, e) { + this.isIndexValid_(t) && + (e + ? (this.adapter_.removeClassForElementIndex( + t, + f.cssClasses.LIST_ITEM_DISABLED_CLASS + ), + this.adapter_.setAttributeForElementIndex( + t, + f.strings.ARIA_DISABLED, + "false" + )) + : (this.adapter_.addClassForElementIndex( + t, + f.cssClasses.LIST_ITEM_DISABLED_CLASS + ), + this.adapter_.setAttributeForElementIndex( + t, + f.strings.ARIA_DISABLED, + "true" + ))); + }), + (l.prototype.preventDefaultEvent_ = function(t) { + var e = ("" + t.target.tagName).toLowerCase(); + -1 === a.indexOf(e) && t.preventDefault(); + }), + (l.prototype.setSingleSelectionAtIndex_ = function(t) { + if (this.selectedIndex_ !== t) { + var e = f.cssClasses.LIST_ITEM_SELECTED_CLASS; + this.useActivatedClass_ && + (e = f.cssClasses.LIST_ITEM_ACTIVATED_CLASS), + this.selectedIndex_ !== f.numbers.UNSET_INDEX && + this.adapter_.removeClassForElementIndex( + this.selectedIndex_, + e + ), + this.adapter_.addClassForElementIndex(t, e), + this.setAriaForSingleSelectionAtIndex_(t), + (this.selectedIndex_ = t); + } + }), + (l.prototype.setAriaForSingleSelectionAtIndex_ = function(t) { + this.selectedIndex_ === f.numbers.UNSET_INDEX && + (this.ariaCurrentAttrValue_ = this.adapter_.getAttributeForElementIndex( + t, + f.strings.ARIA_CURRENT + )); + var e = null !== this.ariaCurrentAttrValue_, + n = e ? f.strings.ARIA_CURRENT : f.strings.ARIA_SELECTED; + this.selectedIndex_ !== f.numbers.UNSET_INDEX && + this.adapter_.setAttributeForElementIndex( + this.selectedIndex_, + n, + "false" + ); + var i = e ? this.ariaCurrentAttrValue_ : "true"; + this.adapter_.setAttributeForElementIndex(t, n, i); + }), + (l.prototype.setRadioAtIndex_ = function(t) { + this.adapter_.setCheckedCheckboxOrRadioAtIndex(t, !0), + this.selectedIndex_ !== f.numbers.UNSET_INDEX && + this.adapter_.setAttributeForElementIndex( + this.selectedIndex_, + f.strings.ARIA_CHECKED, + "false" + ), + this.adapter_.setAttributeForElementIndex( + t, + f.strings.ARIA_CHECKED, + "true" + ), + (this.selectedIndex_ = t); + }), + (l.prototype.setCheckboxAtIndex_ = function(t) { + for (var e = 0; e < this.adapter_.getListItemCount(); e++) { + var n = !1; + 0 <= t.indexOf(e) && (n = !0), + this.adapter_.setCheckedCheckboxOrRadioAtIndex(e, n), + this.adapter_.setAttributeForElementIndex( + e, + f.strings.ARIA_CHECKED, + n ? "true" : "false" + ); + } + this.selectedIndex_ = t; + }), + (l.prototype.setTabindexAtIndex_ = function(t) { + this.focusedItemIndex_ === f.numbers.UNSET_INDEX && 0 !== t + ? this.adapter_.setAttributeForElementIndex(0, "tabindex", "-1") + : 0 <= this.focusedItemIndex_ && + this.focusedItemIndex_ !== t && + this.adapter_.setAttributeForElementIndex( + this.focusedItemIndex_, + "tabindex", + "-1" + ), + this.adapter_.setAttributeForElementIndex(t, "tabindex", "0"); + }), + (l.prototype.isSelectableList_ = function() { + return ( + this.isSingleSelectionList_ || + this.isCheckboxList_ || + this.isRadioList_ + ); + }), + (l.prototype.setTabindexToFirstSelectedItem_ = function() { + var t = 0; + this.isSelectableList_() && + ("number" == typeof this.selectedIndex_ && + this.selectedIndex_ !== f.numbers.UNSET_INDEX + ? (t = this.selectedIndex_) + : (function(t) { + return t instanceof Array; + })(this.selectedIndex_) && + 0 < this.selectedIndex_.length && + (t = this.selectedIndex_.reduce(function(t, e) { + return Math.min(t, e); + }))), + this.setTabindexAtIndex_(t); + }), + (l.prototype.isIndexValid_ = function(t) { + var e = this; + if (t instanceof Array) { + if (!this.isCheckboxList_) + throw new Error( + "MDCListFoundation: Array of index is only supported for checkbox based list" + ); + return ( + 0 === t.length || + t.some(function(t) { + return e.isIndexInRange_(t); + }) + ); + } + if ("number" != typeof t) return !1; + if (this.isCheckboxList_) + throw new Error( + "MDCListFoundation: Expected array of index for checkbox based list but got number: " + + t + ); + return this.isIndexInRange_(t); + }), + (l.prototype.isIndexInRange_ = function(t) { + var e = this.adapter_.getListItemCount(); + return 0 <= t && t < e; + }), + (l.prototype.setSelectedIndexOnAction_ = function(t, e) { + void 0 === e && (e = !0), + this.isCheckboxList_ + ? this.toggleCheckboxAtIndex_(t, e) + : this.setSelectedIndex(t); + }), + (l.prototype.toggleCheckboxAtIndex_ = function(e, t) { + var n = this.adapter_.isCheckboxCheckedAtIndex(e); + t && + ((n = !n), + this.adapter_.setCheckedCheckboxOrRadioAtIndex(e, n)), + this.adapter_.setAttributeForElementIndex( + e, + f.strings.ARIA_CHECKED, + n ? "true" : "false" + ); + var i = + this.selectedIndex_ === f.numbers.UNSET_INDEX + ? [] + : this.selectedIndex_.slice(); + n + ? i.push(e) + : (i = i.filter(function(t) { + return t !== e; + })), + (this.selectedIndex_ = i); + }), + l); + function l(t) { + var e = c.call(this, o(o({}, l.defaultAdapter), t)) || this; + return ( + (e.wrapFocus_ = !1), + (e.isVertical_ = !0), + (e.isSingleSelectionList_ = !1), + (e.selectedIndex_ = f.numbers.UNSET_INDEX), + (e.focusedItemIndex_ = f.numbers.UNSET_INDEX), + (e.useActivatedClass_ = !1), + (e.ariaCurrentAttrValue_ = null), + (e.isCheckboxList_ = !1), + (e.isRadioList_ = !1), + e + ); + } + (e.MDCListFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }, + d = + (this && this.__values) || + function(t) { + var e = "function" == typeof Symbol && Symbol.iterator, + n = e && t[e], + i = 0; + if (n) return n.call(t); + if (t && "number" == typeof t.length) + return { + next: function() { + return ( + t && i >= t.length && (t = void 0), + { value: t && t[i++], done: !t } + ); + } + }; + throw new TypeError( + e + ? "Object is not iterable." + : "Symbol.iterator is not defined." + ); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + y = n(6), + c = + ((s = a.MDCFoundation), + r(E, s), + Object.defineProperty(E, "cssClasses", { + get: function() { + return y.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(E, "strings", { + get: function() { + return y.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(E, "numbers", { + get: function() { + return y.numbers; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(E, "Corner", { + get: function() { + return y.Corner; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(E, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + hasClass: function() { + return !1; + }, + hasAnchor: function() { + return !1; + }, + isElementInContainer: function() { + return !1; + }, + isFocused: function() { + return !1; + }, + isRtl: function() { + return !1; + }, + getInnerDimensions: function() { + return { height: 0, width: 0 }; + }, + getAnchorDimensions: function() { + return null; + }, + getWindowDimensions: function() { + return { height: 0, width: 0 }; + }, + getBodyDimensions: function() { + return { height: 0, width: 0 }; + }, + getWindowScroll: function() { + return { x: 0, y: 0 }; + }, + setPosition: function() {}, + setMaxHeight: function() {}, + setTransformOrigin: function() {}, + saveFocus: function() {}, + restoreFocus: function() {}, + notifyClose: function() {}, + notifyOpen: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (E.prototype.init = function() { + var t = E.cssClasses, + e = t.ROOT, + n = t.OPEN; + if (!this.adapter_.hasClass(e)) + throw new Error(e + " class required in root element."); + this.adapter_.hasClass(n) && (this.isOpen_ = !0); + }), + (E.prototype.destroy = function() { + clearTimeout(this.openAnimationEndTimerId_), + clearTimeout(this.closeAnimationEndTimerId_), + cancelAnimationFrame(this.animationRequestId_); + }), + (E.prototype.setAnchorCorner = function(t) { + this.anchorCorner_ = t; + }), + (E.prototype.flipCornerHorizontally = function() { + this.originCorner_ = this.originCorner_ ^ y.CornerBit.RIGHT; + }), + (E.prototype.setAnchorMargin = function(t) { + (this.anchorMargin_.top = t.top || 0), + (this.anchorMargin_.right = t.right || 0), + (this.anchorMargin_.bottom = t.bottom || 0), + (this.anchorMargin_.left = t.left || 0); + }), + (E.prototype.setIsHoisted = function(t) { + this.isHoistedElement_ = t; + }), + (E.prototype.setFixedPosition = function(t) { + this.isFixedPosition_ = t; + }), + (E.prototype.setAbsolutePosition = function(t, e) { + (this.position_.x = this.isFinite_(t) ? t : 0), + (this.position_.y = this.isFinite_(e) ? e : 0); + }), + (E.prototype.setQuickOpen = function(t) { + this.isQuickOpen_ = t; + }), + (E.prototype.isOpen = function() { + return this.isOpen_; + }), + (E.prototype.open = function() { + var t = this; + this.isOpen_ || + (this.adapter_.saveFocus(), + this.isQuickOpen_ + ? ((this.isOpen_ = !0), + this.adapter_.addClass(E.cssClasses.OPEN), + (this.dimensions_ = this.adapter_.getInnerDimensions()), + this.autoPosition_(), + this.adapter_.notifyOpen()) + : (this.adapter_.addClass(E.cssClasses.ANIMATING_OPEN), + (this.animationRequestId_ = requestAnimationFrame( + function() { + t.adapter_.addClass(E.cssClasses.OPEN), + (t.dimensions_ = t.adapter_.getInnerDimensions()), + t.autoPosition_(), + (t.openAnimationEndTimerId_ = setTimeout(function() { + (t.openAnimationEndTimerId_ = 0), + t.adapter_.removeClass( + E.cssClasses.ANIMATING_OPEN + ), + t.adapter_.notifyOpen(); + }, y.numbers.TRANSITION_OPEN_DURATION)); + } + )), + (this.isOpen_ = !0))); + }), + (E.prototype.close = function(t) { + var e = this; + void 0 === t && (t = !1), + this.isOpen_ && + (this.isQuickOpen_ + ? ((this.isOpen_ = !1), + t || this.maybeRestoreFocus_(), + this.adapter_.removeClass(E.cssClasses.OPEN), + this.adapter_.removeClass(E.cssClasses.IS_OPEN_BELOW), + this.adapter_.notifyClose()) + : (this.adapter_.addClass(E.cssClasses.ANIMATING_CLOSED), + requestAnimationFrame(function() { + e.adapter_.removeClass(E.cssClasses.OPEN), + e.adapter_.removeClass(E.cssClasses.IS_OPEN_BELOW), + (e.closeAnimationEndTimerId_ = setTimeout(function() { + (e.closeAnimationEndTimerId_ = 0), + e.adapter_.removeClass( + E.cssClasses.ANIMATING_CLOSED + ), + e.adapter_.notifyClose(); + }, y.numbers.TRANSITION_CLOSE_DURATION)); + }), + (this.isOpen_ = !1), + t || this.maybeRestoreFocus_())); + }), + (E.prototype.handleBodyClick = function(t) { + var e = t.target; + this.adapter_.isElementInContainer(e) || this.close(); + }), + (E.prototype.handleKeydown = function(t) { + var e = t.keyCode; + ("Escape" !== t.key && 27 !== e) || this.close(); + }), + (E.prototype.autoPosition_ = function() { + var t; + this.measurements_ = this.getAutoLayoutMeasurements_(); + var e = this.getOriginCorner_(), + n = this.getMenuSurfaceMaxHeight_(e), + i = this.hasBit_(e, y.CornerBit.BOTTOM) ? "bottom" : "top", + r = this.hasBit_(e, y.CornerBit.RIGHT) ? "right" : "left", + o = this.getHorizontalOriginOffset_(e), + s = this.getVerticalOriginOffset_(e), + a = this.measurements_, + c = a.anchorSize, + u = a.surfaceSize, + l = (((t = {})[r] = o), (t[i] = s), t); + c.width / u.width > + y.numbers.ANCHOR_TO_MENU_SURFACE_WIDTH_RATIO && (r = "center"), + (this.isHoistedElement_ || this.isFixedPosition_) && + this.adjustPositionForHoistedElement_(l), + this.adapter_.setTransformOrigin(r + " " + i), + this.adapter_.setPosition(l), + this.adapter_.setMaxHeight(n ? n + "px" : ""), + this.hasBit_(e, y.CornerBit.BOTTOM) || + this.adapter_.addClass(E.cssClasses.IS_OPEN_BELOW); + }), + (E.prototype.getAutoLayoutMeasurements_ = function() { + var t = this.adapter_.getAnchorDimensions(), + e = this.adapter_.getBodyDimensions(), + n = this.adapter_.getWindowDimensions(), + i = this.adapter_.getWindowScroll(); + return { + anchorSize: (t = t || { + top: this.position_.y, + right: this.position_.x, + bottom: this.position_.y, + left: this.position_.x, + width: 0, + height: 0 + }), + bodySize: e, + surfaceSize: this.dimensions_, + viewportDistance: { + top: t.top, + right: n.width - t.right, + bottom: n.height - t.bottom, + left: t.left + }, + viewportSize: n, + windowScroll: i + }; + }), + (E.prototype.getOriginCorner_ = function() { + var t, + e, + n = this.originCorner_, + i = this.measurements_, + r = i.viewportDistance, + o = i.anchorSize, + s = i.surfaceSize, + a = E.numbers.MARGIN_TO_EDGE; + !( + 0 < + (e = this.hasBit_(this.anchorCorner_, y.CornerBit.BOTTOM) + ? ((t = r.top - a + o.height + this.anchorMargin_.bottom), + r.bottom - a - this.anchorMargin_.bottom) + : ((t = r.top - a + this.anchorMargin_.top), + r.bottom - a + o.height - this.anchorMargin_.top)) - + s.height + ) && + e <= t && + (n = this.setBit_(n, y.CornerBit.BOTTOM)); + var c, + u, + l = this.adapter_.isRtl(), + d = this.hasBit_(this.anchorCorner_, y.CornerBit.FLIP_RTL), + p = this.hasBit_(this.anchorCorner_, y.CornerBit.RIGHT), + _ = !1; + u = (_ = l && d ? !p : p) + ? ((c = r.left + o.width + this.anchorMargin_.right), + r.right - this.anchorMargin_.right) + : ((c = r.left + this.anchorMargin_.left), + r.right + o.width - this.anchorMargin_.left); + var f = 0 < c - s.width, + h = 0 < u - s.width, + C = + this.hasBit_(n, y.CornerBit.FLIP_RTL) && + this.hasBit_(n, y.CornerBit.RIGHT); + return ( + (h && C && l) || (!f && C) + ? (n = this.unsetBit_(n, y.CornerBit.RIGHT)) + : ((f && _ && l) || (f && !_ && p) || (!h && u <= c)) && + (n = this.setBit_(n, y.CornerBit.RIGHT)), + n + ); + }), + (E.prototype.getMenuSurfaceMaxHeight_ = function(t) { + var e = this.measurements_.viewportDistance, + n = 0, + i = this.hasBit_(t, y.CornerBit.BOTTOM), + r = this.hasBit_(this.anchorCorner_, y.CornerBit.BOTTOM), + o = E.numbers.MARGIN_TO_EDGE; + return ( + i + ? ((n = e.top + this.anchorMargin_.top - o), + r || (n += this.measurements_.anchorSize.height)) + : ((n = + e.bottom - + this.anchorMargin_.bottom + + this.measurements_.anchorSize.height - + o), + r && (n -= this.measurements_.anchorSize.height)), + n + ); + }), + (E.prototype.getHorizontalOriginOffset_ = function(t) { + var e = this.measurements_.anchorSize, + n = this.hasBit_(t, y.CornerBit.RIGHT), + i = this.hasBit_(this.anchorCorner_, y.CornerBit.RIGHT); + if (n) { + var r = i + ? e.width - this.anchorMargin_.left + : this.anchorMargin_.right; + return this.isHoistedElement_ || this.isFixedPosition_ + ? r - + (this.measurements_.viewportSize.width - + this.measurements_.bodySize.width) + : r; + } + return i + ? e.width - this.anchorMargin_.right + : this.anchorMargin_.left; + }), + (E.prototype.getVerticalOriginOffset_ = function(t) { + var e = this.measurements_.anchorSize, + n = this.hasBit_(t, y.CornerBit.BOTTOM), + i = this.hasBit_(this.anchorCorner_, y.CornerBit.BOTTOM); + return n + ? i + ? e.height - this.anchorMargin_.top + : -this.anchorMargin_.bottom + : i + ? e.height + this.anchorMargin_.bottom + : this.anchorMargin_.top; + }), + (E.prototype.adjustPositionForHoistedElement_ = function(t) { + var e, + n, + i = this.measurements_, + r = i.windowScroll, + o = i.viewportDistance, + s = Object.keys(t); + try { + for (var a = d(s), c = a.next(); !c.done; c = a.next()) { + var u = c.value, + l = t[u] || 0; + (l += o[u]), + this.isFixedPosition_ || + ("top" === u + ? (l += r.y) + : "bottom" === u + ? (l -= r.y) + : "left" === u + ? (l += r.x) + : (l -= r.x)), + (t[u] = l); + } + } catch (t) { + e = { error: t }; + } finally { + try { + c && !c.done && (n = a.return) && n.call(a); + } finally { + if (e) throw e.error; + } + } + }), + (E.prototype.maybeRestoreFocus_ = function() { + var t = this.adapter_.isFocused(), + e = + document.activeElement && + this.adapter_.isElementInContainer(document.activeElement); + (t || e) && this.adapter_.restoreFocus(); + }), + (E.prototype.hasBit_ = function(t, e) { + return Boolean(t & e); + }), + (E.prototype.setBit_ = function(t, e) { + return t | e; + }), + (E.prototype.unsetBit_ = function(t, e) { + return t ^ e; + }), + (E.prototype.isFinite_ = function(t) { + return "number" == typeof t && isFinite(t); + }), + E); + function E(t) { + var e = s.call(this, o(o({}, E.defaultAdapter), t)) || this; + return ( + (e.isOpen_ = !1), + (e.isQuickOpen_ = !1), + (e.isHoistedElement_ = !1), + (e.isFixedPosition_ = !1), + (e.openAnimationEndTimerId_ = 0), + (e.closeAnimationEndTimerId_ = 0), + (e.animationRequestId_ = 0), + (e.anchorCorner_ = y.Corner.TOP_START), + (e.originCorner_ = y.Corner.TOP_START), + (e.anchorMargin_ = { top: 0, right: 0, bottom: 0, left: 0 }), + (e.position_ = { x: 0, y: 0 }), + e + ); + } + (e.MDCMenuSurfaceFoundation = c), (e.default = c); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.cssClasses = { + MENU_SELECTED_LIST_ITEM: "mdc-menu-item--selected", + MENU_SELECTION_GROUP: "mdc-menu__selection-group", + ROOT: "mdc-menu" + }; + e.strings = { + ARIA_CHECKED_ATTR: "aria-checked", + ARIA_DISABLED_ATTR: "aria-disabled", + CHECKBOX_SELECTOR: 'input[type="checkbox"]', + LIST_SELECTOR: ".mdc-list", + SELECTED_EVENT: "MDCMenu:selected" + }; + var i, r; + (e.numbers = { FOCUS_ROOT_INDEX: -1 }), + ((r = i = i || {})[(r.NONE = 0)] = "NONE"), + (r[(r.LIST_ROOT = 1)] = "LIST_ROOT"), + (r[(r.FIRST_ITEM = 2)] = "FIRST_ITEM"), + (r[(r.LAST_ITEM = 3)] = "LAST_ITEM"), + (e.DefaultFocusState = i); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.cssClasses = { + CLOSING: "mdc-snackbar--closing", + OPEN: "mdc-snackbar--open", + OPENING: "mdc-snackbar--opening" + }; + e.strings = { + ACTION_SELECTOR: ".mdc-snackbar__action", + ARIA_LIVE_LABEL_TEXT_ATTR: "data-mdc-snackbar-label-text", + CLOSED_EVENT: "MDCSnackbar:closed", + CLOSING_EVENT: "MDCSnackbar:closing", + DISMISS_SELECTOR: ".mdc-snackbar__dismiss", + LABEL_SELECTOR: ".mdc-snackbar__label", + OPENED_EVENT: "MDCSnackbar:opened", + OPENING_EVENT: "MDCSnackbar:opening", + REASON_ACTION: "action", + REASON_DISMISS: "dismiss", + SURFACE_SELECTOR: ".mdc-snackbar__surface" + }; + e.numbers = { + DEFAULT_AUTO_DISMISS_TIMEOUT_MS: 5e3, + INDETERMINATE: -1, + MAX_AUTO_DISMISS_TIMEOUT_MS: 1e4, + MIN_AUTO_DISMISS_TIMEOUT_MS: 4e3, + SNACKBAR_ANIMATION_CLOSE_TIME_MS: 75, + SNACKBAR_ANIMATION_OPEN_TIME_MS: 150, + ARIA_LIVE_DELAY_MS: 1e3 + }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(90), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + computeContentClientRect: function() { + return { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 0, + height: 0 + }; + }, + setContentStyleProperty: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.computeContentClientRect = function() { + return this.adapter_.computeContentClientRect(); + }), + l); + function l(t) { + return s.call(this, o(o({}, l.defaultAdapter), t)) || this; + } + (e.MDCTabIndicatorFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s = { + animation: { prefixed: "-webkit-animation", standard: "animation" }, + transform: { prefixed: "-webkit-transform", standard: "transform" }, + transition: { + prefixed: "-webkit-transition", + standard: "transition" + } + }, + a = { + animationend: { + cssProperty: "animation", + prefixed: "webkitAnimationEnd", + standard: "animationend" + }, + animationiteration: { + cssProperty: "animation", + prefixed: "webkitAnimationIteration", + standard: "animationiteration" + }, + animationstart: { + cssProperty: "animation", + prefixed: "webkitAnimationStart", + standard: "animationstart" + }, + transitionend: { + cssProperty: "transition", + prefixed: "webkitTransitionEnd", + standard: "transitionend" + } + }; + function c(t) { + return ( + Boolean(t.document) && "function" == typeof t.document.createElement + ); + } + (e.getCorrectPropertyName = function(t, e) { + if (c(t) && e in s) { + var n = t.document.createElement("div"), + i = s[e], + r = i.standard, + o = i.prefixed; + return r in n.style ? r : o; + } + return e; + }), + (e.getCorrectEventName = function(t, e) { + if (c(t) && e in a) { + var n = t.document.createElement("div"), + i = a[e], + r = i.standard, + o = i.prefixed; + return i.cssProperty in n.style ? r : o; + } + return e; + }); + }, + function(t, e, n) { + "use strict"; + var s; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.supportsCssVariables = function(t, e) { + void 0 === e && (e = !1); + var n, + i = t.CSS; + if ("boolean" == typeof s && !e) return s; + if (!(i && "function" == typeof i.supports)) return !1; + var r = i.supports("--css-vars", "yes"), + o = + i.supports("(--css-vars: yes)") && + i.supports("color", "#00000000"); + return (n = r || o), e || (s = n), n; + }), + (e.getNormalizedEventCoords = function(t, e, n) { + if (!t) return { x: 0, y: 0 }; + var i, + r, + o = e.x, + s = e.y, + a = o + n.left, + c = s + n.top; + if ("touchstart" === t.type) { + var u = t; + (i = u.changedTouches[0].pageX - a), + (r = u.changedTouches[0].pageY - c); + } else { + var l = t; + (i = l.pageX - a), (r = l.pageY - c); + } + return { x: i, y: r }; + }); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.cssClasses = { + ANIM_CHECKED_INDETERMINATE: + "mdc-checkbox--anim-checked-indeterminate", + ANIM_CHECKED_UNCHECKED: "mdc-checkbox--anim-checked-unchecked", + ANIM_INDETERMINATE_CHECKED: + "mdc-checkbox--anim-indeterminate-checked", + ANIM_INDETERMINATE_UNCHECKED: + "mdc-checkbox--anim-indeterminate-unchecked", + ANIM_UNCHECKED_CHECKED: "mdc-checkbox--anim-unchecked-checked", + ANIM_UNCHECKED_INDETERMINATE: + "mdc-checkbox--anim-unchecked-indeterminate", + BACKGROUND: "mdc-checkbox__background", + CHECKED: "mdc-checkbox--checked", + CHECKMARK: "mdc-checkbox__checkmark", + CHECKMARK_PATH: "mdc-checkbox__checkmark-path", + DISABLED: "mdc-checkbox--disabled", + INDETERMINATE: "mdc-checkbox--indeterminate", + MIXEDMARK: "mdc-checkbox__mixedmark", + NATIVE_CONTROL: "mdc-checkbox__native-control", + ROOT: "mdc-checkbox", + SELECTED: "mdc-checkbox--selected", + UPGRADED: "mdc-checkbox--upgraded" + }), + (e.strings = { + ARIA_CHECKED_ATTR: "aria-checked", + ARIA_CHECKED_INDETERMINATE_VALUE: "mixed", + DATA_INDETERMINATE_ATTR: "data-indeterminate", + NATIVE_CONTROL_SELECTOR: ".mdc-checkbox__native-control", + TRANSITION_STATE_CHECKED: "checked", + TRANSITION_STATE_INDETERMINATE: "indeterminate", + TRANSITION_STATE_INIT: "init", + TRANSITION_STATE_UNCHECKED: "unchecked" + }), + (e.numbers = { ANIM_END_LATCH_MS: 250 }); + }, + function(t, e, n) { + "use strict"; + var i; + Object.defineProperty(e, "__esModule", { value: !0 }), + ((i = e.InteractionTrigger || (e.InteractionTrigger = {}))[ + (i.UNSPECIFIED = 0) + ] = "UNSPECIFIED"), + (i[(i.CLICK = 1)] = "CLICK"), + (i[(i.BACKSPACE_KEY = 2)] = "BACKSPACE_KEY"), + (i[(i.DELETE_KEY = 3)] = "DELETE_KEY"), + (i[(i.SPACEBAR_KEY = 4)] = "SPACEBAR_KEY"), + (i[(i.ENTER_KEY = 5)] = "ENTER_KEY"), + (e.strings = { + ARIA_HIDDEN: "aria-hidden", + INTERACTION_EVENT: "MDCChipTrailingAction:interaction", + NAVIGATION_EVENT: "MDCChipTrailingAction:navigation", + TAB_INDEX: "tabindex" + }); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + u = n(8), + c = { bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0 }, + l = + ((s = a.MDCFoundation), + r(d, s), + Object.defineProperty(d, "strings", { + get: function() { + return u.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d, "cssClasses", { + get: function() { + return u.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + addClassToLeadingIcon: function() {}, + eventTargetHasClass: function() { + return !1; + }, + focusPrimaryAction: function() {}, + focusTrailingAction: function() {}, + getAttribute: function() { + return null; + }, + getCheckmarkBoundingClientRect: function() { + return c; + }, + getComputedStyleValue: function() { + return ""; + }, + getRootBoundingClientRect: function() { + return c; + }, + hasClass: function() { + return !1; + }, + hasLeadingIcon: function() { + return !1; + }, + hasTrailingAction: function() { + return !1; + }, + isRTL: function() { + return !1; + }, + notifyInteraction: function() {}, + notifyNavigation: function() {}, + notifyRemoval: function() {}, + notifySelection: function() {}, + notifyTrailingIconInteraction: function() {}, + removeClass: function() {}, + removeClassFromLeadingIcon: function() {}, + setPrimaryActionAttr: function() {}, + setStyleProperty: function() {}, + setTrailingActionAttr: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (d.prototype.isSelected = function() { + return this.adapter_.hasClass(u.cssClasses.SELECTED); + }), + (d.prototype.setSelected = function(t) { + this.setSelected_(t), this.notifySelection_(t); + }), + (d.prototype.setSelectedFromChipSet = function(t, e) { + this.setSelected_(t), e && this.notifyIgnoredSelection_(t); + }), + (d.prototype.getShouldRemoveOnTrailingIconClick = function() { + return this.shouldRemoveOnTrailingIconClick_; + }), + (d.prototype.setShouldRemoveOnTrailingIconClick = function(t) { + this.shouldRemoveOnTrailingIconClick_ = t; + }), + (d.prototype.getDimensions = function() { + function t() { + return e.adapter_.getRootBoundingClientRect(); + } + var e = this; + if (!this.adapter_.hasLeadingIcon()) { + var n = e.adapter_.getCheckmarkBoundingClientRect(); + if (n) { + var i = t(); + return { + bottom: i.bottom, + height: i.height, + left: i.left, + right: i.right, + top: i.top, + width: i.width + n.height + }; + } + } + return t(); + }), + (d.prototype.beginExit = function() { + this.adapter_.addClass(u.cssClasses.CHIP_EXIT); + }), + (d.prototype.handleInteraction = function(t) { + this.shouldHandleInteraction_(t) && + (this.adapter_.notifyInteraction(), this.focusPrimaryAction_()); + }), + (d.prototype.handleTransitionEnd = function(t) { + var e = this, + n = this.adapter_.eventTargetHasClass( + t.target, + u.cssClasses.CHIP_EXIT + ), + i = "width" === t.propertyName, + r = "opacity" === t.propertyName; + if (n && r) { + var o = this.adapter_.getComputedStyleValue("width"); + requestAnimationFrame(function() { + e.adapter_.setStyleProperty("width", o), + e.adapter_.setStyleProperty("padding", "0"), + e.adapter_.setStyleProperty("margin", "0"), + requestAnimationFrame(function() { + e.adapter_.setStyleProperty("width", "0"); + }); + }); + } else { + if (n && i) { + this.removeFocus_(); + var s = this.adapter_.getAttribute( + u.strings.REMOVED_ANNOUNCEMENT_ATTRIBUTE + ); + this.adapter_.notifyRemoval(s); + } + if (r) { + var a = + this.adapter_.eventTargetHasClass( + t.target, + u.cssClasses.LEADING_ICON + ) && this.adapter_.hasClass(u.cssClasses.SELECTED), + c = + this.adapter_.eventTargetHasClass( + t.target, + u.cssClasses.CHECKMARK + ) && !this.adapter_.hasClass(u.cssClasses.SELECTED); + return a + ? this.adapter_.addClassToLeadingIcon( + u.cssClasses.HIDDEN_LEADING_ICON + ) + : c + ? this.adapter_.removeClassFromLeadingIcon( + u.cssClasses.HIDDEN_LEADING_ICON + ) + : void 0; + } + } + }), + (d.prototype.handleFocusIn = function(t) { + this.eventFromPrimaryAction_(t) && + this.adapter_.addClass(u.cssClasses.PRIMARY_ACTION_FOCUSED); + }), + (d.prototype.handleFocusOut = function(t) { + this.eventFromPrimaryAction_(t) && + this.adapter_.removeClass(u.cssClasses.PRIMARY_ACTION_FOCUSED); + }), + (d.prototype.handleTrailingIconInteraction = function(t) { + this.shouldHandleInteraction_(t) && + (this.adapter_.notifyTrailingIconInteraction(), + this.removeChip_(t)); + }), + (d.prototype.handleKeydown = function(t) { + if (this.shouldRemoveChip_(t)) return this.removeChip_(t); + var e = t.key; + u.navigationKeys.has(e) && + (t.preventDefault(), this.focusNextAction_(t)); + }), + (d.prototype.removeFocus = function() { + this.adapter_.setPrimaryActionAttr(u.strings.TAB_INDEX, "-1"), + this.adapter_.setTrailingActionAttr(u.strings.TAB_INDEX, "-1"); + }), + (d.prototype.focusPrimaryAction = function() { + this.focusPrimaryAction_(); + }), + (d.prototype.focusTrailingAction = function() { + if (!this.adapter_.hasTrailingAction()) + return this.focusPrimaryAction_(); + this.focusTrailingAction_(); + }), + (d.prototype.focusNextAction_ = function(t) { + var e = t.key, + n = this.adapter_.hasTrailingAction(), + i = this.getDirection_(e), + r = this.getEvtSource_(t); + if (!u.jumpChipKeys.has(e) && n) + return r === u.EventSource.PRIMARY && i === u.Direction.RIGHT + ? this.focusTrailingAction_() + : r === u.EventSource.TRAILING && i === u.Direction.LEFT + ? this.focusPrimaryAction_() + : void this.adapter_.notifyNavigation(e, u.EventSource.NONE); + this.adapter_.notifyNavigation(e, r); + }), + (d.prototype.getEvtSource_ = function(t) { + return this.adapter_.eventTargetHasClass( + t.target, + u.cssClasses.PRIMARY_ACTION + ) + ? u.EventSource.PRIMARY + : this.adapter_.eventTargetHasClass( + t.target, + u.cssClasses.TRAILING_ACTION + ) + ? u.EventSource.TRAILING + : u.EventSource.NONE; + }), + (d.prototype.getDirection_ = function(t) { + var e = this.adapter_.isRTL(), + n = + t === u.strings.ARROW_LEFT_KEY || + t === u.strings.IE_ARROW_LEFT_KEY, + i = + t === u.strings.ARROW_RIGHT_KEY || + t === u.strings.IE_ARROW_RIGHT_KEY; + return (!e && n) || (e && i) + ? u.Direction.LEFT + : u.Direction.RIGHT; + }), + (d.prototype.focusPrimaryAction_ = function() { + this.adapter_.setPrimaryActionAttr(u.strings.TAB_INDEX, "0"), + this.adapter_.focusPrimaryAction(), + this.adapter_.setTrailingActionAttr(u.strings.TAB_INDEX, "-1"); + }), + (d.prototype.focusTrailingAction_ = function() { + this.adapter_.setTrailingActionAttr(u.strings.TAB_INDEX, "0"), + this.adapter_.focusTrailingAction(), + this.adapter_.setPrimaryActionAttr(u.strings.TAB_INDEX, "-1"); + }), + (d.prototype.removeFocus_ = function() { + this.adapter_.setTrailingActionAttr(u.strings.TAB_INDEX, "-1"), + this.adapter_.setPrimaryActionAttr(u.strings.TAB_INDEX, "-1"); + }), + (d.prototype.removeChip_ = function(t) { + t.stopPropagation(), + t.preventDefault(), + this.shouldRemoveOnTrailingIconClick_ && this.beginExit(); + }), + (d.prototype.shouldHandleInteraction_ = function(t) { + if ("click" === t.type) return !0; + var e = t; + return ( + e.key === u.strings.ENTER_KEY || + e.key === u.strings.SPACEBAR_KEY + ); + }), + (d.prototype.shouldRemoveChip_ = function(t) { + return ( + this.adapter_.hasClass(u.cssClasses.DELETABLE) && + (t.key === u.strings.BACKSPACE_KEY || + t.key === u.strings.DELETE_KEY || + t.key === u.strings.IE_DELETE_KEY) + ); + }), + (d.prototype.setSelected_ = function(t) { + t + ? (this.adapter_.addClass(u.cssClasses.SELECTED), + this.adapter_.setPrimaryActionAttr( + u.strings.ARIA_CHECKED, + "true" + )) + : (this.adapter_.removeClass(u.cssClasses.SELECTED), + this.adapter_.setPrimaryActionAttr( + u.strings.ARIA_CHECKED, + "false" + )); + }), + (d.prototype.notifySelection_ = function(t) { + this.adapter_.notifySelection(t, !1); + }), + (d.prototype.notifyIgnoredSelection_ = function(t) { + this.adapter_.notifySelection(t, !0); + }), + (d.prototype.eventFromPrimaryAction_ = function(t) { + return this.adapter_.eventTargetHasClass( + t.target, + u.cssClasses.PRIMARY_ACTION + ); + }), + d); + function d(t) { + var e = s.call(this, o(o({}, d.defaultAdapter), t)) || this; + return (e.shouldRemoveOnTrailingIconClick_ = !0), e; + } + (e.MDCChipFoundation = l), (e.default = l); + }, + function(t, e, n) { + "use strict"; + var i; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.cssClasses = { + CELL: "mdc-data-table__cell", + CELL_NUMERIC: "mdc-data-table__cell--numeric", + CONTENT: "mdc-data-table__content", + HEADER_CELL_SORTED: "mdc-data-table__header-cell--sorted", + HEADER_CELL_SORTED_DESCENDING: + "mdc-data-table__header-cell--sorted-descending", + HEADER_CELL_WITH_SORT: "mdc-data-table__header-cell--with-sort", + HEADER_ROW: "mdc-data-table__header-row", + HEADER_ROW_CHECKBOX: "mdc-data-table__header-row-checkbox", + IN_PROGRESS: "mdc-data-table--in-progress", + ROOT: "mdc-data-table", + ROW: "mdc-data-table__row", + ROW_CHECKBOX: "mdc-data-table__row-checkbox", + ROW_SELECTED: "mdc-data-table__row--selected", + SORT_ICON_BUTTON: "mdc-data-table__sort-icon-button" + }), + (e.dataAttributes = { + ROW_ID: "data-row-id", + COLUMND_ID: "data-columnd-id" + }), + (e.strings = { + ARIA_SELECTED: "aria-selected", + ARIA_SORT: "aria-sort", + DATA_ROW_ID_ATTR: e.dataAttributes.ROW_ID, + HEADER_ROW_CHECKBOX_SELECTOR: + "." + e.cssClasses.HEADER_ROW_CHECKBOX, + ROW_CHECKBOX_SELECTOR: "." + e.cssClasses.ROW_CHECKBOX, + ROW_SELECTED_SELECTOR: "." + e.cssClasses.ROW_SELECTED, + ROW_SELECTOR: "." + e.cssClasses.ROW + }), + ((i = e.SortValue || (e.SortValue = {})).ASCENDING = "ascending"), + (i.DESCENDING = "descending"), + (i.NONE = "none"), + (i.OTHER = "other"), + (e.events = { + ROW_SELECTION_CHANGED: "MDCDataTable:rowSelectionChanged", + SELECTED_ALL: "MDCDataTable:selectedAll", + UNSELECTED_ALL: "MDCDataTable:unselectedAll", + SORTED: "MDCDataTable:sorted" + }); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + var o = "mdc-dom-focus-sentinel", + i = + ((r.prototype.trapFocus = function() { + var t = this.getFocusableElements(this.root); + if (0 === t.length) + throw new Error( + "FocusTrap: Element must have at least one focusable child." + ); + (this.elFocusedBeforeTrapFocus = + document.activeElement instanceof HTMLElement + ? document.activeElement + : null), + this.wrapTabFocus(this.root, t), + this.options.skipInitialFocus || + this.focusInitialElement(t, this.options.initialFocusEl); + }), + (r.prototype.releaseFocus = function() { + [].slice + .call(this.root.querySelectorAll("." + o)) + .forEach(function(t) { + t.parentElement.removeChild(t); + }), + this.elFocusedBeforeTrapFocus && + this.elFocusedBeforeTrapFocus.focus(); + }), + (r.prototype.wrapTabFocus = function(t, e) { + var n = this.createSentinel(), + i = this.createSentinel(); + n.addEventListener("focus", function() { + 0 < e.length && e[e.length - 1].focus(); + }), + i.addEventListener("focus", function() { + 0 < e.length && e[0].focus(); + }), + t.insertBefore(n, t.children[0]), + t.appendChild(i); + }), + (r.prototype.focusInitialElement = function(t, e) { + var n = 0; + e && (n = Math.max(t.indexOf(e), 0)), t[n].focus(); + }), + (r.prototype.getFocusableElements = function(t) { + return [].slice + .call( + t.querySelectorAll( + "[autofocus], [tabindex], a, input, textarea, select, button" + ) + ) + .filter(function(t) { + var e = + "true" === t.getAttribute("aria-disabled") || + null != t.getAttribute("disabled") || + null != t.getAttribute("hidden") || + "true" === t.getAttribute("aria-hidden"), + n = + 0 <= t.tabIndex && + 0 < t.getBoundingClientRect().width && + !t.classList.contains(o) && + !e, + i = !1; + if (n) { + var r = getComputedStyle(t); + i = "none" === r.display || "hidden" === r.visibility; + } + return n && !i; + }); + }), + (r.prototype.createSentinel = function() { + var t = document.createElement("div"); + return ( + t.setAttribute("tabindex", "0"), + t.setAttribute("aria-hidden", "true"), + t.classList.add(o), + t + ); + }), + r); + function r(t, e) { + void 0 === e && (e = {}), + (this.root = t), + (this.options = e), + (this.elFocusedBeforeTrapFocus = null); + } + e.FocusTrap = i; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(2), + c = n(9), + u = n(10), + l = + ((o = s.MDCComponent), + r(d, o), + Object.defineProperty(d.prototype, "vertical", { + set: function(t) { + this.foundation_.setVerticalOrientation(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d.prototype, "listElements", { + get: function() { + return [].slice.call( + this.root_.querySelectorAll( + "." + c.cssClasses.LIST_ITEM_CLASS + ) + ); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d.prototype, "wrapFocus", { + set: function(t) { + this.foundation_.setWrapFocus(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d.prototype, "singleSelection", { + set: function(t) { + this.foundation_.setSingleSelection(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d.prototype, "selectedIndex", { + get: function() { + return this.foundation_.getSelectedIndex(); + }, + set: function(t) { + this.foundation_.setSelectedIndex(t); + }, + enumerable: !0, + configurable: !0 + }), + (d.attachTo = function(t) { + return new d(t); + }), + (d.prototype.initialSyncWithDOM = function() { + (this.handleClick_ = this.handleClickEvent_.bind(this)), + (this.handleKeydown_ = this.handleKeydownEvent_.bind(this)), + (this.focusInEventListener_ = this.handleFocusInEvent_.bind( + this + )), + (this.focusOutEventListener_ = this.handleFocusOutEvent_.bind( + this + )), + this.listen("keydown", this.handleKeydown_), + this.listen("click", this.handleClick_), + this.listen("focusin", this.focusInEventListener_), + this.listen("focusout", this.focusOutEventListener_), + this.layout(), + this.initializeListType(); + }), + (d.prototype.destroy = function() { + this.unlisten("keydown", this.handleKeydown_), + this.unlisten("click", this.handleClick_), + this.unlisten("focusin", this.focusInEventListener_), + this.unlisten("focusout", this.focusOutEventListener_); + }), + (d.prototype.layout = function() { + var t = this.root_.getAttribute(c.strings.ARIA_ORIENTATION); + (this.vertical = t !== c.strings.ARIA_ORIENTATION_HORIZONTAL), + [].slice + .call( + this.root_.querySelectorAll( + ".mdc-list-item:not([tabindex])" + ) + ) + .forEach(function(t) { + t.setAttribute("tabindex", "-1"); + }), + [].slice + .call( + this.root_.querySelectorAll( + c.strings.FOCUSABLE_CHILD_ELEMENTS + ) + ) + .forEach(function(t) { + return t.setAttribute("tabindex", "-1"); + }), + this.foundation_.layout(); + }), + (d.prototype.initializeListType = function() { + var e = this, + t = this.root_.querySelectorAll( + c.strings.ARIA_ROLE_CHECKBOX_SELECTOR + ), + n = this.root_.querySelector( + "\n ." + + c.cssClasses.LIST_ITEM_ACTIVATED_CLASS + + ",\n ." + + c.cssClasses.LIST_ITEM_SELECTED_CLASS + + "\n " + ), + i = this.root_.querySelector( + c.strings.ARIA_CHECKED_RADIO_SELECTOR + ); + if (t.length) { + var r = this.root_.querySelectorAll( + c.strings.ARIA_CHECKED_CHECKBOX_SELECTOR + ); + this.selectedIndex = [].map.call(r, function(t) { + return e.listElements.indexOf(t); + }); + } else + n + ? (n.classList.contains( + c.cssClasses.LIST_ITEM_ACTIVATED_CLASS + ) && this.foundation_.setUseActivatedClass(!0), + (this.singleSelection = !0), + (this.selectedIndex = this.listElements.indexOf(n))) + : i && (this.selectedIndex = this.listElements.indexOf(i)); + }), + (d.prototype.setEnabled = function(t, e) { + this.foundation_.setEnabled(t, e); + }), + (d.prototype.getDefaultFoundation = function() { + var r = this, + t = { + addClassForElementIndex: function(t, e) { + var n = r.listElements[t]; + n && n.classList.add(e); + }, + focusItemAtIndex: function(t) { + var e = r.listElements[t]; + e && e.focus(); + }, + getAttributeForElementIndex: function(t, e) { + return r.listElements[t].getAttribute(e); + }, + getFocusedElementIndex: function() { + return r.listElements.indexOf(document.activeElement); + }, + getListItemCount: function() { + return r.listElements.length; + }, + hasCheckboxAtIndex: function(t) { + return !!r.listElements[t].querySelector( + c.strings.CHECKBOX_SELECTOR + ); + }, + hasRadioAtIndex: function(t) { + return !!r.listElements[t].querySelector( + c.strings.RADIO_SELECTOR + ); + }, + isCheckboxCheckedAtIndex: function(t) { + return r.listElements[t].querySelector( + c.strings.CHECKBOX_SELECTOR + ).checked; + }, + isFocusInsideList: function() { + return r.root_.contains(document.activeElement); + }, + isRootFocused: function() { + return document.activeElement === r.root_; + }, + listItemAtIndexHasClass: function(t, e) { + return r.listElements[t].classList.contains(e); + }, + notifyAction: function(t) { + r.emit(c.strings.ACTION_EVENT, { index: t }, !0); + }, + removeClassForElementIndex: function(t, e) { + var n = r.listElements[t]; + n && n.classList.remove(e); + }, + setAttributeForElementIndex: function(t, e, n) { + var i = r.listElements[t]; + i && i.setAttribute(e, n); + }, + setCheckedCheckboxOrRadioAtIndex: function(t, e) { + var n = r.listElements[t].querySelector( + c.strings.CHECKBOX_RADIO_SELECTOR + ); + n.checked = e; + var i = document.createEvent("Event"); + i.initEvent("change", !0, !0), n.dispatchEvent(i); + }, + setTabIndexForListItemChildren: function(t, e) { + var n = r.listElements[t]; + [].slice + .call( + n.querySelectorAll( + c.strings.CHILD_ELEMENTS_TO_TOGGLE_TABINDEX + ) + ) + .forEach(function(t) { + return t.setAttribute("tabindex", e); + }); + } + }; + return new u.MDCListFoundation(t); + }), + (d.prototype.getListItemIndex_ = function(t) { + var e = t.target, + n = a.closest( + e, + "." + c.cssClasses.LIST_ITEM_CLASS + ", ." + c.cssClasses.ROOT + ); + return n && a.matches(n, "." + c.cssClasses.LIST_ITEM_CLASS) + ? this.listElements.indexOf(n) + : -1; + }), + (d.prototype.handleFocusInEvent_ = function(t) { + var e = this.getListItemIndex_(t); + this.foundation_.handleFocusIn(t, e); + }), + (d.prototype.handleFocusOutEvent_ = function(t) { + var e = this.getListItemIndex_(t); + this.foundation_.handleFocusOut(t, e); + }), + (d.prototype.handleKeydownEvent_ = function(t) { + var e = this.getListItemIndex_(t), + n = t.target; + this.foundation_.handleKeydown( + t, + n.classList.contains(c.cssClasses.LIST_ITEM_CLASS), + e + ); + }), + (d.prototype.handleClickEvent_ = function(t) { + var e = this.getListItemIndex_(t), + n = t.target, + i = !a.matches(n, c.strings.CHECKBOX_RADIO_SELECTOR); + this.foundation_.handleClick(e, i); + }), + d); + function d() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCList = l; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(54), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + hasClass: function() { + return !1; + }, + elementHasClass: function() { + return !1; + }, + notifyClose: function() {}, + notifyOpen: function() {}, + saveFocus: function() {}, + restoreFocus: function() {}, + focusActiveNavigationItem: function() {}, + trapFocus: function() {}, + releaseFocus: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.destroy = function() { + this.animationFrame_ && + cancelAnimationFrame(this.animationFrame_), + this.animationTimer_ && clearTimeout(this.animationTimer_); + }), + (l.prototype.open = function() { + var t = this; + this.isOpen() || + this.isOpening() || + this.isClosing() || + (this.adapter_.addClass(c.cssClasses.OPEN), + this.adapter_.addClass(c.cssClasses.ANIMATE), + this.runNextAnimationFrame_(function() { + t.adapter_.addClass(c.cssClasses.OPENING); + }), + this.adapter_.saveFocus()); + }), + (l.prototype.close = function() { + !this.isOpen() || + this.isOpening() || + this.isClosing() || + this.adapter_.addClass(c.cssClasses.CLOSING); + }), + (l.prototype.isOpen = function() { + return this.adapter_.hasClass(c.cssClasses.OPEN); + }), + (l.prototype.isOpening = function() { + return ( + this.adapter_.hasClass(c.cssClasses.OPENING) || + this.adapter_.hasClass(c.cssClasses.ANIMATE) + ); + }), + (l.prototype.isClosing = function() { + return this.adapter_.hasClass(c.cssClasses.CLOSING); + }), + (l.prototype.handleKeydown = function(t) { + var e = t.keyCode; + ("Escape" !== t.key && 27 !== e) || this.close(); + }), + (l.prototype.handleTransitionEnd = function(t) { + var e = c.cssClasses.OPENING, + n = c.cssClasses.CLOSING, + i = c.cssClasses.OPEN, + r = c.cssClasses.ANIMATE, + o = c.cssClasses.ROOT; + this.isElement_(t.target) && + this.adapter_.elementHasClass(t.target, o) && + (this.isClosing() + ? (this.adapter_.removeClass(i), + this.closed_(), + this.adapter_.restoreFocus(), + this.adapter_.notifyClose()) + : (this.adapter_.focusActiveNavigationItem(), + this.opened_(), + this.adapter_.notifyOpen()), + this.adapter_.removeClass(r), + this.adapter_.removeClass(e), + this.adapter_.removeClass(n)); + }), + (l.prototype.opened_ = function() {}), + (l.prototype.closed_ = function() {}), + (l.prototype.runNextAnimationFrame_ = function(t) { + var e = this; + cancelAnimationFrame(this.animationFrame_), + (this.animationFrame_ = requestAnimationFrame(function() { + (e.animationFrame_ = 0), + clearTimeout(e.animationTimer_), + (e.animationTimer_ = setTimeout(t, 0)); + })); + }), + (l.prototype.isElement_ = function(t) { + return Boolean(t.classList); + }), + l); + function l(t) { + var e = s.call(this, o(o({}, l.defaultAdapter), t)) || this; + return (e.animationFrame_ = 0), (e.animationTimer_ = 0), e; + } + (e.MDCDismissibleDrawerFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(2), + c = n(25), + u = + ((o = s.MDCComponent), + r(l, o), + (l.attachTo = function(t) { + return new l(t); + }), + (l.prototype.shake = function(t) { + this.foundation_.shake(t); + }), + (l.prototype.float = function(t) { + this.foundation_.float(t); + }), + (l.prototype.getWidth = function() { + return this.foundation_.getWidth(); + }), + (l.prototype.getDefaultFoundation = function() { + var n = this, + t = { + addClass: function(t) { + return n.root_.classList.add(t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + getWidth: function() { + return a.estimateScrollWidth(n.root_); + }, + registerInteractionHandler: function(t, e) { + return n.listen(t, e); + }, + deregisterInteractionHandler: function(t, e) { + return n.unlisten(t, e); + } + }; + return new c.MDCFloatingLabelFoundation(t); + }), + l); + function l() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCFloatingLabel = u; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(56), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + getWidth: function() { + return 0; + }, + registerInteractionHandler: function() {}, + deregisterInteractionHandler: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.init = function() { + this.adapter_.registerInteractionHandler( + "animationend", + this.shakeAnimationEndHandler_ + ); + }), + (l.prototype.destroy = function() { + this.adapter_.deregisterInteractionHandler( + "animationend", + this.shakeAnimationEndHandler_ + ); + }), + (l.prototype.getWidth = function() { + return this.adapter_.getWidth(); + }), + (l.prototype.shake = function(t) { + var e = l.cssClasses.LABEL_SHAKE; + t ? this.adapter_.addClass(e) : this.adapter_.removeClass(e); + }), + (l.prototype.float = function(t) { + var e = l.cssClasses, + n = e.LABEL_FLOAT_ABOVE, + i = e.LABEL_SHAKE; + t + ? this.adapter_.addClass(n) + : (this.adapter_.removeClass(n), this.adapter_.removeClass(i)); + }), + (l.prototype.handleShakeAnimationEnd_ = function() { + var t = l.cssClasses.LABEL_SHAKE; + this.adapter_.removeClass(t); + }), + l); + function l(t) { + var e = s.call(this, o(o({}, l.defaultAdapter), t)) || this; + return ( + (e.shakeAnimationEndHandler_ = function() { + return e.handleShakeAnimationEnd_(); + }), + e + ); + } + (e.MDCFloatingLabelFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(61), + c = + ((o = s.MDCComponent), + r(u, o), + (u.attachTo = function(t) { + return new u(t); + }), + (u.prototype.activate = function() { + this.foundation_.activate(); + }), + (u.prototype.deactivate = function() { + this.foundation_.deactivate(); + }), + (u.prototype.setRippleCenter = function(t) { + this.foundation_.setRippleCenter(t); + }), + (u.prototype.getDefaultFoundation = function() { + var n = this, + t = { + addClass: function(t) { + return n.root_.classList.add(t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + hasClass: function(t) { + return n.root_.classList.contains(t); + }, + setStyle: function(t, e) { + return n.root_.style.setProperty(t, e); + }, + registerEventHandler: function(t, e) { + return n.listen(t, e); + }, + deregisterEventHandler: function(t, e) { + return n.unlisten(t, e); + } + }; + return new a.MDCLineRippleFoundation(t); + }), + u); + function u() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCLineRipple = c; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(25), + c = n(28), + u = n(69), + l = + ((o = s.MDCComponent), + r(d, o), + (d.attachTo = function(t) { + return new d(t); + }), + (d.prototype.initialSyncWithDOM = function() { + this.notchElement_ = this.root_.querySelector( + c.strings.NOTCH_ELEMENT_SELECTOR + ); + var t = this.root_.querySelector( + "." + a.MDCFloatingLabelFoundation.cssClasses.ROOT + ); + t + ? ((t.style.transitionDuration = "0s"), + this.root_.classList.add(c.cssClasses.OUTLINE_UPGRADED), + requestAnimationFrame(function() { + t.style.transitionDuration = ""; + })) + : this.root_.classList.add(c.cssClasses.NO_LABEL); + }), + (d.prototype.notch = function(t) { + this.foundation_.notch(t); + }), + (d.prototype.closeNotch = function() { + this.foundation_.closeNotch(); + }), + (d.prototype.getDefaultFoundation = function() { + var e = this, + t = { + addClass: function(t) { + return e.root_.classList.add(t); + }, + removeClass: function(t) { + return e.root_.classList.remove(t); + }, + setNotchWidthProperty: function(t) { + return e.notchElement_.style.setProperty("width", t + "px"); + }, + removeNotchWidthProperty: function() { + return e.notchElement_.style.removeProperty("width"); + } + }; + return new u.MDCNotchedOutlineFoundation(t); + }), + d); + function d() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCNotchedOutline = l; + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.strings = { NOTCH_ELEMENT_SELECTOR: ".mdc-notched-outline__notch" }; + e.numbers = { NOTCH_ELEMENT_PADDING: 8 }; + e.cssClasses = { + NO_LABEL: "mdc-notched-outline--no-label", + OUTLINE_NOTCHED: "mdc-notched-outline--notched", + OUTLINE_UPGRADED: "mdc-notched-outline--upgraded" + }; + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + var i = { + ACTIVATED: "mdc-select--activated", + DISABLED: "mdc-select--disabled", + FOCUSED: "mdc-select--focused", + INVALID: "mdc-select--invalid", + OUTLINED: "mdc-select--outlined", + REQUIRED: "mdc-select--required", + ROOT: "mdc-select", + SELECTED_ITEM_CLASS: "mdc-list-item--selected", + WITH_LEADING_ICON: "mdc-select--with-leading-icon" + }, + r = { + ARIA_CONTROLS: "aria-controls", + ARIA_SELECTED_ATTR: "aria-selected", + CHANGE_EVENT: "MDCSelect:change", + LABEL_SELECTOR: ".mdc-floating-label", + LEADING_ICON_SELECTOR: ".mdc-select__icon", + LINE_RIPPLE_SELECTOR: ".mdc-line-ripple", + MENU_SELECTOR: ".mdc-select__menu", + OUTLINE_SELECTOR: ".mdc-notched-outline", + SELECTED_ITEM_SELECTOR: + "." + (e.cssClasses = i).SELECTED_ITEM_CLASS, + SELECTED_TEXT_SELECTOR: ".mdc-select__selected-text", + SELECT_ANCHOR_SELECTOR: ".mdc-select__anchor", + VALUE_ATTR: "data-value" + }; + e.strings = r; + e.numbers = { LABEL_SCALE: 0.75, UNSET_INDEX: -1 }; + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.cssClasses = { + ACTIVE: "mdc-slider--active", + DISABLED: "mdc-slider--disabled", + DISCRETE: "mdc-slider--discrete", + FOCUS: "mdc-slider--focus", + HAS_TRACK_MARKER: "mdc-slider--display-markers", + IN_TRANSIT: "mdc-slider--in-transit", + IS_DISCRETE: "mdc-slider--discrete" + }; + e.strings = { + ARIA_DISABLED: "aria-disabled", + ARIA_VALUEMAX: "aria-valuemax", + ARIA_VALUEMIN: "aria-valuemin", + ARIA_VALUENOW: "aria-valuenow", + CHANGE_EVENT: "MDCSlider:change", + INPUT_EVENT: "MDCSlider:input", + PIN_VALUE_MARKER_SELECTOR: ".mdc-slider__pin-value-marker", + STEP_DATA_ATTR: "data-step", + THUMB_CONTAINER_SELECTOR: ".mdc-slider__thumb-container", + TRACK_MARKER_CONTAINER_SELECTOR: + ".mdc-slider__track-marker-container", + TRACK_SELECTOR: ".mdc-slider__track" + }; + e.numbers = { PAGE_FACTOR: 4 }; + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.cssClasses = { + ANIMATING: "mdc-tab-scroller--animating", + SCROLL_AREA_SCROLL: "mdc-tab-scroller__scroll-area--scroll", + SCROLL_TEST: "mdc-tab-scroller__test" + }; + e.strings = { + AREA_SELECTOR: ".mdc-tab-scroller__scroll-area", + CONTENT_SELECTOR: ".mdc-tab-scroller__scroll-content" + }; + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + function i(t) { + this.adapter_ = t; + } + (e.MDCTabScrollerRTL = i), (e.default = i); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(92), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + hasClass: function() { + return !1; + }, + setAttr: function() {}, + activateIndicator: function() {}, + deactivateIndicator: function() {}, + notifyInteracted: function() {}, + getOffsetLeft: function() { + return 0; + }, + getOffsetWidth: function() { + return 0; + }, + getContentOffsetLeft: function() { + return 0; + }, + getContentOffsetWidth: function() { + return 0; + }, + focus: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.handleClick = function() { + this.adapter_.notifyInteracted(); + }), + (l.prototype.isActive = function() { + return this.adapter_.hasClass(c.cssClasses.ACTIVE); + }), + (l.prototype.setFocusOnActivate = function(t) { + this.focusOnActivate_ = t; + }), + (l.prototype.activate = function(t) { + this.adapter_.addClass(c.cssClasses.ACTIVE), + this.adapter_.setAttr(c.strings.ARIA_SELECTED, "true"), + this.adapter_.setAttr(c.strings.TABINDEX, "0"), + this.adapter_.activateIndicator(t), + this.focusOnActivate_ && this.adapter_.focus(); + }), + (l.prototype.deactivate = function() { + this.isActive() && + (this.adapter_.removeClass(c.cssClasses.ACTIVE), + this.adapter_.setAttr(c.strings.ARIA_SELECTED, "false"), + this.adapter_.setAttr(c.strings.TABINDEX, "-1"), + this.adapter_.deactivateIndicator()); + }), + (l.prototype.computeDimensions = function() { + var t = this.adapter_.getOffsetWidth(), + e = this.adapter_.getOffsetLeft(), + n = this.adapter_.getContentOffsetWidth(), + i = this.adapter_.getContentOffsetLeft(); + return { + contentLeft: e + i, + contentRight: e + i + n, + rootLeft: e, + rootRight: e + t + }; + }), + l); + function l(t) { + var e = s.call(this, o(o({}, l.defaultAdapter), t)) || this; + return (e.focusOnActivate_ = !0), e; + } + (e.MDCTabFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(96), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { setContent: function() {} }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.setCounterValue = function(t, e) { + (t = Math.min(t, e)), this.adapter_.setContent(t + " / " + e); + }), + l); + function l(t) { + return s.call(this, o(o({}, l.defaultAdapter), t)) || this; + } + (e.MDCTextFieldCharacterCounterFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.strings = { + ARIA_CONTROLS: "aria-controls", + INPUT_SELECTOR: ".mdc-text-field__input", + LABEL_SELECTOR: ".mdc-floating-label", + LEADING_ICON_SELECTOR: ".mdc-text-field__icon--leading", + LINE_RIPPLE_SELECTOR: ".mdc-line-ripple", + OUTLINE_SELECTOR: ".mdc-notched-outline", + PREFIX_SELECTOR: ".mdc-text-field__affix--prefix", + SUFFIX_SELECTOR: ".mdc-text-field__affix--suffix", + TRAILING_ICON_SELECTOR: ".mdc-text-field__icon--trailing" + }; + e.cssClasses = { + DISABLED: "mdc-text-field--disabled", + FOCUSED: "mdc-text-field--focused", + FULLWIDTH: "mdc-text-field--fullwidth", + HELPER_LINE: "mdc-text-field-helper-line", + INVALID: "mdc-text-field--invalid", + LABEL_FLOATING: "mdc-text-field--label-floating", + NO_LABEL: "mdc-text-field--no-label", + OUTLINED: "mdc-text-field--outlined", + ROOT: "mdc-text-field", + TEXTAREA: "mdc-text-field--textarea", + WITH_LEADING_ICON: "mdc-text-field--with-leading-icon", + WITH_TRAILING_ICON: "mdc-text-field--with-trailing-icon" + }; + e.numbers = { LABEL_SCALE: 0.75 }; + e.VALIDATION_ATTR_WHITELIST = [ + "pattern", + "min", + "max", + "required", + "step", + "minlength", + "maxlength" + ]; + e.ALWAYS_FLOAT_TYPES = [ + "color", + "date", + "datetime-local", + "month", + "range", + "time", + "week" + ]; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(99), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + hasClass: function() { + return !1; + }, + setAttr: function() {}, + removeAttr: function() {}, + setContent: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.setContent = function(t) { + this.adapter_.setContent(t); + }), + (l.prototype.setPersistent = function(t) { + t + ? this.adapter_.addClass(c.cssClasses.HELPER_TEXT_PERSISTENT) + : this.adapter_.removeClass( + c.cssClasses.HELPER_TEXT_PERSISTENT + ); + }), + (l.prototype.setValidation = function(t) { + t + ? this.adapter_.addClass( + c.cssClasses.HELPER_TEXT_VALIDATION_MSG + ) + : this.adapter_.removeClass( + c.cssClasses.HELPER_TEXT_VALIDATION_MSG + ); + }), + (l.prototype.showToScreenReader = function() { + this.adapter_.removeAttr(c.strings.ARIA_HIDDEN); + }), + (l.prototype.setValidity = function(t) { + var e = this.adapter_.hasClass( + c.cssClasses.HELPER_TEXT_PERSISTENT + ), + n = + this.adapter_.hasClass( + c.cssClasses.HELPER_TEXT_VALIDATION_MSG + ) && !t; + n + ? this.adapter_.setAttr(c.strings.ROLE, "alert") + : this.adapter_.removeAttr(c.strings.ROLE), + e || n || this.hide_(); + }), + (l.prototype.hide_ = function() { + this.adapter_.setAttr(c.strings.ARIA_HIDDEN, "true"); + }), + l); + function l(t) { + return s.call(this, o(o({}, l.defaultAdapter), t)) || this; + } + (e.MDCTextFieldHelperTextFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(7), + a = n(38), + c = + ((o = a.MDCTopAppBarBaseFoundation), + r(u, o), + (u.prototype.destroy = function() { + o.prototype.destroy.call(this), this.adapter_.setStyle("top", ""); + }), + (u.prototype.handleTargetScroll = function() { + var t = Math.max(this.adapter_.getViewportScrollY(), 0), + e = t - this.lastScrollPosition_; + (this.lastScrollPosition_ = t), + this.isCurrentlyBeingResized_ || + ((this.currentAppBarOffsetTop_ -= e), + 0 < this.currentAppBarOffsetTop_ + ? (this.currentAppBarOffsetTop_ = 0) + : Math.abs(this.currentAppBarOffsetTop_) > + this.topAppBarHeight_ && + (this.currentAppBarOffsetTop_ = -this.topAppBarHeight_), + this.moveTopAppBar_()); + }), + (u.prototype.handleWindowResize = function() { + var t = this; + this.resizeThrottleId_ || + (this.resizeThrottleId_ = setTimeout(function() { + (t.resizeThrottleId_ = 0), t.throttledResizeHandler_(); + }, s.numbers.DEBOUNCE_THROTTLE_RESIZE_TIME_MS)), + (this.isCurrentlyBeingResized_ = !0), + this.resizeDebounceId_ && clearTimeout(this.resizeDebounceId_), + (this.resizeDebounceId_ = setTimeout(function() { + t.handleTargetScroll(), + (t.isCurrentlyBeingResized_ = !1), + (t.resizeDebounceId_ = 0); + }, s.numbers.DEBOUNCE_THROTTLE_RESIZE_TIME_MS)); + }), + (u.prototype.checkForUpdate_ = function() { + var t = -this.topAppBarHeight_, + e = this.currentAppBarOffsetTop_ < 0, + n = this.currentAppBarOffsetTop_ > t, + i = e && n; + if (i) this.wasDocked_ = !1; + else { + if (!this.wasDocked_) return (this.wasDocked_ = !0); + if (this.isDockedShowing_ !== n) + return (this.isDockedShowing_ = n), !0; + } + return i; + }), + (u.prototype.moveTopAppBar_ = function() { + if (this.checkForUpdate_()) { + var t = this.currentAppBarOffsetTop_; + Math.abs(t) >= this.topAppBarHeight_ && + (t = -s.numbers.MAX_TOP_APP_BAR_HEIGHT), + this.adapter_.setStyle("top", t + "px"); + } + }), + (u.prototype.throttledResizeHandler_ = function() { + var t = this.adapter_.getTopAppBarHeight(); + this.topAppBarHeight_ !== t && + ((this.wasDocked_ = !1), + (this.currentAppBarOffsetTop_ -= this.topAppBarHeight_ - t), + (this.topAppBarHeight_ = t)), + this.handleTargetScroll(); + }), + u); + function u(t) { + var e = o.call(this, t) || this; + return ( + (e.wasDocked_ = !0), + (e.isDockedShowing_ = !0), + (e.currentAppBarOffsetTop_ = 0), + (e.isCurrentlyBeingResized_ = !1), + (e.resizeThrottleId_ = 0), + (e.resizeDebounceId_ = 0), + (e.lastScrollPosition_ = e.adapter_.getViewportScrollY()), + (e.topAppBarHeight_ = e.adapter_.getTopAppBarHeight()), + e + ); + } + (e.MDCTopAppBarFoundation = c), (e.default = c); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(7), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "numbers", { + get: function() { + return c.numbers; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + hasClass: function() { + return !1; + }, + setStyle: function() {}, + getTopAppBarHeight: function() { + return 0; + }, + notifyNavigationIconClicked: function() {}, + getViewportScrollY: function() { + return 0; + }, + getTotalActionItems: function() { + return 0; + } + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.handleTargetScroll = function() {}), + (l.prototype.handleWindowResize = function() {}), + (l.prototype.handleNavigationClick = function() { + this.adapter_.notifyNavigationIconClicked(); + }), + l); + function l(t) { + return s.call(this, o(o({}, l.defaultAdapter), t)) || this; + } + (e.MDCTopAppBarBaseFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(15), + c = n(1), + u = n(5), + l = n(2), + d = n(3), + p = n(4), + _ = n(17), + f = n(41), + h = ["checked", "indeterminate"], + C = + ((s = c.MDCComponent), + r(y, s), + (y.attachTo = function(t) { + return new y(t); + }), + Object.defineProperty(y.prototype, "ripple", { + get: function() { + return this.ripple_; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(y.prototype, "checked", { + get: function() { + return this.nativeControl_.checked; + }, + set: function(t) { + this.nativeControl_.checked = t; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(y.prototype, "indeterminate", { + get: function() { + return this.nativeControl_.indeterminate; + }, + set: function(t) { + this.nativeControl_.indeterminate = t; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(y.prototype, "disabled", { + get: function() { + return this.nativeControl_.disabled; + }, + set: function(t) { + this.foundation_.setDisabled(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(y.prototype, "value", { + get: function() { + return this.nativeControl_.value; + }, + set: function(t) { + this.nativeControl_.value = t; + }, + enumerable: !0, + configurable: !0 + }), + (y.prototype.initialize = function() { + var t = _.strings.DATA_INDETERMINATE_ATTR; + (this.nativeControl_.indeterminate = + "true" === this.nativeControl_.getAttribute(t)), + this.nativeControl_.removeAttribute(t); + }), + (y.prototype.initialSyncWithDOM = function() { + var t = this; + (this.handleChange_ = function() { + return t.foundation_.handleChange(); + }), + (this.handleAnimationEnd_ = function() { + return t.foundation_.handleAnimationEnd(); + }), + this.nativeControl_.addEventListener( + "change", + this.handleChange_ + ), + this.listen( + a.getCorrectEventName(window, "animationend"), + this.handleAnimationEnd_ + ), + this.installPropertyChangeHooks_(); + }), + (y.prototype.destroy = function() { + this.ripple_.destroy(), + this.nativeControl_.removeEventListener( + "change", + this.handleChange_ + ), + this.unlisten( + a.getCorrectEventName(window, "animationend"), + this.handleAnimationEnd_ + ), + this.uninstallPropertyChangeHooks_(), + s.prototype.destroy.call(this); + }), + (y.prototype.getDefaultFoundation = function() { + var n = this, + t = { + addClass: function(t) { + return n.root_.classList.add(t); + }, + forceLayout: function() { + return n.root_.offsetWidth; + }, + hasNativeControl: function() { + return !!n.nativeControl_; + }, + isAttachedToDOM: function() { + return Boolean(n.root_.parentNode); + }, + isChecked: function() { + return n.checked; + }, + isIndeterminate: function() { + return n.indeterminate; + }, + removeClass: function(t) { + n.root_.classList.remove(t); + }, + removeNativeControlAttr: function(t) { + n.nativeControl_.removeAttribute(t); + }, + setNativeControlAttr: function(t, e) { + n.nativeControl_.setAttribute(t, e); + }, + setNativeControlDisabled: function(t) { + n.nativeControl_.disabled = t; + } + }; + return new f.MDCCheckboxFoundation(t); + }), + (y.prototype.createRipple_ = function() { + var n = this, + t = o(o({}, d.MDCRipple.createAdapter(this)), { + deregisterInteractionHandler: function(t, e) { + return n.nativeControl_.removeEventListener( + t, + e, + u.applyPassive() + ); + }, + isSurfaceActive: function() { + return l.matches(n.nativeControl_, ":active"); + }, + isUnbounded: function() { + return !0; + }, + registerInteractionHandler: function(t, e) { + return n.nativeControl_.addEventListener( + t, + e, + u.applyPassive() + ); + } + }); + return new d.MDCRipple(this.root_, new p.MDCRippleFoundation(t)); + }), + (y.prototype.installPropertyChangeHooks_ = function() { + var r = this, + o = this.nativeControl_, + s = Object.getPrototypeOf(o); + h.forEach(function(t) { + var e = Object.getOwnPropertyDescriptor(s, t); + if (E(e)) { + var n = e.get, + i = { + configurable: e.configurable, + enumerable: e.enumerable, + get: n, + set: function(t) { + e.set.call(o, t), r.foundation_.handleChange(); + } + }; + Object.defineProperty(o, t, i); + } + }); + }), + (y.prototype.uninstallPropertyChangeHooks_ = function() { + var n = this.nativeControl_, + i = Object.getPrototypeOf(n); + h.forEach(function(t) { + var e = Object.getOwnPropertyDescriptor(i, t); + E(e) && Object.defineProperty(n, t, e); + }); + }), + Object.defineProperty(y.prototype, "nativeControl_", { + get: function() { + var t = _.strings.NATIVE_CONTROL_SELECTOR, + e = this.root_.querySelector(t); + if (!e) + throw new Error( + "Checkbox component requires a " + t + " element" + ); + return e; + }, + enumerable: !0, + configurable: !0 + }), + y); + function y() { + var t = (null !== s && s.apply(this, arguments)) || this; + return (t.ripple_ = t.createRipple_()), t; + } + function E(t) { + return !!t && "function" == typeof t.set; + } + e.MDCCheckbox = C; + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.cssClasses = { + BG_FOCUSED: "mdc-ripple-upgraded--background-focused", + FG_ACTIVATION: "mdc-ripple-upgraded--foreground-activation", + FG_DEACTIVATION: "mdc-ripple-upgraded--foreground-deactivation", + ROOT: "mdc-ripple-upgraded", + UNBOUNDED: "mdc-ripple-upgraded--unbounded" + }), + (e.strings = { + VAR_FG_SCALE: "--mdc-ripple-fg-scale", + VAR_FG_SIZE: "--mdc-ripple-fg-size", + VAR_FG_TRANSLATE_END: "--mdc-ripple-fg-translate-end", + VAR_FG_TRANSLATE_START: "--mdc-ripple-fg-translate-start", + VAR_LEFT: "--mdc-ripple-left", + VAR_TOP: "--mdc-ripple-top" + }), + (e.numbers = { + DEACTIVATION_TIMEOUT_MS: 225, + FG_DEACTIVATION_MS: 150, + INITIAL_ORIGIN_SCALE: 0.6, + PADDING: 10, + TAP_DELAY_MS: 300 + }); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + p = n(17), + c = + ((s = a.MDCFoundation), + r(_, s), + Object.defineProperty(_, "cssClasses", { + get: function() { + return p.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(_, "strings", { + get: function() { + return p.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(_, "numbers", { + get: function() { + return p.numbers; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(_, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + forceLayout: function() {}, + hasNativeControl: function() { + return !1; + }, + isAttachedToDOM: function() { + return !1; + }, + isChecked: function() { + return !1; + }, + isIndeterminate: function() { + return !1; + }, + removeClass: function() {}, + removeNativeControlAttr: function() {}, + setNativeControlAttr: function() {}, + setNativeControlDisabled: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (_.prototype.init = function() { + (this.currentCheckState_ = this.determineCheckState_()), + this.updateAriaChecked_(), + this.adapter_.addClass(p.cssClasses.UPGRADED); + }), + (_.prototype.destroy = function() { + clearTimeout(this.animEndLatchTimer_); + }), + (_.prototype.setDisabled = function(t) { + this.adapter_.setNativeControlDisabled(t), + t + ? this.adapter_.addClass(p.cssClasses.DISABLED) + : this.adapter_.removeClass(p.cssClasses.DISABLED); + }), + (_.prototype.handleAnimationEnd = function() { + var t = this; + this.enableAnimationEndHandler_ && + (clearTimeout(this.animEndLatchTimer_), + (this.animEndLatchTimer_ = setTimeout(function() { + t.adapter_.removeClass(t.currentAnimationClass_), + (t.enableAnimationEndHandler_ = !1); + }, p.numbers.ANIM_END_LATCH_MS))); + }), + (_.prototype.handleChange = function() { + this.transitionCheckState_(); + }), + (_.prototype.transitionCheckState_ = function() { + if (this.adapter_.hasNativeControl()) { + var t = this.currentCheckState_, + e = this.determineCheckState_(); + if (t !== e) { + this.updateAriaChecked_(); + var n = p.strings.TRANSITION_STATE_UNCHECKED, + i = p.cssClasses.SELECTED; + e === n + ? this.adapter_.removeClass(i) + : this.adapter_.addClass(i), + 0 < this.currentAnimationClass_.length && + (clearTimeout(this.animEndLatchTimer_), + this.adapter_.forceLayout(), + this.adapter_.removeClass(this.currentAnimationClass_)), + (this.currentAnimationClass_ = this.getTransitionAnimationClass_( + t, + e + )), + (this.currentCheckState_ = e), + this.adapter_.isAttachedToDOM() && + 0 < this.currentAnimationClass_.length && + (this.adapter_.addClass(this.currentAnimationClass_), + (this.enableAnimationEndHandler_ = !0)); + } + } + }), + (_.prototype.determineCheckState_ = function() { + var t = p.strings.TRANSITION_STATE_INDETERMINATE, + e = p.strings.TRANSITION_STATE_CHECKED, + n = p.strings.TRANSITION_STATE_UNCHECKED; + return this.adapter_.isIndeterminate() + ? t + : this.adapter_.isChecked() + ? e + : n; + }), + (_.prototype.getTransitionAnimationClass_ = function(t, e) { + var n = p.strings.TRANSITION_STATE_INIT, + i = p.strings.TRANSITION_STATE_CHECKED, + r = p.strings.TRANSITION_STATE_UNCHECKED, + o = _.cssClasses, + s = o.ANIM_UNCHECKED_CHECKED, + a = o.ANIM_UNCHECKED_INDETERMINATE, + c = o.ANIM_CHECKED_UNCHECKED, + u = o.ANIM_CHECKED_INDETERMINATE, + l = o.ANIM_INDETERMINATE_CHECKED, + d = o.ANIM_INDETERMINATE_UNCHECKED; + switch (t) { + case n: + return e === r ? "" : e === i ? l : d; + case r: + return e === i ? s : a; + case i: + return e === r ? c : u; + default: + return e === i ? l : d; + } + }), + (_.prototype.updateAriaChecked_ = function() { + this.adapter_.isIndeterminate() + ? this.adapter_.setNativeControlAttr( + p.strings.ARIA_CHECKED_ATTR, + p.strings.ARIA_CHECKED_INDETERMINATE_VALUE + ) + : this.adapter_.removeNativeControlAttr( + p.strings.ARIA_CHECKED_ATTR + ); + }), + _); + function _(t) { + var e = s.call(this, o(o({}, _.defaultAdapter), t)) || this; + return ( + (e.currentCheckState_ = p.strings.TRANSITION_STATE_INIT), + (e.currentAnimationClass_ = ""), + (e.animEndLatchTimer_ = 0), + (e.enableAnimationEndHandler_ = !1), + e + ); + } + (e.MDCCheckboxFoundation = c), (e.default = c); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(43), + u = n(18), + l = + ((s = a.MDCFoundation), + r(d, s), + Object.defineProperty(d, "strings", { + get: function() { + return u.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d, "defaultAdapter", { + get: function() { + return { + focus: function() {}, + getAttribute: function() { + return null; + }, + setAttribute: function() {}, + notifyInteraction: function() {}, + notifyNavigation: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (d.prototype.handleClick = function(t) { + t.stopPropagation(), + this.adapter_.notifyInteraction(u.InteractionTrigger.CLICK); + }), + (d.prototype.handleKeydown = function(t) { + t.stopPropagation(); + var e = c.normalizeKey(t); + if (this.shouldNotifyInteractionFromKey_(e)) { + var n = this.getTriggerFromKey_(e); + this.adapter_.notifyInteraction(n); + } else + c.isNavigationEvent(t) && this.adapter_.notifyNavigation(e); + }), + (d.prototype.removeFocus = function() { + this.adapter_.setAttribute(u.strings.TAB_INDEX, "-1"); + }), + (d.prototype.focus = function() { + this.adapter_.setAttribute(u.strings.TAB_INDEX, "0"), + this.adapter_.focus(); + }), + (d.prototype.isNavigable = function() { + return ( + "true" !== this.adapter_.getAttribute(u.strings.ARIA_HIDDEN) + ); + }), + (d.prototype.shouldNotifyInteractionFromKey_ = function(t) { + var e = t === c.KEY.ENTER || t === c.KEY.SPACEBAR, + n = t === c.KEY.BACKSPACE || t === c.KEY.DELETE; + return e || n; + }), + (d.prototype.getTriggerFromKey_ = function(t) { + return t === c.KEY.SPACEBAR + ? u.InteractionTrigger.SPACEBAR_KEY + : t === c.KEY.ENTER + ? u.InteractionTrigger.ENTER_KEY + : t === c.KEY.DELETE + ? u.InteractionTrigger.DELETE_KEY + : t === c.KEY.BACKSPACE + ? u.InteractionTrigger.BACKSPACE_KEY + : u.InteractionTrigger.UNSPECIFIED; + }), + d); + function d(t) { + return s.call(this, o(o({}, d.defaultAdapter), t)) || this; + } + (e.MDCChipTrailingActionFoundation = l), (e.default = l); + }, + function(t, i, e) { + "use strict"; + Object.defineProperty(i, "__esModule", { value: !0 }), + (i.KEY = { + UNKNOWN: "Unknown", + BACKSPACE: "Backspace", + ENTER: "Enter", + SPACEBAR: "Spacebar", + PAGE_UP: "PageUp", + PAGE_DOWN: "PageDown", + END: "End", + HOME: "Home", + ARROW_LEFT: "ArrowLeft", + ARROW_UP: "ArrowUp", + ARROW_RIGHT: "ArrowRight", + ARROW_DOWN: "ArrowDown", + DELETE: "Delete" + }); + var r = new Set(); + r.add(i.KEY.BACKSPACE), + r.add(i.KEY.ENTER), + r.add(i.KEY.SPACEBAR), + r.add(i.KEY.PAGE_UP), + r.add(i.KEY.PAGE_DOWN), + r.add(i.KEY.END), + r.add(i.KEY.HOME), + r.add(i.KEY.ARROW_LEFT), + r.add(i.KEY.ARROW_UP), + r.add(i.KEY.ARROW_RIGHT), + r.add(i.KEY.ARROW_DOWN), + r.add(i.KEY.DELETE); + var n = 8, + o = 13, + s = 32, + a = 33, + c = 34, + u = 35, + l = 36, + d = 37, + p = 38, + _ = 39, + f = 40, + h = 46, + C = new Map(); + C.set(n, i.KEY.BACKSPACE), + C.set(o, i.KEY.ENTER), + C.set(s, i.KEY.SPACEBAR), + C.set(a, i.KEY.PAGE_UP), + C.set(c, i.KEY.PAGE_DOWN), + C.set(u, i.KEY.END), + C.set(l, i.KEY.HOME), + C.set(d, i.KEY.ARROW_LEFT), + C.set(p, i.KEY.ARROW_UP), + C.set(_, i.KEY.ARROW_RIGHT), + C.set(f, i.KEY.ARROW_DOWN), + C.set(h, i.KEY.DELETE); + var y = new Set(); + function E(t) { + var e = t.key; + if (r.has(e)) return e; + var n = C.get(t.keyCode); + return n || i.KEY.UNKNOWN; + } + y.add(i.KEY.PAGE_UP), + y.add(i.KEY.PAGE_DOWN), + y.add(i.KEY.END), + y.add(i.KEY.HOME), + y.add(i.KEY.ARROW_LEFT), + y.add(i.KEY.ARROW_UP), + y.add(i.KEY.ARROW_RIGHT), + y.add(i.KEY.ARROW_DOWN), + (i.normalizeKey = E), + (i.isNavigationEvent = function(t) { + return y.has(E(t)); + }); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(1), + c = n(3), + u = n(4), + l = n(8), + d = n(19), + p = ["click", "keydown"], + _ = + ((s = a.MDCComponent), + r(f, s), + Object.defineProperty(f.prototype, "selected", { + get: function() { + return this.foundation_.isSelected(); + }, + set: function(t) { + this.foundation_.setSelected(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty( + f.prototype, + "shouldRemoveOnTrailingIconClick", + { + get: function() { + return this.foundation_.getShouldRemoveOnTrailingIconClick(); + }, + set: function(t) { + this.foundation_.setShouldRemoveOnTrailingIconClick(t); + }, + enumerable: !0, + configurable: !0 + } + ), + Object.defineProperty(f.prototype, "ripple", { + get: function() { + return this.ripple_; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "id", { + get: function() { + return this.root_.id; + }, + enumerable: !0, + configurable: !0 + }), + (f.attachTo = function(t) { + return new f(t); + }), + (f.prototype.initialize = function(t) { + var e = this; + void 0 === t && + (t = function(t, e) { + return new c.MDCRipple(t, e); + }), + (this.leadingIcon_ = this.root_.querySelector( + l.strings.LEADING_ICON_SELECTOR + )), + (this.trailingIcon_ = this.root_.querySelector( + l.strings.TRAILING_ICON_SELECTOR + )), + (this.checkmark_ = this.root_.querySelector( + l.strings.CHECKMARK_SELECTOR + )), + (this.primaryAction_ = this.root_.querySelector( + l.strings.PRIMARY_ACTION_SELECTOR + )), + (this.trailingAction_ = this.root_.querySelector( + l.strings.TRAILING_ACTION_SELECTOR + )); + var n = o(o({}, c.MDCRipple.createAdapter(this)), { + computeBoundingRect: function() { + return e.foundation_.getDimensions(); + } + }); + this.ripple_ = t(this.root_, new u.MDCRippleFoundation(n)); + }), + (f.prototype.initialSyncWithDOM = function() { + var e = this; + (this.handleInteraction_ = function(t) { + return e.foundation_.handleInteraction(t); + }), + (this.handleTransitionEnd_ = function(t) { + return e.foundation_.handleTransitionEnd(t); + }), + (this.handleTrailingIconInteraction_ = function(t) { + return e.foundation_.handleTrailingIconInteraction(t); + }), + (this.handleKeydown_ = function(t) { + return e.foundation_.handleKeydown(t); + }), + (this.handleFocusIn_ = function(t) { + e.foundation_.handleFocusIn(t); + }), + (this.handleFocusOut_ = function(t) { + e.foundation_.handleFocusOut(t); + }), + p.forEach(function(t) { + e.listen(t, e.handleInteraction_); + }), + this.listen("transitionend", this.handleTransitionEnd_), + this.listen("keydown", this.handleKeydown_), + this.listen("focusin", this.handleFocusIn_), + this.listen("focusout", this.handleFocusOut_), + this.trailingIcon_ && + p.forEach(function(t) { + e.trailingIcon_.addEventListener( + t, + e.handleTrailingIconInteraction_ + ); + }); + }), + (f.prototype.destroy = function() { + var e = this; + this.ripple_.destroy(), + p.forEach(function(t) { + e.unlisten(t, e.handleInteraction_); + }), + this.unlisten("transitionend", this.handleTransitionEnd_), + this.unlisten("keydown", this.handleKeydown_), + this.unlisten("focusin", this.handleFocusIn_), + this.unlisten("focusout", this.handleFocusOut_), + this.trailingIcon_ && + p.forEach(function(t) { + e.trailingIcon_.removeEventListener( + t, + e.handleTrailingIconInteraction_ + ); + }), + s.prototype.destroy.call(this); + }), + (f.prototype.beginExit = function() { + this.foundation_.beginExit(); + }), + (f.prototype.getDefaultFoundation = function() { + var n = this, + t = { + addClass: function(t) { + return n.root_.classList.add(t); + }, + addClassToLeadingIcon: function(t) { + n.leadingIcon_ && n.leadingIcon_.classList.add(t); + }, + eventTargetHasClass: function(t, e) { + return !!t && t.classList.contains(e); + }, + focusPrimaryAction: function() { + n.primaryAction_ && n.primaryAction_.focus(); + }, + focusTrailingAction: function() { + n.trailingAction_ && n.trailingAction_.focus(); + }, + getAttribute: function(t) { + return n.root_.getAttribute(t); + }, + getCheckmarkBoundingClientRect: function() { + return n.checkmark_ + ? n.checkmark_.getBoundingClientRect() + : null; + }, + getComputedStyleValue: function(t) { + return window.getComputedStyle(n.root_).getPropertyValue(t); + }, + getRootBoundingClientRect: function() { + return n.root_.getBoundingClientRect(); + }, + hasClass: function(t) { + return n.root_.classList.contains(t); + }, + hasLeadingIcon: function() { + return !!n.leadingIcon_; + }, + hasTrailingAction: function() { + return !!n.trailingAction_; + }, + isRTL: function() { + return ( + "rtl" === + window + .getComputedStyle(n.root_) + .getPropertyValue("direction") + ); + }, + notifyInteraction: function() { + return n.emit( + l.strings.INTERACTION_EVENT, + { chipId: n.id }, + !0 + ); + }, + notifyNavigation: function(t, e) { + return n.emit( + l.strings.NAVIGATION_EVENT, + { chipId: n.id, key: t, source: e }, + !0 + ); + }, + notifyRemoval: function(t) { + n.emit( + l.strings.REMOVAL_EVENT, + { chipId: n.id, removedAnnouncement: t }, + !0 + ); + }, + notifySelection: function(t, e) { + return n.emit( + l.strings.SELECTION_EVENT, + { chipId: n.id, selected: t, shouldIgnore: e }, + !0 + ); + }, + notifyTrailingIconInteraction: function() { + return n.emit( + l.strings.TRAILING_ICON_INTERACTION_EVENT, + { chipId: n.id }, + !0 + ); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + removeClassFromLeadingIcon: function(t) { + n.leadingIcon_ && n.leadingIcon_.classList.remove(t); + }, + setPrimaryActionAttr: function(t, e) { + n.primaryAction_ && n.primaryAction_.setAttribute(t, e); + }, + setStyleProperty: function(t, e) { + return n.root_.style.setProperty(t, e); + }, + setTrailingActionAttr: function(t, e) { + n.trailingAction_ && n.trailingAction_.setAttribute(t, e); + } + }; + return new d.MDCChipFoundation(t); + }), + (f.prototype.setSelectedFromChipSet = function(t, e) { + this.foundation_.setSelectedFromChipSet(t, e); + }), + (f.prototype.focusPrimaryAction = function() { + this.foundation_.focusPrimaryAction(); + }), + (f.prototype.focusTrailingAction = function() { + this.foundation_.focusTrailingAction(); + }), + (f.prototype.removeFocus = function() { + this.foundation_.removeFocus(); + }), + (f.prototype.remove = function() { + var t = this.root_.parentNode; + null !== t && t.removeChild(this.root_); + }), + f); + function f() { + return (null !== s && s.apply(this, arguments)) || this; + } + e.MDCChip = _; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + _ = n(8), + c = n(46), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + announceMessage: function() {}, + focusChipPrimaryActionAtIndex: function() {}, + focusChipTrailingActionAtIndex: function() {}, + getChipListCount: function() { + return -1; + }, + getIndexOfChipById: function() { + return -1; + }, + hasClass: function() { + return !1; + }, + isRTL: function() { + return !1; + }, + removeChipAtIndex: function() {}, + removeFocusFromChipAtIndex: function() {}, + selectChipAtIndex: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.getSelectedChipIds = function() { + return this.selectedChipIds_.slice(); + }), + (l.prototype.select = function(t) { + this.select_(t, !1); + }), + (l.prototype.handleChipInteraction = function(t) { + var e = t.chipId, + n = this.adapter_.getIndexOfChipById(e); + this.removeFocusFromChipsExcept_(n), + (this.adapter_.hasClass(c.cssClasses.CHOICE) || + this.adapter_.hasClass(c.cssClasses.FILTER)) && + this.toggleSelect_(e); + }), + (l.prototype.handleChipSelection = function(t) { + var e = t.chipId, + n = t.selected; + if (!t.shouldIgnore) { + var i = 0 <= this.selectedChipIds_.indexOf(e); + n && !i ? this.select(e) : !n && i && this.deselect_(e); + } + }), + (l.prototype.handleChipRemoval = function(t) { + var e = t.chipId, + n = t.removedAnnouncement; + n && this.adapter_.announceMessage(n); + var i = this.adapter_.getIndexOfChipById(e); + this.deselectAndNotifyClients_(e), + this.adapter_.removeChipAtIndex(i); + var r = this.adapter_.getChipListCount() - 1, + o = Math.min(i, r); + this.removeFocusFromChipsExcept_(o), + this.adapter_.focusChipTrailingActionAtIndex(o); + }), + (l.prototype.handleChipNavigation = function(t) { + var e = t.chipId, + n = t.key, + i = t.source, + r = this.adapter_.getChipListCount() - 1, + o = this.adapter_.getIndexOfChipById(e); + if (-1 !== o && _.navigationKeys.has(n)) { + var s = this.adapter_.isRTL(), + a = + n === _.strings.ARROW_LEFT_KEY || + n === _.strings.IE_ARROW_LEFT_KEY, + c = + n === _.strings.ARROW_RIGHT_KEY || + n === _.strings.IE_ARROW_RIGHT_KEY, + u = + n === _.strings.ARROW_DOWN_KEY || + n === _.strings.IE_ARROW_DOWN_KEY, + l = (!s && c) || (s && a) || u, + d = n === _.strings.HOME_KEY, + p = n === _.strings.END_KEY; + l ? o++ : d ? (o = 0) : p ? (o = r) : o--, + o < 0 || + r < o || + (this.removeFocusFromChipsExcept_(o), + this.focusChipAction_(o, n, i)); + } + }), + (l.prototype.focusChipAction_ = function(t, e, n) { + var i = _.jumpChipKeys.has(e); + if (i && n === _.EventSource.PRIMARY) + return this.adapter_.focusChipPrimaryActionAtIndex(t); + if (i && n === _.EventSource.TRAILING) + return this.adapter_.focusChipTrailingActionAtIndex(t); + var r = this.getDirection_(e); + return r === _.Direction.LEFT + ? this.adapter_.focusChipTrailingActionAtIndex(t) + : r === _.Direction.RIGHT + ? this.adapter_.focusChipPrimaryActionAtIndex(t) + : void 0; + }), + (l.prototype.getDirection_ = function(t) { + var e = this.adapter_.isRTL(), + n = + t === _.strings.ARROW_LEFT_KEY || + t === _.strings.IE_ARROW_LEFT_KEY, + i = + t === _.strings.ARROW_RIGHT_KEY || + t === _.strings.IE_ARROW_RIGHT_KEY; + return (!e && n) || (e && i) + ? _.Direction.LEFT + : _.Direction.RIGHT; + }), + (l.prototype.deselect_ = function(t, e) { + void 0 === e && (e = !1); + var n = this.selectedChipIds_.indexOf(t); + if (0 <= n) { + this.selectedChipIds_.splice(n, 1); + var i = this.adapter_.getIndexOfChipById(t); + this.adapter_.selectChipAtIndex(i, !1, e); + } + }), + (l.prototype.deselectAndNotifyClients_ = function(t) { + this.deselect_(t, !0); + }), + (l.prototype.toggleSelect_ = function(t) { + 0 <= this.selectedChipIds_.indexOf(t) + ? this.deselectAndNotifyClients_(t) + : this.selectAndNotifyClients_(t); + }), + (l.prototype.removeFocusFromChipsExcept_ = function(t) { + for (var e = this.adapter_.getChipListCount(), n = 0; n < e; n++) + n !== t && this.adapter_.removeFocusFromChipAtIndex(n); + }), + (l.prototype.selectAndNotifyClients_ = function(t) { + this.select_(t, !0); + }), + (l.prototype.select_ = function(t, e) { + if (!(0 <= this.selectedChipIds_.indexOf(t))) { + if ( + this.adapter_.hasClass(c.cssClasses.CHOICE) && + 0 < this.selectedChipIds_.length + ) { + var n = this.selectedChipIds_[0], + i = this.adapter_.getIndexOfChipById(n); + (this.selectedChipIds_ = []), + this.adapter_.selectChipAtIndex(i, !1, e); + } + this.selectedChipIds_.push(t); + var r = this.adapter_.getIndexOfChipById(t); + this.adapter_.selectChipAtIndex(r, !0, e); + } + }), + l); + function l(t) { + var e = s.call(this, o(o({}, l.defaultAdapter), t)) || this; + return (e.selectedChipIds_ = []), e; + } + (e.MDCChipSetFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.strings = { CHIP_SELECTOR: ".mdc-chip" }), + (e.cssClasses = { + CHOICE: "mdc-chip-set--choice", + FILTER: "mdc-chip-set--filter" + }); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(48), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + getDeterminateCircleAttribute: function() { + return null; + }, + hasClass: function() { + return !1; + }, + removeClass: function() {}, + removeAttribute: function() {}, + setAttribute: function() {}, + setDeterminateCircleAttribute: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.init = function() { + (this.isClosed_ = this.adapter_.hasClass( + c.cssClasses.CLOSED_CLASS + )), + (this.isDeterminate_ = !this.adapter_.hasClass( + c.cssClasses.INDETERMINATE_CLASS + )), + (this.progress_ = 0), + this.isDeterminate_ && + this.adapter_.setAttribute( + c.strings.ARIA_VALUENOW, + this.progress_.toString() + ), + (this.radius_ = Number( + this.adapter_.getDeterminateCircleAttribute(c.strings.RADIUS) + )); + }), + (l.prototype.isDeterminate = function() { + return this.isDeterminate_; + }), + (l.prototype.getProgress = function() { + return this.progress_; + }), + (l.prototype.isClosed = function() { + return this.isClosed_; + }), + (l.prototype.setDeterminate = function(t) { + (this.isDeterminate_ = t), + this.isDeterminate_ + ? (this.adapter_.removeClass( + c.cssClasses.INDETERMINATE_CLASS + ), + this.setProgress(this.progress_)) + : (this.adapter_.addClass(c.cssClasses.INDETERMINATE_CLASS), + this.adapter_.removeAttribute(c.strings.ARIA_VALUENOW)); + }), + (l.prototype.setProgress = function(t) { + if (((this.progress_ = t), this.isDeterminate_)) { + var e = (1 - this.progress_) * (2 * Math.PI * this.radius_); + this.adapter_.setDeterminateCircleAttribute( + c.strings.STROKE_DASHOFFSET, + "" + e + ), + this.adapter_.setAttribute( + c.strings.ARIA_VALUENOW, + this.progress_.toString() + ); + } + }), + (l.prototype.open = function() { + (this.isClosed_ = !1), + this.adapter_.removeClass(c.cssClasses.CLOSED_CLASS); + }), + (l.prototype.close = function() { + (this.isClosed_ = !0), + this.adapter_.addClass(c.cssClasses.CLOSED_CLASS); + }), + l); + function l(t) { + return s.call(this, o(o({}, l.defaultAdapter), t)) || this; + } + (e.MDCCircularProgressFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.cssClasses = { + INDETERMINATE_CLASS: "mdc-circular-progress--indeterminate", + CLOSED_CLASS: "mdc-circular-progress--closed" + }), + (e.strings = { + DETERMINATE_CIRCLE_SELECTOR: + ".mdc-circular-progress__determinate-circle", + ARIA_VALUENOW: "aria-valuenow", + RADIUS: "r", + STROKE_DASHOFFSET: "stroke-dashoffset" + }); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }, + s = + (this && this.__awaiter) || + function(t, s, a, c) { + return new (a = a || Promise)(function(e, n) { + function i(t) { + try { + o(c.next(t)); + } catch (t) { + n(t); + } + } + function r(t) { + try { + o(c.throw(t)); + } catch (t) { + n(t); + } + } + function o(t) { + t.done + ? e(t.value) + : (function(e) { + return e instanceof a + ? e + : new a(function(t) { + t(e); + }); + })(t.value).then(i, r); + } + o((c = c.apply(t, s || [])).next()); + }); + }, + a = + (this && this.__generator) || + function(n, i) { + var r, + o, + s, + t, + a = { + label: 0, + sent: function() { + if (1 & s[0]) throw s[1]; + return s[1]; + }, + trys: [], + ops: [] + }; + return ( + (t = { next: e(0), throw: e(1), return: e(2) }), + "function" == typeof Symbol && + (t[Symbol.iterator] = function() { + return this; + }), + t + ); + function e(e) { + return function(t) { + return (function(e) { + if (r) + throw new TypeError("Generator is already executing."); + for (; a; ) + try { + if ( + ((r = 1), + o && + (s = + 2 & e[0] + ? o.return + : e[0] + ? o.throw || ((s = o.return) && s.call(o), 0) + : o.next) && + !(s = s.call(o, e[1])).done) + ) + return s; + switch ( + ((o = 0), s && (e = [2 & e[0], s.value]), e[0]) + ) { + case 0: + case 1: + s = e; + break; + case 4: + return a.label++, { value: e[1], done: !1 }; + case 5: + a.label++, (o = e[1]), (e = [0]); + continue; + case 7: + (e = a.ops.pop()), a.trys.pop(); + continue; + default: + if ( + !(s = + 0 < (s = a.trys).length && s[s.length - 1]) && + (6 === e[0] || 2 === e[0]) + ) { + a = 0; + continue; + } + if ( + 3 === e[0] && + (!s || (e[1] > s[0] && e[1] < s[3])) + ) { + a.label = e[1]; + break; + } + if (6 === e[0] && a.label < s[1]) { + (a.label = s[1]), (s = e); + break; + } + if (s && a.label < s[2]) { + (a.label = s[2]), a.ops.push(e); + break; + } + s[2] && a.ops.pop(), a.trys.pop(); + continue; + } + e = i.call(n, a); + } catch (t) { + (e = [6, t]), (o = 0); + } finally { + r = s = 0; + } + if (5 & e[0]) throw e[1]; + return { value: e[0] ? e[1] : void 0, done: !0 }; + })([e, t]); + }; + } + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var c, + u = n(0), + l = n(20), + d = + ((c = u.MDCFoundation), + r(p, c), + Object.defineProperty(p, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + addClassAtRowIndex: function() {}, + getAttributeByHeaderCellIndex: function() { + return ""; + }, + getHeaderCellCount: function() { + return 0; + }, + getHeaderCellElements: function() { + return []; + }, + getRowCount: function() { + return 0; + }, + getRowElements: function() { + return []; + }, + getRowIdAtIndex: function() { + return ""; + }, + getRowIndexByChildElement: function() { + return 0; + }, + getSelectedRowCount: function() { + return 0; + }, + getTableBodyHeight: function() { + return ""; + }, + getTableHeaderHeight: function() { + return ""; + }, + isCheckboxAtRowIndexChecked: function() { + return !1; + }, + isHeaderRowCheckboxChecked: function() { + return !1; + }, + isRowsSelectable: function() { + return !1; + }, + notifyRowSelectionChanged: function() {}, + notifySelectedAll: function() {}, + notifySortAction: function() {}, + notifyUnselectedAll: function() {}, + registerHeaderRowCheckbox: function() {}, + registerRowCheckboxes: function() {}, + removeClass: function() {}, + removeClassAtRowIndex: function() {}, + removeClassNameByHeaderCellIndex: function() {}, + setAttributeAtRowIndex: function() {}, + setAttributeByHeaderCellIndex: function() {}, + setClassNameByHeaderCellIndex: function() {}, + setHeaderRowCheckboxChecked: function() {}, + setHeaderRowCheckboxIndeterminate: function() {}, + setProgressIndicatorStyles: function() {}, + setRowCheckboxCheckedAtIndex: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (p.prototype.layout = function() { + this.adapter_.isRowsSelectable() && + (this.adapter_.registerHeaderRowCheckbox(), + this.adapter_.registerRowCheckboxes(), + this.setHeaderRowCheckboxState_()); + }), + (p.prototype.layoutAsync = function() { + return s(this, void 0, void 0, function() { + return a(this, function(t) { + switch (t.label) { + case 0: + return this.adapter_.isRowsSelectable() + ? [4, this.adapter_.registerHeaderRowCheckbox()] + : [3, 3]; + case 1: + return ( + t.sent(), [4, this.adapter_.registerRowCheckboxes()] + ); + case 2: + t.sent(), + this.setHeaderRowCheckboxState_(), + (t.label = 3); + case 3: + return [2]; + } + }); + }); + }), + (p.prototype.getRows = function() { + return this.adapter_.getRowElements(); + }), + (p.prototype.getHeaderCells = function() { + return this.adapter_.getHeaderCellElements(); + }), + (p.prototype.setSelectedRowIds = function(t) { + for (var e = 0; e < this.adapter_.getRowCount(); e++) { + var n = this.adapter_.getRowIdAtIndex(e), + i = !1; + n && 0 <= t.indexOf(n) && (i = !0), + this.adapter_.setRowCheckboxCheckedAtIndex(e, i), + this.selectRowAtIndex_(e, i); + } + this.setHeaderRowCheckboxState_(); + }), + (p.prototype.getRowIds = function() { + for (var t = [], e = 0; e < this.adapter_.getRowCount(); e++) + t.push(this.adapter_.getRowIdAtIndex(e)); + return t; + }), + (p.prototype.getSelectedRowIds = function() { + for (var t = [], e = 0; e < this.adapter_.getRowCount(); e++) + this.adapter_.isCheckboxAtRowIndexChecked(e) && + t.push(this.adapter_.getRowIdAtIndex(e)); + return t; + }), + (p.prototype.handleHeaderRowCheckboxChange = function() { + for ( + var t = this.adapter_.isHeaderRowCheckboxChecked(), e = 0; + e < this.adapter_.getRowCount(); + e++ + ) + this.adapter_.setRowCheckboxCheckedAtIndex(e, t), + this.selectRowAtIndex_(e, t); + t + ? this.adapter_.notifySelectedAll() + : this.adapter_.notifyUnselectedAll(); + }), + (p.prototype.handleRowCheckboxChange = function(t) { + var e = this.adapter_.getRowIndexByChildElement(t.target); + if (-1 !== e) { + var n = this.adapter_.isCheckboxAtRowIndexChecked(e); + this.selectRowAtIndex_(e, n), this.setHeaderRowCheckboxState_(); + var i = this.adapter_.getRowIdAtIndex(e); + this.adapter_.notifyRowSelectionChanged({ + rowId: i, + rowIndex: e, + selected: n + }); + } + }), + (p.prototype.handleSortAction = function(t) { + for ( + var e = t.columnId, n = t.columnIndex, i = t.headerCell, r = 0; + r < this.adapter_.getHeaderCellCount(); + r++ + ) + r !== n && + (this.adapter_.removeClassNameByHeaderCellIndex( + r, + l.cssClasses.HEADER_CELL_SORTED + ), + this.adapter_.removeClassNameByHeaderCellIndex( + r, + l.cssClasses.HEADER_CELL_SORTED_DESCENDING + ), + this.adapter_.setAttributeByHeaderCellIndex( + r, + l.strings.ARIA_SORT, + l.SortValue.NONE + )); + this.adapter_.setClassNameByHeaderCellIndex( + n, + l.cssClasses.HEADER_CELL_SORTED + ); + var o = this.adapter_.getAttributeByHeaderCellIndex( + n, + l.strings.ARIA_SORT + ), + s = l.SortValue.NONE; + (s = + o === l.SortValue.ASCENDING + ? (this.adapter_.setClassNameByHeaderCellIndex( + n, + l.cssClasses.HEADER_CELL_SORTED_DESCENDING + ), + this.adapter_.setAttributeByHeaderCellIndex( + n, + l.strings.ARIA_SORT, + l.SortValue.DESCENDING + ), + l.SortValue.DESCENDING) + : (o === l.SortValue.DESCENDING && + this.adapter_.removeClassNameByHeaderCellIndex( + n, + l.cssClasses.HEADER_CELL_SORTED_DESCENDING + ), + this.adapter_.setAttributeByHeaderCellIndex( + n, + l.strings.ARIA_SORT, + l.SortValue.ASCENDING + ), + l.SortValue.ASCENDING)), + this.adapter_.notifySortAction({ + columnId: e, + columnIndex: n, + headerCell: i, + sortValue: s + }); + }), + (p.prototype.showProgress = function() { + var t = this.adapter_.getTableBodyHeight(), + e = this.adapter_.getTableHeaderHeight(); + this.adapter_.setProgressIndicatorStyles({ height: t, top: e }), + this.adapter_.addClass(l.cssClasses.IN_PROGRESS); + }), + (p.prototype.hideProgress = function() { + this.adapter_.removeClass(l.cssClasses.IN_PROGRESS); + }), + (p.prototype.setHeaderRowCheckboxState_ = function() { + this.adapter_.getSelectedRowCount() === + this.adapter_.getRowCount() + ? (this.adapter_.setHeaderRowCheckboxChecked(!0), + this.adapter_.setHeaderRowCheckboxIndeterminate(!1)) + : (0 === this.adapter_.getSelectedRowCount() + ? this.adapter_.setHeaderRowCheckboxIndeterminate(!1) + : this.adapter_.setHeaderRowCheckboxIndeterminate(!0), + this.adapter_.setHeaderRowCheckboxChecked(!1)); + }), + (p.prototype.selectRowAtIndex_ = function(t, e) { + e + ? (this.adapter_.addClassAtRowIndex( + t, + l.cssClasses.ROW_SELECTED + ), + this.adapter_.setAttributeAtRowIndex( + t, + l.strings.ARIA_SELECTED, + "true" + )) + : (this.adapter_.removeClassAtRowIndex( + t, + l.cssClasses.ROW_SELECTED + ), + this.adapter_.setAttributeAtRowIndex( + t, + l.strings.ARIA_SELECTED, + "false" + )); + }), + p); + function p(t) { + return c.call(this, o(o({}, p.defaultAdapter), t)) || this; + } + e.MDCDataTableFoundation = d; + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.createFocusTrapInstance = function(t, e, n) { + return e(t, { initialFocusEl: n }); + }), + (e.isScrollable = function(t) { + return !!t && t.scrollHeight > t.offsetHeight; + }), + (e.areTopsMisaligned = function(t) { + var e = new Set(); + return ( + [].forEach.call(t, function(t) { + return e.add(t.offsetTop); + }), + 1 < e.size + ); + }); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(52), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "numbers", { + get: function() { + return c.numbers; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addBodyClass: function() {}, + addClass: function() {}, + areButtonsStacked: function() { + return !1; + }, + clickDefaultButton: function() {}, + eventTargetMatches: function() { + return !1; + }, + getActionFromEvent: function() { + return ""; + }, + getInitialFocusEl: function() { + return null; + }, + hasClass: function() { + return !1; + }, + isContentScrollable: function() { + return !1; + }, + notifyClosed: function() {}, + notifyClosing: function() {}, + notifyOpened: function() {}, + notifyOpening: function() {}, + releaseFocus: function() {}, + removeBodyClass: function() {}, + removeClass: function() {}, + reverseButtons: function() {}, + trapFocus: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.init = function() { + this.adapter_.hasClass(c.cssClasses.STACKED) && + this.setAutoStackButtons(!1); + }), + (l.prototype.destroy = function() { + this.isOpen_ && this.close(c.strings.DESTROY_ACTION), + this.animationTimer_ && + (clearTimeout(this.animationTimer_), + this.handleAnimationTimerEnd_()), + this.layoutFrame_ && + (cancelAnimationFrame(this.layoutFrame_), + (this.layoutFrame_ = 0)); + }), + (l.prototype.open = function() { + var t = this; + (this.isOpen_ = !0), + this.adapter_.notifyOpening(), + this.adapter_.addClass(c.cssClasses.OPENING), + this.runNextAnimationFrame_(function() { + t.adapter_.addClass(c.cssClasses.OPEN), + t.adapter_.addBodyClass(c.cssClasses.SCROLL_LOCK), + t.layout(), + (t.animationTimer_ = setTimeout(function() { + t.handleAnimationTimerEnd_(), + t.adapter_.trapFocus(t.adapter_.getInitialFocusEl()), + t.adapter_.notifyOpened(); + }, c.numbers.DIALOG_ANIMATION_OPEN_TIME_MS)); + }); + }), + (l.prototype.close = function(t) { + var e = this; + void 0 === t && (t = ""), + this.isOpen_ && + ((this.isOpen_ = !1), + this.adapter_.notifyClosing(t), + this.adapter_.addClass(c.cssClasses.CLOSING), + this.adapter_.removeClass(c.cssClasses.OPEN), + this.adapter_.removeBodyClass(c.cssClasses.SCROLL_LOCK), + cancelAnimationFrame(this.animationFrame_), + (this.animationFrame_ = 0), + clearTimeout(this.animationTimer_), + (this.animationTimer_ = setTimeout(function() { + e.adapter_.releaseFocus(), + e.handleAnimationTimerEnd_(), + e.adapter_.notifyClosed(t); + }, c.numbers.DIALOG_ANIMATION_CLOSE_TIME_MS))); + }), + (l.prototype.isOpen = function() { + return this.isOpen_; + }), + (l.prototype.getEscapeKeyAction = function() { + return this.escapeKeyAction_; + }), + (l.prototype.setEscapeKeyAction = function(t) { + this.escapeKeyAction_ = t; + }), + (l.prototype.getScrimClickAction = function() { + return this.scrimClickAction_; + }), + (l.prototype.setScrimClickAction = function(t) { + this.scrimClickAction_ = t; + }), + (l.prototype.getAutoStackButtons = function() { + return this.autoStackButtons_; + }), + (l.prototype.setAutoStackButtons = function(t) { + this.autoStackButtons_ = t; + }), + (l.prototype.layout = function() { + var t = this; + this.layoutFrame_ && cancelAnimationFrame(this.layoutFrame_), + (this.layoutFrame_ = requestAnimationFrame(function() { + t.layoutInternal_(), (t.layoutFrame_ = 0); + })); + }), + (l.prototype.handleClick = function(t) { + if ( + this.adapter_.eventTargetMatches( + t.target, + c.strings.SCRIM_SELECTOR + ) && + "" !== this.scrimClickAction_ + ) + this.close(this.scrimClickAction_); + else { + var e = this.adapter_.getActionFromEvent(t); + e && this.close(e); + } + }), + (l.prototype.handleKeydown = function(t) { + var e = "Enter" === t.key || 13 === t.keyCode; + if (e && !this.adapter_.getActionFromEvent(t)) { + var n = !this.adapter_.eventTargetMatches( + t.target, + c.strings.SUPPRESS_DEFAULT_PRESS_SELECTOR + ); + e && n && this.adapter_.clickDefaultButton(); + } + }), + (l.prototype.handleDocumentKeydown = function(t) { + ("Escape" !== t.key && 27 !== t.keyCode) || + "" === this.escapeKeyAction_ || + this.close(this.escapeKeyAction_); + }), + (l.prototype.layoutInternal_ = function() { + this.autoStackButtons_ && this.detectStackedButtons_(), + this.detectScrollableContent_(); + }), + (l.prototype.handleAnimationTimerEnd_ = function() { + (this.animationTimer_ = 0), + this.adapter_.removeClass(c.cssClasses.OPENING), + this.adapter_.removeClass(c.cssClasses.CLOSING); + }), + (l.prototype.runNextAnimationFrame_ = function(t) { + var e = this; + cancelAnimationFrame(this.animationFrame_), + (this.animationFrame_ = requestAnimationFrame(function() { + (e.animationFrame_ = 0), + clearTimeout(e.animationTimer_), + (e.animationTimer_ = setTimeout(t, 0)); + })); + }), + (l.prototype.detectStackedButtons_ = function() { + this.adapter_.removeClass(c.cssClasses.STACKED); + var t = this.adapter_.areButtonsStacked(); + t && this.adapter_.addClass(c.cssClasses.STACKED), + t !== this.areButtonsStacked_ && + (this.adapter_.reverseButtons(), + (this.areButtonsStacked_ = t)); + }), + (l.prototype.detectScrollableContent_ = function() { + this.adapter_.removeClass(c.cssClasses.SCROLLABLE), + this.adapter_.isContentScrollable() && + this.adapter_.addClass(c.cssClasses.SCROLLABLE); + }), + l); + function l(t) { + var e = s.call(this, o(o({}, l.defaultAdapter), t)) || this; + return ( + (e.isOpen_ = !1), + (e.animationFrame_ = 0), + (e.animationTimer_ = 0), + (e.layoutFrame_ = 0), + (e.escapeKeyAction_ = c.strings.CLOSE_ACTION), + (e.scrimClickAction_ = c.strings.CLOSE_ACTION), + (e.autoStackButtons_ = !0), + (e.areButtonsStacked_ = !1), + e + ); + } + (e.MDCDialogFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.cssClasses = { + CLOSING: "mdc-dialog--closing", + OPEN: "mdc-dialog--open", + OPENING: "mdc-dialog--opening", + SCROLLABLE: "mdc-dialog--scrollable", + SCROLL_LOCK: "mdc-dialog-scroll-lock", + STACKED: "mdc-dialog--stacked" + }), + (e.strings = { + ACTION_ATTRIBUTE: "data-mdc-dialog-action", + BUTTON_DEFAULT_ATTRIBUTE: "data-mdc-dialog-button-default", + BUTTON_SELECTOR: ".mdc-dialog__button", + CLOSED_EVENT: "MDCDialog:closed", + CLOSE_ACTION: "close", + CLOSING_EVENT: "MDCDialog:closing", + CONTAINER_SELECTOR: ".mdc-dialog__container", + CONTENT_SELECTOR: ".mdc-dialog__content", + DESTROY_ACTION: "destroy", + INITIAL_FOCUS_ATTRIBUTE: "data-mdc-dialog-initial-focus", + OPENED_EVENT: "MDCDialog:opened", + OPENING_EVENT: "MDCDialog:opening", + SCRIM_SELECTOR: ".mdc-dialog__scrim", + SUPPRESS_DEFAULT_PRESS_SELECTOR: [ + "textarea", + ".mdc-menu .mdc-list-item" + ].join(", "), + SURFACE_SELECTOR: ".mdc-dialog__surface" + }), + (e.numbers = { + DIALOG_ANIMATION_CLOSE_TIME_MS: 75, + DIALOG_ANIMATION_OPEN_TIME_MS: 150 + }); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.createFocusTrapInstance = function(t, e) { + return e(t, { skipInitialFocus: !0 }); + }); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.cssClasses = { + ANIMATE: "mdc-drawer--animate", + CLOSING: "mdc-drawer--closing", + DISMISSIBLE: "mdc-drawer--dismissible", + MODAL: "mdc-drawer--modal", + OPEN: "mdc-drawer--open", + OPENING: "mdc-drawer--opening", + ROOT: "mdc-drawer" + }; + e.strings = { + APP_CONTENT_SELECTOR: ".mdc-drawer-app-content", + CLOSE_EVENT: "MDCDrawer:closed", + OPEN_EVENT: "MDCDrawer:opened", + SCRIM_SELECTOR: ".mdc-drawer-scrim" + }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(23), + a = + ((o = s.MDCDismissibleDrawerFoundation), + r(c, o), + (c.prototype.handleScrimClick = function() { + this.close(); + }), + (c.prototype.opened_ = function() { + this.adapter_.trapFocus(); + }), + (c.prototype.closed_ = function() { + this.adapter_.releaseFocus(); + }), + c); + function c() { + return (null !== o && o.apply(this, arguments)) || this; + } + (e.MDCModalDrawerFoundation = a), (e.default = a); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.cssClasses = { + LABEL_FLOAT_ABOVE: "mdc-floating-label--float-above", + LABEL_SHAKE: "mdc-floating-label--shake", + ROOT: "mdc-floating-label" + }); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(58), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + activateInputRipple: function() {}, + deactivateInputRipple: function() {}, + deregisterInteractionHandler: function() {}, + registerInteractionHandler: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.init = function() { + this.adapter_.registerInteractionHandler( + "click", + this.clickHandler_ + ); + }), + (l.prototype.destroy = function() { + this.adapter_.deregisterInteractionHandler( + "click", + this.clickHandler_ + ); + }), + (l.prototype.handleClick_ = function() { + var t = this; + this.adapter_.activateInputRipple(), + requestAnimationFrame(function() { + return t.adapter_.deactivateInputRipple(); + }); + }), + l); + function l(t) { + var e = s.call(this, o(o({}, l.defaultAdapter), t)) || this; + return ( + (e.clickHandler_ = function() { + return e.handleClick_(); + }), + e + ); + } + (e.MDCFormFieldFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.cssClasses = { ROOT: "mdc-form-field" }), + (e.strings = { LABEL_SELECTOR: ".mdc-form-field > label" }); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(60), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + hasClass: function() { + return !1; + }, + notifyChange: function() {}, + removeClass: function() {}, + getAttr: function() { + return null; + }, + setAttr: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.init = function() { + var t = this.adapter_.getAttr(c.strings.DATA_ARIA_LABEL_ON), + e = this.adapter_.getAttr(c.strings.DATA_ARIA_LABEL_OFF); + if (t && e) { + if (null !== this.adapter_.getAttr(c.strings.ARIA_PRESSED)) + throw new Error( + "MDCIconButtonToggleFoundation: Button should not set `aria-pressed` if it has a toggled aria label." + ); + this.hasToggledAriaLabel = !0; + } else + this.adapter_.setAttr( + c.strings.ARIA_PRESSED, + String(this.isOn()) + ); + }), + (l.prototype.handleClick = function() { + this.toggle(), this.adapter_.notifyChange({ isOn: this.isOn() }); + }), + (l.prototype.isOn = function() { + return this.adapter_.hasClass(c.cssClasses.ICON_BUTTON_ON); + }), + (l.prototype.toggle = function(t) { + if ( + (void 0 === t && (t = !this.isOn()), + t + ? this.adapter_.addClass(c.cssClasses.ICON_BUTTON_ON) + : this.adapter_.removeClass(c.cssClasses.ICON_BUTTON_ON), + this.hasToggledAriaLabel) + ) { + var e = t + ? this.adapter_.getAttr(c.strings.DATA_ARIA_LABEL_ON) + : this.adapter_.getAttr(c.strings.DATA_ARIA_LABEL_OFF); + this.adapter_.setAttr(c.strings.ARIA_LABEL, e || ""); + } else this.adapter_.setAttr(c.strings.ARIA_PRESSED, "" + t); + }), + l); + function l(t) { + var e = s.call(this, o(o({}, l.defaultAdapter), t)) || this; + return (e.hasToggledAriaLabel = !1), e; + } + (e.MDCIconButtonToggleFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.cssClasses = { + ICON_BUTTON_ON: "mdc-icon-button--on", + ROOT: "mdc-icon-button" + }), + (e.strings = { + ARIA_LABEL: "aria-label", + ARIA_PRESSED: "aria-pressed", + DATA_ARIA_LABEL_OFF: "data-aria-label-off", + DATA_ARIA_LABEL_ON: "data-aria-label-on", + CHANGE_EVENT: "MDCIconButtonToggle:change" + }); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(62), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + hasClass: function() { + return !1; + }, + setStyle: function() {}, + registerEventHandler: function() {}, + deregisterEventHandler: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.init = function() { + this.adapter_.registerEventHandler( + "transitionend", + this.transitionEndHandler_ + ); + }), + (l.prototype.destroy = function() { + this.adapter_.deregisterEventHandler( + "transitionend", + this.transitionEndHandler_ + ); + }), + (l.prototype.activate = function() { + this.adapter_.removeClass(c.cssClasses.LINE_RIPPLE_DEACTIVATING), + this.adapter_.addClass(c.cssClasses.LINE_RIPPLE_ACTIVE); + }), + (l.prototype.setRippleCenter = function(t) { + this.adapter_.setStyle("transform-origin", t + "px center"); + }), + (l.prototype.deactivate = function() { + this.adapter_.addClass(c.cssClasses.LINE_RIPPLE_DEACTIVATING); + }), + (l.prototype.handleTransitionEnd = function(t) { + var e = this.adapter_.hasClass( + c.cssClasses.LINE_RIPPLE_DEACTIVATING + ); + "opacity" === t.propertyName && + e && + (this.adapter_.removeClass(c.cssClasses.LINE_RIPPLE_ACTIVE), + this.adapter_.removeClass( + c.cssClasses.LINE_RIPPLE_DEACTIVATING + )); + }), + l); + function l(t) { + var e = s.call(this, o(o({}, l.defaultAdapter), t)) || this; + return ( + (e.transitionEndHandler_ = function(t) { + return e.handleTransitionEnd(t); + }), + e + ); + } + (e.MDCLineRippleFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.cssClasses = { + LINE_RIPPLE_ACTIVE: "mdc-line-ripple--active", + LINE_RIPPLE_DEACTIVATING: "mdc-line-ripple--deactivating" + }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(15), + c = n(0), + u = n(64), + l = + ((s = c.MDCFoundation), + r(d, s), + Object.defineProperty(d, "cssClasses", { + get: function() { + return u.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d, "strings", { + get: function() { + return u.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + forceLayout: function() {}, + setBufferBarStyle: function() { + return null; + }, + setPrimaryBarStyle: function() { + return null; + }, + hasClass: function() { + return !1; + }, + removeAttribute: function() {}, + removeClass: function() {}, + setAttribute: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (d.prototype.init = function() { + (this.isDeterminate_ = !this.adapter_.hasClass( + u.cssClasses.INDETERMINATE_CLASS + )), + (this.isReversed_ = this.adapter_.hasClass( + u.cssClasses.REVERSED_CLASS + )), + (this.progress_ = 0), + (this.buffer_ = 1); + }), + (d.prototype.setDeterminate = function(t) { + if (((this.isDeterminate_ = t), this.isDeterminate_)) + return ( + this.adapter_.removeClass(u.cssClasses.INDETERMINATE_CLASS), + this.adapter_.setAttribute( + u.strings.ARIA_VALUENOW, + this.progress_.toString() + ), + this.setPrimaryBarProgress_(this.progress_), + void this.setBufferBarProgress_(this.buffer_) + ); + this.isReversed_ && + (this.adapter_.removeClass(u.cssClasses.REVERSED_CLASS), + this.adapter_.forceLayout(), + this.adapter_.addClass(u.cssClasses.REVERSED_CLASS)), + this.adapter_.addClass(u.cssClasses.INDETERMINATE_CLASS), + this.adapter_.removeAttribute(u.strings.ARIA_VALUENOW), + this.setPrimaryBarProgress_(1), + this.setBufferBarProgress_(1); + }), + (d.prototype.isDeterminate = function() { + return this.isDeterminate_; + }), + (d.prototype.setProgress = function(t) { + (this.progress_ = t), + this.isDeterminate_ && + (this.setPrimaryBarProgress_(t), + this.adapter_.setAttribute( + u.strings.ARIA_VALUENOW, + t.toString() + )); + }), + (d.prototype.getProgress = function() { + return this.progress_; + }), + (d.prototype.setBuffer = function(t) { + (this.buffer_ = t), + this.isDeterminate_ && this.setBufferBarProgress_(t); + }), + (d.prototype.setReverse = function(t) { + (this.isReversed_ = t), + this.isDeterminate_ || + (this.adapter_.removeClass(u.cssClasses.INDETERMINATE_CLASS), + this.adapter_.forceLayout(), + this.adapter_.addClass(u.cssClasses.INDETERMINATE_CLASS)), + this.isReversed_ + ? this.adapter_.addClass(u.cssClasses.REVERSED_CLASS) + : this.adapter_.removeClass(u.cssClasses.REVERSED_CLASS); + }), + (d.prototype.open = function() { + this.adapter_.removeClass(u.cssClasses.CLOSED_CLASS); + }), + (d.prototype.close = function() { + this.adapter_.addClass(u.cssClasses.CLOSED_CLASS); + }), + (d.prototype.setPrimaryBarProgress_ = function(t) { + var e = "scaleX(" + t + ")", + n = + "undefined" != typeof window + ? a.getCorrectPropertyName(window, "transform") + : "transform"; + this.adapter_.setPrimaryBarStyle(n, e); + }), + (d.prototype.setBufferBarProgress_ = function(t) { + var e = 100 * t + "%"; + this.adapter_.setBufferBarStyle(u.strings.FLEX_BASIS, e); + }), + d); + function d(t) { + return s.call(this, o(o({}, d.defaultAdapter), t)) || this; + } + (e.MDCLinearProgressFoundation = l), (e.default = l); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.cssClasses = { + CLOSED_CLASS: "mdc-linear-progress--closed", + INDETERMINATE_CLASS: "mdc-linear-progress--indeterminate", + REVERSED_CLASS: "mdc-linear-progress--reversed" + }), + (e.strings = { + ARIA_VALUENOW: "aria-valuenow", + BUFFER_BAR_SELECTOR: ".mdc-linear-progress__buffer-bar", + FLEX_BASIS: "flex-basis", + PRIMARY_BAR_SELECTOR: ".mdc-linear-progress__primary-bar" + }); + }, + function(t, e, n) { + "use strict"; + var i; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.getTransformPropertyName = function(t, e) { + if ((void 0 === e && (e = !1), void 0 === i || e)) { + var n = t.document.createElement("div"); + i = "transform" in n.style ? "transform" : "webkitTransform"; + } + return i; + }); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(1), + c = n(6), + u = n(11), + l = o(n(65)), + d = + ((s = a.MDCComponent), + r(p, s), + (p.attachTo = function(t) { + return new p(t); + }), + (p.prototype.initialSyncWithDOM = function() { + var e = this, + t = this.root_.parentElement; + (this.anchorElement = + t && t.classList.contains(c.cssClasses.ANCHOR) ? t : null), + this.root_.classList.contains(c.cssClasses.FIXED) && + this.setFixedPosition(!0), + (this.handleKeydown_ = function(t) { + return e.foundation_.handleKeydown(t); + }), + (this.handleBodyClick_ = function(t) { + return e.foundation_.handleBodyClick(t); + }), + (this.registerBodyClickListener_ = function() { + return document.body.addEventListener( + "click", + e.handleBodyClick_, + { capture: !0 } + ); + }), + (this.deregisterBodyClickListener_ = function() { + return document.body.removeEventListener( + "click", + e.handleBodyClick_ + ); + }), + this.listen("keydown", this.handleKeydown_), + this.listen( + c.strings.OPENED_EVENT, + this.registerBodyClickListener_ + ), + this.listen( + c.strings.CLOSED_EVENT, + this.deregisterBodyClickListener_ + ); + }), + (p.prototype.destroy = function() { + this.unlisten("keydown", this.handleKeydown_), + this.unlisten( + c.strings.OPENED_EVENT, + this.registerBodyClickListener_ + ), + this.unlisten( + c.strings.CLOSED_EVENT, + this.deregisterBodyClickListener_ + ), + s.prototype.destroy.call(this); + }), + (p.prototype.isOpen = function() { + return this.foundation_.isOpen(); + }), + (p.prototype.open = function() { + this.foundation_.open(); + }), + (p.prototype.close = function(t) { + void 0 === t && (t = !1), this.foundation_.close(t); + }), + Object.defineProperty(p.prototype, "quickOpen", { + set: function(t) { + this.foundation_.setQuickOpen(t); + }, + enumerable: !0, + configurable: !0 + }), + (p.prototype.setIsHoisted = function(t) { + this.foundation_.setIsHoisted(t); + }), + (p.prototype.setMenuSurfaceAnchorElement = function(t) { + this.anchorElement = t; + }), + (p.prototype.setFixedPosition = function(t) { + t + ? this.root_.classList.add(c.cssClasses.FIXED) + : this.root_.classList.remove(c.cssClasses.FIXED), + this.foundation_.setFixedPosition(t); + }), + (p.prototype.setAbsolutePosition = function(t, e) { + this.foundation_.setAbsolutePosition(t, e), this.setIsHoisted(!0); + }), + (p.prototype.setAnchorCorner = function(t) { + this.foundation_.setAnchorCorner(t); + }), + (p.prototype.setAnchorMargin = function(t) { + this.foundation_.setAnchorMargin(t); + }), + (p.prototype.getDefaultFoundation = function() { + var n = this, + t = { + addClass: function(t) { + return n.root_.classList.add(t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + hasClass: function(t) { + return n.root_.classList.contains(t); + }, + hasAnchor: function() { + return !!n.anchorElement; + }, + notifyClose: function() { + return n.emit( + u.MDCMenuSurfaceFoundation.strings.CLOSED_EVENT, + {} + ); + }, + notifyOpen: function() { + return n.emit( + u.MDCMenuSurfaceFoundation.strings.OPENED_EVENT, + {} + ); + }, + isElementInContainer: function(t) { + return n.root_.contains(t); + }, + isRtl: function() { + return ( + "rtl" === + getComputedStyle(n.root_).getPropertyValue("direction") + ); + }, + setTransformOrigin: function(t) { + var e = l.getTransformPropertyName(window) + "-origin"; + n.root_.style.setProperty(e, t); + }, + isFocused: function() { + return document.activeElement === n.root_; + }, + saveFocus: function() { + n.previousFocus_ = document.activeElement; + }, + restoreFocus: function() { + n.root_.contains(document.activeElement) && + n.previousFocus_ && + n.previousFocus_.focus && + n.previousFocus_.focus(); + }, + getInnerDimensions: function() { + return { + width: n.root_.offsetWidth, + height: n.root_.offsetHeight + }; + }, + getAnchorDimensions: function() { + return n.anchorElement + ? n.anchorElement.getBoundingClientRect() + : null; + }, + getWindowDimensions: function() { + return { + width: window.innerWidth, + height: window.innerHeight + }; + }, + getBodyDimensions: function() { + return { + width: document.body.clientWidth, + height: document.body.clientHeight + }; + }, + getWindowScroll: function() { + return { x: window.pageXOffset, y: window.pageYOffset }; + }, + setPosition: function(t) { + (n.root_.style.left = "left" in t ? t.left + "px" : ""), + (n.root_.style.right = + "right" in t ? t.right + "px" : ""), + (n.root_.style.top = "top" in t ? t.top + "px" : ""), + (n.root_.style.bottom = + "bottom" in t ? t.bottom + "px" : ""); + }, + setMaxHeight: function(t) { + n.root_.style.maxHeight = t; + } + }; + return new u.MDCMenuSurfaceFoundation(t); + }), + p); + function p() { + return (null !== s && s.apply(this, arguments)) || this; + } + e.MDCMenuSurface = d; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(2), + c = n(22), + u = n(10), + l = n(66), + d = n(11), + p = n(12), + _ = n(68), + f = + ((o = s.MDCComponent), + r(h, o), + (h.attachTo = function(t) { + return new h(t); + }), + (h.prototype.initialize = function(t, e) { + void 0 === t && + (t = function(t) { + return new l.MDCMenuSurface(t); + }), + void 0 === e && + (e = function(t) { + return new c.MDCList(t); + }), + (this.menuSurfaceFactory_ = t), + (this.listFactory_ = e); + }), + (h.prototype.initialSyncWithDOM = function() { + var e = this; + this.menuSurface_ = this.menuSurfaceFactory_(this.root_); + var t = this.root_.querySelector(p.strings.LIST_SELECTOR); + t + ? ((this.list_ = this.listFactory_(t)), + (this.list_.wrapFocus = !0)) + : (this.list_ = null), + (this.handleKeydown_ = function(t) { + return e.foundation_.handleKeydown(t); + }), + (this.handleItemAction_ = function(t) { + return e.foundation_.handleItemAction( + e.items[t.detail.index] + ); + }), + (this.handleMenuSurfaceOpened_ = function() { + return e.foundation_.handleMenuSurfaceOpened(); + }), + this.menuSurface_.listen( + d.MDCMenuSurfaceFoundation.strings.OPENED_EVENT, + this.handleMenuSurfaceOpened_ + ), + this.listen("keydown", this.handleKeydown_), + this.listen( + u.MDCListFoundation.strings.ACTION_EVENT, + this.handleItemAction_ + ); + }), + (h.prototype.destroy = function() { + this.list_ && this.list_.destroy(), + this.menuSurface_.destroy(), + this.menuSurface_.unlisten( + d.MDCMenuSurfaceFoundation.strings.OPENED_EVENT, + this.handleMenuSurfaceOpened_ + ), + this.unlisten("keydown", this.handleKeydown_), + this.unlisten( + u.MDCListFoundation.strings.ACTION_EVENT, + this.handleItemAction_ + ), + o.prototype.destroy.call(this); + }), + Object.defineProperty(h.prototype, "open", { + get: function() { + return this.menuSurface_.isOpen(); + }, + set: function(t) { + t ? this.menuSurface_.open() : this.menuSurface_.close(); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(h.prototype, "wrapFocus", { + get: function() { + return !!this.list_ && this.list_.wrapFocus; + }, + set: function(t) { + this.list_ && (this.list_.wrapFocus = t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(h.prototype, "items", { + get: function() { + return this.list_ ? this.list_.listElements : []; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(h.prototype, "quickOpen", { + set: function(t) { + this.menuSurface_.quickOpen = t; + }, + enumerable: !0, + configurable: !0 + }), + (h.prototype.setDefaultFocusState = function(t) { + this.foundation_.setDefaultFocusState(t); + }), + (h.prototype.setAnchorCorner = function(t) { + this.menuSurface_.setAnchorCorner(t); + }), + (h.prototype.setAnchorMargin = function(t) { + this.menuSurface_.setAnchorMargin(t); + }), + (h.prototype.setSelectedIndex = function(t) { + this.foundation_.setSelectedIndex(t); + }), + (h.prototype.setEnabled = function(t, e) { + this.foundation_.setEnabled(t, e); + }), + (h.prototype.getOptionByIndex = function(t) { + return t < this.items.length ? this.items[t] : null; + }), + (h.prototype.setFixedPosition = function(t) { + this.menuSurface_.setFixedPosition(t); + }), + (h.prototype.setIsHoisted = function(t) { + this.menuSurface_.setIsHoisted(t); + }), + (h.prototype.setAbsolutePosition = function(t, e) { + this.menuSurface_.setAbsolutePosition(t, e); + }), + (h.prototype.setAnchorElement = function(t) { + this.menuSurface_.anchorElement = t; + }), + (h.prototype.getDefaultFoundation = function() { + var i = this, + t = { + addClassToElementAtIndex: function(t, e) { + i.items[t].classList.add(e); + }, + removeClassFromElementAtIndex: function(t, e) { + i.items[t].classList.remove(e); + }, + addAttributeToElementAtIndex: function(t, e, n) { + i.items[t].setAttribute(e, n); + }, + removeAttributeFromElementAtIndex: function(t, e) { + i.items[t].removeAttribute(e); + }, + elementContainsClass: function(t, e) { + return t.classList.contains(e); + }, + closeSurface: function(t) { + return i.menuSurface_.close(t); + }, + getElementIndex: function(t) { + return i.items.indexOf(t); + }, + notifySelected: function(t) { + return i.emit(p.strings.SELECTED_EVENT, { + index: t.index, + item: i.items[t.index] + }); + }, + getMenuItemCount: function() { + return i.items.length; + }, + focusItemAtIndex: function(t) { + return i.items[t].focus(); + }, + focusListRoot: function() { + return i.root_ + .querySelector(p.strings.LIST_SELECTOR) + .focus(); + }, + isSelectableItemAtIndex: function(t) { + return !!a.closest( + i.items[t], + "." + p.cssClasses.MENU_SELECTION_GROUP + ); + }, + getSelectedSiblingOfItemAtIndex: function(t) { + var e = a + .closest( + i.items[t], + "." + p.cssClasses.MENU_SELECTION_GROUP + ) + .querySelector( + "." + p.cssClasses.MENU_SELECTED_LIST_ITEM + ); + return e ? i.items.indexOf(e) : -1; + } + }; + return new _.MDCMenuFoundation(t); + }), + h); + function h() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCMenu = f; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(9), + u = n(11), + l = n(12), + d = + ((s = a.MDCFoundation), + r(p, s), + Object.defineProperty(p, "cssClasses", { + get: function() { + return l.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(p, "strings", { + get: function() { + return l.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(p, "numbers", { + get: function() { + return l.numbers; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(p, "defaultAdapter", { + get: function() { + return { + addClassToElementAtIndex: function() {}, + removeClassFromElementAtIndex: function() {}, + addAttributeToElementAtIndex: function() {}, + removeAttributeFromElementAtIndex: function() {}, + elementContainsClass: function() { + return !1; + }, + closeSurface: function() {}, + getElementIndex: function() { + return -1; + }, + notifySelected: function() {}, + getMenuItemCount: function() { + return 0; + }, + focusItemAtIndex: function() {}, + focusListRoot: function() {}, + getSelectedSiblingOfItemAtIndex: function() { + return -1; + }, + isSelectableItemAtIndex: function() { + return !1; + } + }; + }, + enumerable: !0, + configurable: !0 + }), + (p.prototype.destroy = function() { + this.closeAnimationEndTimerId_ && + clearTimeout(this.closeAnimationEndTimerId_), + this.adapter_.closeSurface(); + }), + (p.prototype.handleKeydown = function(t) { + var e = t.key, + n = t.keyCode; + ("Tab" !== e && 9 !== n) || this.adapter_.closeSurface(!0); + }), + (p.prototype.handleItemAction = function(e) { + var n = this, + t = this.adapter_.getElementIndex(e); + t < 0 || + (this.adapter_.notifySelected({ index: t }), + this.adapter_.closeSurface(), + (this.closeAnimationEndTimerId_ = setTimeout(function() { + var t = n.adapter_.getElementIndex(e); + n.adapter_.isSelectableItemAtIndex(t) && + n.setSelectedIndex(t); + }, u + .MDCMenuSurfaceFoundation.numbers.TRANSITION_CLOSE_DURATION))); + }), + (p.prototype.handleMenuSurfaceOpened = function() { + switch (this.defaultFocusState_) { + case l.DefaultFocusState.FIRST_ITEM: + this.adapter_.focusItemAtIndex(0); + break; + case l.DefaultFocusState.LAST_ITEM: + this.adapter_.focusItemAtIndex( + this.adapter_.getMenuItemCount() - 1 + ); + break; + case l.DefaultFocusState.NONE: + break; + default: + this.adapter_.focusListRoot(); + } + }), + (p.prototype.setDefaultFocusState = function(t) { + this.defaultFocusState_ = t; + }), + (p.prototype.setSelectedIndex = function(t) { + if ( + (this.validatedIndex_(t), + !this.adapter_.isSelectableItemAtIndex(t)) + ) + throw new Error( + "MDCMenuFoundation: No selection group at specified index." + ); + var e = this.adapter_.getSelectedSiblingOfItemAtIndex(t); + 0 <= e && + (this.adapter_.removeAttributeFromElementAtIndex( + e, + l.strings.ARIA_CHECKED_ATTR + ), + this.adapter_.removeClassFromElementAtIndex( + e, + l.cssClasses.MENU_SELECTED_LIST_ITEM + )), + this.adapter_.addClassToElementAtIndex( + t, + l.cssClasses.MENU_SELECTED_LIST_ITEM + ), + this.adapter_.addAttributeToElementAtIndex( + t, + l.strings.ARIA_CHECKED_ATTR, + "true" + ); + }), + (p.prototype.setEnabled = function(t, e) { + this.validatedIndex_(t), + e + ? (this.adapter_.removeClassFromElementAtIndex( + t, + c.cssClasses.LIST_ITEM_DISABLED_CLASS + ), + this.adapter_.addAttributeToElementAtIndex( + t, + l.strings.ARIA_DISABLED_ATTR, + "false" + )) + : (this.adapter_.addClassToElementAtIndex( + t, + c.cssClasses.LIST_ITEM_DISABLED_CLASS + ), + this.adapter_.addAttributeToElementAtIndex( + t, + l.strings.ARIA_DISABLED_ATTR, + "true" + )); + }), + (p.prototype.validatedIndex_ = function(t) { + var e = this.adapter_.getMenuItemCount(); + if (!(0 <= t && t < e)) + throw new Error( + "MDCMenuFoundation: No list item at specified index." + ); + }), + p); + function p(t) { + var e = s.call(this, o(o({}, p.defaultAdapter), t)) || this; + return ( + (e.closeAnimationEndTimerId_ = 0), + (e.defaultFocusState_ = l.DefaultFocusState.LIST_ROOT), + e + ); + } + (e.MDCMenuFoundation = d), (e.default = d); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(28), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "numbers", { + get: function() { + return c.numbers; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + setNotchWidthProperty: function() {}, + removeNotchWidthProperty: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.notch = function(t) { + var e = l.cssClasses.OUTLINE_NOTCHED; + 0 < t && (t += c.numbers.NOTCH_ELEMENT_PADDING), + this.adapter_.setNotchWidthProperty(t), + this.adapter_.addClass(e); + }), + (l.prototype.closeNotch = function() { + var t = l.cssClasses.OUTLINE_NOTCHED; + this.adapter_.removeClass(t), + this.adapter_.removeNotchWidthProperty(); + }), + l); + function l(t) { + return s.call(this, o(o({}, l.defaultAdapter), t)) || this; + } + (e.MDCNotchedOutlineFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(71), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + setNativeControlDisabled: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.setDisabled = function(t) { + var e = l.cssClasses.DISABLED; + this.adapter_.setNativeControlDisabled(t), + t ? this.adapter_.addClass(e) : this.adapter_.removeClass(e); + }), + l); + function l(t) { + return s.call(this, o(o({}, l.defaultAdapter), t)) || this; + } + (e.MDCRadioFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.strings = { NATIVE_CONTROL_SELECTOR: ".mdc-radio__native-control" }; + e.cssClasses = { DISABLED: "mdc-radio--disabled", ROOT: "mdc-radio" }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(6), + u = n(29), + l = + ((s = a.MDCFoundation), + r(d, s), + Object.defineProperty(d, "cssClasses", { + get: function() { + return u.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d, "numbers", { + get: function() { + return u.numbers; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d, "strings", { + get: function() { + return u.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + hasClass: function() { + return !1; + }, + activateBottomLine: function() {}, + deactivateBottomLine: function() {}, + getSelectedMenuItem: function() { + return null; + }, + hasLabel: function() { + return !1; + }, + floatLabel: function() {}, + getLabelWidth: function() { + return 0; + }, + hasOutline: function() { + return !1; + }, + notchOutline: function() {}, + closeOutline: function() {}, + setRippleCenter: function() {}, + notifyChange: function() {}, + setSelectedText: function() {}, + isSelectAnchorFocused: function() { + return !1; + }, + getSelectAnchorAttr: function() { + return ""; + }, + setSelectAnchorAttr: function() {}, + openMenu: function() {}, + closeMenu: function() {}, + getAnchorElement: function() { + return null; + }, + setMenuAnchorElement: function() {}, + setMenuAnchorCorner: function() {}, + setMenuWrapFocus: function() {}, + setAttributeAtIndex: function() {}, + removeAttributeAtIndex: function() {}, + focusMenuItemAtIndex: function() {}, + getMenuItemCount: function() { + return 0; + }, + getMenuItemValues: function() { + return []; + }, + getMenuItemTextAtIndex: function() { + return ""; + }, + getMenuItemAttr: function() { + return ""; + }, + addClassAtIndex: function() {}, + removeClassAtIndex: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (d.prototype.getSelectedIndex = function() { + return this.selectedIndex; + }), + (d.prototype.setSelectedIndex = function(t, e) { + if ( + (void 0 === e && (e = !1), + !(t >= this.adapter_.getMenuItemCount())) + ) { + var n = this.selectedIndex; + (this.selectedIndex = t), + this.selectedIndex === u.numbers.UNSET_INDEX + ? this.adapter_.setSelectedText("") + : this.adapter_.setSelectedText( + this.adapter_ + .getMenuItemTextAtIndex(this.selectedIndex) + .trim() + ), + n !== u.numbers.UNSET_INDEX && + (this.adapter_.removeClassAtIndex( + n, + u.cssClasses.SELECTED_ITEM_CLASS + ), + this.adapter_.removeAttributeAtIndex( + n, + u.strings.ARIA_SELECTED_ATTR + )), + this.selectedIndex !== u.numbers.UNSET_INDEX && + (this.adapter_.addClassAtIndex( + this.selectedIndex, + u.cssClasses.SELECTED_ITEM_CLASS + ), + this.adapter_.setAttributeAtIndex( + this.selectedIndex, + u.strings.ARIA_SELECTED_ATTR, + "true" + )), + this.layout(), + e && this.adapter_.closeMenu(), + this.handleChange(); + } + }), + (d.prototype.setValue = function(t) { + var e = this.menuItemValues.indexOf(t); + this.setSelectedIndex(e); + }), + (d.prototype.getValue = function() { + var t = this.adapter_.getSelectedMenuItem(); + return ( + (t && this.adapter_.getMenuItemAttr(t, u.strings.VALUE_ATTR)) || + "" + ); + }), + (d.prototype.getDisabled = function() { + return this.disabled; + }), + (d.prototype.setDisabled = function(t) { + (this.disabled = t), + this.disabled + ? (this.adapter_.addClass(u.cssClasses.DISABLED), + this.adapter_.closeMenu()) + : this.adapter_.removeClass(u.cssClasses.DISABLED), + this.leadingIcon && this.leadingIcon.setDisabled(this.disabled), + this.adapter_.setSelectAnchorAttr( + "tabindex", + this.disabled ? "-1" : "0" + ), + this.adapter_.setSelectAnchorAttr( + "aria-disabled", + this.disabled.toString() + ); + }), + (d.prototype.setHelperTextContent = function(t) { + this.helperText && this.helperText.setContent(t); + }), + (d.prototype.layout = function() { + if (this.adapter_.hasLabel()) { + var t = 0 < this.getValue().length; + this.notchOutline(t); + } + }), + (d.prototype.handleMenuOpened = function() { + if (0 !== this.adapter_.getMenuItemValues().length) { + this.adapter_.addClass(u.cssClasses.ACTIVATED); + var t = 0 <= this.selectedIndex ? this.selectedIndex : 0; + this.adapter_.focusMenuItemAtIndex(t); + } + }), + (d.prototype.handleMenuClosed = function() { + this.adapter_.removeClass(u.cssClasses.ACTIVATED), + (this.isMenuOpen = !1), + this.adapter_.setSelectAnchorAttr("aria-expanded", "false"), + this.adapter_.isSelectAnchorFocused() || this.blur(); + }), + (d.prototype.handleChange = function() { + this.updateLabel(), + this.adapter_.notifyChange(this.getValue()), + this.adapter_.hasClass(u.cssClasses.REQUIRED) && + (this.setValid(this.isValid()), + this.helperText && + this.helperText.setValidity(this.isValid())); + }), + (d.prototype.handleMenuItemAction = function(t) { + this.setSelectedIndex(t, !0); + }), + (d.prototype.handleFocus = function() { + this.adapter_.addClass(u.cssClasses.FOCUSED), + this.adapter_.hasLabel() && + (this.notchOutline(!0), this.adapter_.floatLabel(!0)), + this.adapter_.activateBottomLine(), + this.helperText && this.helperText.showToScreenReader(); + }), + (d.prototype.handleBlur = function() { + this.isMenuOpen || this.blur(); + }), + (d.prototype.handleClick = function(t) { + this.isMenuOpen || + (this.adapter_.setRippleCenter(t), + this.adapter_.openMenu(), + (this.isMenuOpen = !0), + this.adapter_.setSelectAnchorAttr("aria-expanded", "true")); + }), + (d.prototype.handleKeydown = function(t) { + if (!this.isMenuOpen) { + var e = "Enter" === t.key || 13 === t.keyCode, + n = "Space" === t.key || 32 === t.keyCode, + i = "ArrowUp" === t.key || 38 === t.keyCode, + r = "ArrowDown" === t.key || 40 === t.keyCode; + this.adapter_.hasClass(u.cssClasses.FOCUSED) && + (e || n || i || r) && + (this.adapter_.openMenu(), + (this.isMenuOpen = !0), + this.adapter_.setSelectAnchorAttr("aria-expanded", "true"), + t.preventDefault()); + } + }), + (d.prototype.notchOutline = function(t) { + if (this.adapter_.hasOutline()) { + var e = this.adapter_.hasClass(u.cssClasses.FOCUSED); + if (t) { + var n = u.numbers.LABEL_SCALE, + i = this.adapter_.getLabelWidth() * n; + this.adapter_.notchOutline(i); + } else e || this.adapter_.closeOutline(); + } + }), + (d.prototype.setLeadingIconAriaLabel = function(t) { + this.leadingIcon && this.leadingIcon.setAriaLabel(t); + }), + (d.prototype.setLeadingIconContent = function(t) { + this.leadingIcon && this.leadingIcon.setContent(t); + }), + (d.prototype.setValid = function(t) { + this.adapter_.setSelectAnchorAttr( + "aria-invalid", + (!t).toString() + ), + t + ? this.adapter_.removeClass(u.cssClasses.INVALID) + : this.adapter_.addClass(u.cssClasses.INVALID); + }), + (d.prototype.isValid = function() { + return ( + !( + this.adapter_.hasClass(u.cssClasses.REQUIRED) && + !this.adapter_.hasClass(u.cssClasses.DISABLED) + ) || + (this.selectedIndex !== u.numbers.UNSET_INDEX && + (0 !== this.selectedIndex || Boolean(this.getValue()))) + ); + }), + (d.prototype.setRequired = function(t) { + t + ? this.adapter_.addClass(u.cssClasses.REQUIRED) + : this.adapter_.removeClass(u.cssClasses.REQUIRED), + this.adapter_.setSelectAnchorAttr( + "aria-required", + t.toString() + ); + }), + (d.prototype.getRequired = function() { + return ( + "true" === this.adapter_.getSelectAnchorAttr("aria-required") + ); + }), + (d.prototype.init = function() { + var t = this.adapter_.getAnchorElement(); + t && + (this.adapter_.setMenuAnchorElement(t), + this.adapter_.setMenuAnchorCorner(c.Corner.BOTTOM_START)), + this.adapter_.setMenuWrapFocus(!1); + var e = this.getValue(); + e && this.setValue(e), this.updateLabel(); + }), + (d.prototype.updateLabel = function() { + var t = 0 < this.getValue().length; + this.adapter_.hasLabel() && + (this.notchOutline(t), + this.adapter_.hasClass(u.cssClasses.FOCUSED) || + this.adapter_.floatLabel(t)); + }), + (d.prototype.blur = function() { + this.adapter_.removeClass(u.cssClasses.FOCUSED), + this.updateLabel(), + this.adapter_.deactivateBottomLine(), + this.adapter_.hasClass(u.cssClasses.REQUIRED) && + (this.setValid(this.isValid()), + this.helperText && + this.helperText.setValidity(this.isValid())); + }), + d); + function d(t, e) { + void 0 === e && (e = {}); + var n = s.call(this, o(o({}, d.defaultAdapter), t)) || this; + return ( + (n.selectedIndex = u.numbers.UNSET_INDEX), + (n.disabled = !1), + (n.isMenuOpen = !1), + (n.leadingIcon = e.leadingIcon), + (n.helperText = e.helperText), + (n.menuItemValues = n.adapter_.getMenuItemValues()), + n.setDisabled(n.adapter_.hasClass(u.cssClasses.DISABLED)), + n + ); + } + (e.MDCSelectFoundation = l), (e.default = l); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(74), + c = + ((o = s.MDCComponent), + r(u, o), + (u.attachTo = function(t) { + return new u(t); + }), + Object.defineProperty(u.prototype, "foundation", { + get: function() { + return this.foundation_; + }, + enumerable: !0, + configurable: !0 + }), + (u.prototype.getDefaultFoundation = function() { + var n = this, + t = { + addClass: function(t) { + return n.root_.classList.add(t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + hasClass: function(t) { + return n.root_.classList.contains(t); + }, + setAttr: function(t, e) { + return n.root_.setAttribute(t, e); + }, + removeAttr: function(t) { + return n.root_.removeAttribute(t); + }, + setContent: function(t) { + n.root_.textContent = t; + } + }; + return new a.MDCSelectHelperTextFoundation(t); + }), + u); + function u() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCSelectHelperText = c; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(75), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + hasClass: function() { + return !1; + }, + setAttr: function() {}, + removeAttr: function() {}, + setContent: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.setContent = function(t) { + this.adapter_.setContent(t); + }), + (l.prototype.setPersistent = function(t) { + t + ? this.adapter_.addClass(c.cssClasses.HELPER_TEXT_PERSISTENT) + : this.adapter_.removeClass( + c.cssClasses.HELPER_TEXT_PERSISTENT + ); + }), + (l.prototype.setValidation = function(t) { + t + ? this.adapter_.addClass( + c.cssClasses.HELPER_TEXT_VALIDATION_MSG + ) + : this.adapter_.removeClass( + c.cssClasses.HELPER_TEXT_VALIDATION_MSG + ); + }), + (l.prototype.showToScreenReader = function() { + this.adapter_.removeAttr(c.strings.ARIA_HIDDEN); + }), + (l.prototype.setValidity = function(t) { + var e = this.adapter_.hasClass( + c.cssClasses.HELPER_TEXT_PERSISTENT + ), + n = + this.adapter_.hasClass( + c.cssClasses.HELPER_TEXT_VALIDATION_MSG + ) && !t; + n + ? this.adapter_.setAttr(c.strings.ROLE, "alert") + : this.adapter_.removeAttr(c.strings.ROLE), + e || n || this.hide_(); + }), + (l.prototype.hide_ = function() { + this.adapter_.setAttr(c.strings.ARIA_HIDDEN, "true"); + }), + l); + function l(t) { + return s.call(this, o(o({}, l.defaultAdapter), t)) || this; + } + (e.MDCSelectHelperTextFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.strings = { ARIA_HIDDEN: "aria-hidden", ROLE: "role" }; + e.cssClasses = { + HELPER_TEXT_PERSISTENT: "mdc-select-helper-text--persistent", + HELPER_TEXT_VALIDATION_MSG: "mdc-select-helper-text--validation-msg" + }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(77), + c = + ((o = s.MDCComponent), + r(u, o), + (u.attachTo = function(t) { + return new u(t); + }), + Object.defineProperty(u.prototype, "foundation", { + get: function() { + return this.foundation_; + }, + enumerable: !0, + configurable: !0 + }), + (u.prototype.getDefaultFoundation = function() { + var n = this, + t = { + getAttr: function(t) { + return n.root_.getAttribute(t); + }, + setAttr: function(t, e) { + return n.root_.setAttribute(t, e); + }, + removeAttr: function(t) { + return n.root_.removeAttribute(t); + }, + setContent: function(t) { + n.root_.textContent = t; + }, + registerInteractionHandler: function(t, e) { + return n.listen(t, e); + }, + deregisterInteractionHandler: function(t, e) { + return n.unlisten(t, e); + }, + notifyIconAction: function() { + return n.emit( + a.MDCSelectIconFoundation.strings.ICON_EVENT, + {}, + !0 + ); + } + }; + return new a.MDCSelectIconFoundation(t); + }), + u); + function u() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCSelectIcon = c; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(78), + u = ["click", "keydown"], + l = + ((s = a.MDCFoundation), + r(d, s), + Object.defineProperty(d, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d, "defaultAdapter", { + get: function() { + return { + getAttr: function() { + return null; + }, + setAttr: function() {}, + removeAttr: function() {}, + setContent: function() {}, + registerInteractionHandler: function() {}, + deregisterInteractionHandler: function() {}, + notifyIconAction: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (d.prototype.init = function() { + var e = this; + (this.savedTabIndex_ = this.adapter_.getAttr("tabindex")), + u.forEach(function(t) { + e.adapter_.registerInteractionHandler( + t, + e.interactionHandler_ + ); + }); + }), + (d.prototype.destroy = function() { + var e = this; + u.forEach(function(t) { + e.adapter_.deregisterInteractionHandler( + t, + e.interactionHandler_ + ); + }); + }), + (d.prototype.setDisabled = function(t) { + this.savedTabIndex_ && + (t + ? (this.adapter_.setAttr("tabindex", "-1"), + this.adapter_.removeAttr("role")) + : (this.adapter_.setAttr("tabindex", this.savedTabIndex_), + this.adapter_.setAttr("role", c.strings.ICON_ROLE))); + }), + (d.prototype.setAriaLabel = function(t) { + this.adapter_.setAttr("aria-label", t); + }), + (d.prototype.setContent = function(t) { + this.adapter_.setContent(t); + }), + (d.prototype.handleInteraction = function(t) { + var e = "Enter" === t.key || 13 === t.keyCode; + ("click" !== t.type && !e) || this.adapter_.notifyIconAction(); + }), + d); + function d(t) { + var e = s.call(this, o(o({}, d.defaultAdapter), t)) || this; + return ( + (e.savedTabIndex_ = null), + (e.interactionHandler_ = function(t) { + return e.handleInteraction(t); + }), + e + ); + } + (e.MDCSelectIconFoundation = l), (e.default = l); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.strings = { ICON_EVENT: "MDCSelect:icon", ICON_ROLE: "button" }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + c = n(15), + a = n(0), + u = n(30), + l = !!window.PointerEvent, + d = l ? ["pointerdown"] : ["mousedown", "touchstart"], + p = l ? ["pointerup"] : ["mouseup", "touchend"], + _ = { + mousedown: "mousemove", + pointerdown: "pointermove", + touchstart: "touchmove" + }, + f = "ArrowDown", + h = "ArrowLeft", + C = "ArrowRight", + y = "ArrowUp", + E = "End", + g = "Home", + m = "PageDown", + A = "PageUp", + v = + ((s = a.MDCFoundation), + r(b, s), + Object.defineProperty(b, "cssClasses", { + get: function() { + return u.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(b, "strings", { + get: function() { + return u.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(b, "numbers", { + get: function() { + return u.numbers; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(b, "defaultAdapter", { + get: function() { + return { + hasClass: function() { + return !1; + }, + addClass: function() {}, + removeClass: function() {}, + getAttribute: function() { + return null; + }, + setAttribute: function() {}, + removeAttribute: function() {}, + computeBoundingRect: function() { + return { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 0, + height: 0 + }; + }, + getTabIndex: function() { + return 0; + }, + registerInteractionHandler: function() {}, + deregisterInteractionHandler: function() {}, + registerThumbContainerInteractionHandler: function() {}, + deregisterThumbContainerInteractionHandler: function() {}, + registerBodyInteractionHandler: function() {}, + deregisterBodyInteractionHandler: function() {}, + registerResizeHandler: function() {}, + deregisterResizeHandler: function() {}, + notifyInput: function() {}, + notifyChange: function() {}, + setThumbContainerStyleProperty: function() {}, + setTrackStyleProperty: function() {}, + setMarkerValue: function() {}, + setTrackMarkers: function() {}, + isRTL: function() { + return !1; + } + }; + }, + enumerable: !0, + configurable: !0 + }), + (b.prototype.init = function() { + var e = this; + (this.isDiscrete_ = this.adapter_.hasClass( + u.cssClasses.IS_DISCRETE + )), + (this.hasTrackMarker_ = this.adapter_.hasClass( + u.cssClasses.HAS_TRACK_MARKER + )), + d.forEach(function(t) { + e.adapter_.registerInteractionHandler( + t, + e.interactionStartHandler_ + ), + e.adapter_.registerThumbContainerInteractionHandler( + t, + e.thumbContainerPointerHandler_ + ); + }), + this.adapter_.registerInteractionHandler( + "keydown", + this.keydownHandler_ + ), + this.adapter_.registerInteractionHandler( + "focus", + this.focusHandler_ + ), + this.adapter_.registerInteractionHandler( + "blur", + this.blurHandler_ + ), + this.adapter_.registerResizeHandler(this.resizeHandler_), + this.layout(), + this.isDiscrete_ && 0 === this.getStep() && (this.step_ = 1); + }), + (b.prototype.destroy = function() { + var e = this; + d.forEach(function(t) { + e.adapter_.deregisterInteractionHandler( + t, + e.interactionStartHandler_ + ), + e.adapter_.deregisterThumbContainerInteractionHandler( + t, + e.thumbContainerPointerHandler_ + ); + }), + this.adapter_.deregisterInteractionHandler( + "keydown", + this.keydownHandler_ + ), + this.adapter_.deregisterInteractionHandler( + "focus", + this.focusHandler_ + ), + this.adapter_.deregisterInteractionHandler( + "blur", + this.blurHandler_ + ), + this.adapter_.deregisterResizeHandler(this.resizeHandler_); + }), + (b.prototype.setupTrackMarker = function() { + this.isDiscrete_ && + this.hasTrackMarker_ && + 0 !== this.getStep() && + this.adapter_.setTrackMarkers( + this.getStep(), + this.getMax(), + this.getMin() + ); + }), + (b.prototype.layout = function() { + (this.rect_ = this.adapter_.computeBoundingRect()), + this.updateUIForCurrentValue_(); + }), + (b.prototype.getValue = function() { + return this.value_; + }), + (b.prototype.setValue = function(t) { + this.setValue_(t, !1); + }), + (b.prototype.getMax = function() { + return this.max_; + }), + (b.prototype.setMax = function(t) { + if (t < this.min_) + throw new Error( + "Cannot set max to be less than the slider's minimum value" + ); + (this.max_ = t), + this.setValue_(this.value_, !1, !0), + this.adapter_.setAttribute( + u.strings.ARIA_VALUEMAX, + String(this.max_) + ), + this.setupTrackMarker(); + }), + (b.prototype.getMin = function() { + return this.min_; + }), + (b.prototype.setMin = function(t) { + if (t > this.max_) + throw new Error( + "Cannot set min to be greater than the slider's maximum value" + ); + (this.min_ = t), + this.setValue_(this.value_, !1, !0), + this.adapter_.setAttribute( + u.strings.ARIA_VALUEMIN, + String(this.min_) + ), + this.setupTrackMarker(); + }), + (b.prototype.getStep = function() { + return this.step_; + }), + (b.prototype.setStep = function(t) { + if (t < 0) + throw new Error("Step cannot be set to a negative number"); + this.isDiscrete_ && ("number" != typeof t || t < 1) && (t = 1), + (this.step_ = t), + this.setValue_(this.value_, !1, !0), + this.setupTrackMarker(); + }), + (b.prototype.isDisabled = function() { + return this.disabled_; + }), + (b.prototype.setDisabled = function(t) { + (this.disabled_ = t), + this.toggleClass_(u.cssClasses.DISABLED, this.disabled_), + this.disabled_ + ? ((this.savedTabIndex_ = this.adapter_.getTabIndex()), + this.adapter_.setAttribute(u.strings.ARIA_DISABLED, "true"), + this.adapter_.removeAttribute("tabindex")) + : (this.adapter_.removeAttribute(u.strings.ARIA_DISABLED), + isNaN(this.savedTabIndex_) || + this.adapter_.setAttribute( + "tabindex", + String(this.savedTabIndex_) + )); + }), + (b.prototype.handleDown_ = function(t) { + var n = this; + if (!this.disabled_) { + (this.preventFocusState_ = !0), + this.setInTransit_(!this.handlingThumbTargetEvt_), + (this.handlingThumbTargetEvt_ = !1), + this.setActive_(!0); + var i = function(t) { + n.handleMove_(t); + }, + r = _[t.type], + e = function e() { + n.handleUp_(), + n.adapter_.deregisterBodyInteractionHandler(r, i), + p.forEach(function(t) { + return n.adapter_.deregisterBodyInteractionHandler( + t, + e + ); + }); + }; + this.adapter_.registerBodyInteractionHandler(r, i), + p.forEach(function(t) { + return n.adapter_.registerBodyInteractionHandler(t, e); + }), + this.setValueFromEvt_(t); + } + }), + (b.prototype.handleMove_ = function(t) { + t.preventDefault(), this.setValueFromEvt_(t); + }), + (b.prototype.handleUp_ = function() { + this.setActive_(!1), this.adapter_.notifyChange(); + }), + (b.prototype.getClientX_ = function(t) { + return t.targetTouches && 0 < t.targetTouches.length + ? t.targetTouches[0].clientX + : t.clientX; + }), + (b.prototype.setValueFromEvt_ = function(t) { + var e = this.getClientX_(t), + n = this.computeValueFromClientX_(e); + this.setValue_(n, !0); + }), + (b.prototype.computeValueFromClientX_ = function(t) { + var e = this.max_, + n = this.min_, + i = (t - this.rect_.left) / this.rect_.width; + return this.adapter_.isRTL() && (i = 1 - i), n + i * (e - n); + }), + (b.prototype.handleKeydown_ = function(t) { + var e = this.getKeyId_(t), + n = this.getValueForKeyId_(e); + isNaN(n) || + (t.preventDefault(), + this.adapter_.addClass(u.cssClasses.FOCUS), + this.setValue_(n, !0), + this.adapter_.notifyChange()); + }), + (b.prototype.getKeyId_ = function(t) { + return t.key === h || 37 === t.keyCode + ? h + : t.key === C || 39 === t.keyCode + ? C + : t.key === y || 38 === t.keyCode + ? y + : t.key === f || 40 === t.keyCode + ? f + : t.key === g || 36 === t.keyCode + ? g + : t.key === E || 35 === t.keyCode + ? E + : t.key === A || 33 === t.keyCode + ? A + : t.key === m || 34 === t.keyCode + ? m + : ""; + }), + (b.prototype.getValueForKeyId_ = function(t) { + var e = this.max_, + n = this.min_, + i = this.step_ || (e - n) / 100; + switch ( + (!this.adapter_.isRTL() || (t !== h && t !== C) || (i = -i), t) + ) { + case h: + case f: + return this.value_ - i; + case C: + case y: + return this.value_ + i; + case g: + return this.min_; + case E: + return this.max_; + case A: + return this.value_ + i * u.numbers.PAGE_FACTOR; + case m: + return this.value_ - i * u.numbers.PAGE_FACTOR; + default: + return NaN; + } + }), + (b.prototype.handleFocus_ = function() { + this.preventFocusState_ || + this.adapter_.addClass(u.cssClasses.FOCUS); + }), + (b.prototype.handleBlur_ = function() { + (this.preventFocusState_ = !1), + this.adapter_.removeClass(u.cssClasses.FOCUS); + }), + (b.prototype.setValue_ = function(t, e, n) { + if ((void 0 === n && (n = !1), t !== this.value_ || n)) { + var i = this.min_, + r = this.max_, + o = t === i || t === r; + this.step_ && !o && (t = this.quantize_(t)), + t < i ? (t = i) : r < t && (t = r), + (t = t || 0), + (this.value_ = t), + this.adapter_.setAttribute( + u.strings.ARIA_VALUENOW, + String(this.value_) + ), + this.updateUIForCurrentValue_(), + e && + (this.adapter_.notifyInput(), + this.isDiscrete_ && this.adapter_.setMarkerValue(t)); + } + }), + (b.prototype.quantize_ = function(t) { + return Math.round(t / this.step_) * this.step_; + }), + (b.prototype.updateUIForCurrentValue_ = function() { + var e = this, + t = this.max_, + n = this.min_, + i = (this.value_ - n) / (t - n), + r = i * this.rect_.width; + this.adapter_.isRTL() && (r = this.rect_.width - r); + var o = "undefined" != typeof window, + s = o + ? c.getCorrectPropertyName(window, "transform") + : "transform", + a = o + ? c.getCorrectEventName(window, "transitionend") + : "transitionend"; + this.inTransit_ && + this.adapter_.registerThumbContainerInteractionHandler( + a, + function t() { + e.setInTransit_(!1), + e.adapter_.deregisterThumbContainerInteractionHandler( + a, + t + ); + } + ), + requestAnimationFrame(function() { + e.adapter_.setThumbContainerStyleProperty( + s, + "translateX(" + r + "px) translateX(-50%)" + ), + e.adapter_.setTrackStyleProperty(s, "scaleX(" + i + ")"); + }); + }), + (b.prototype.setActive_ = function(t) { + (this.active_ = t), + this.toggleClass_(u.cssClasses.ACTIVE, this.active_); + }), + (b.prototype.setInTransit_ = function(t) { + (this.inTransit_ = t), + this.toggleClass_(u.cssClasses.IN_TRANSIT, this.inTransit_); + }), + (b.prototype.toggleClass_ = function(t, e) { + e ? this.adapter_.addClass(t) : this.adapter_.removeClass(t); + }), + b); + function b(t) { + var e = s.call(this, o(o({}, b.defaultAdapter), t)) || this; + return ( + (e.savedTabIndex_ = NaN), + (e.active_ = !1), + (e.inTransit_ = !1), + (e.isDiscrete_ = !1), + (e.hasTrackMarker_ = !1), + (e.handlingThumbTargetEvt_ = !1), + (e.min_ = 0), + (e.max_ = 100), + (e.step_ = 0), + (e.value_ = 0), + (e.disabled_ = !1), + (e.preventFocusState_ = !1), + (e.thumbContainerPointerHandler_ = function() { + return (e.handlingThumbTargetEvt_ = !0); + }), + (e.interactionStartHandler_ = function(t) { + return e.handleDown_(t); + }), + (e.keydownHandler_ = function(t) { + return e.handleKeydown_(t); + }), + (e.focusHandler_ = function() { + return e.handleFocus_(); + }), + (e.blurHandler_ = function() { + return e.handleBlur_(); + }), + (e.resizeHandler_ = function() { + return e.layout(); + }), + e + ); + } + (e.MDCSliderFoundation = v), (e.default = v); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + var i = n(13), + r = i.numbers.ARIA_LIVE_DELAY_MS, + o = i.strings.ARIA_LIVE_LABEL_TEXT_ATTR; + e.announce = function(t, e) { + void 0 === e && (e = t); + var n = t.getAttribute("aria-live"), + i = e.textContent.trim(); + i && + n && + (t.setAttribute("aria-live", "off"), + (e.textContent = ""), + (e.innerHTML = + ' '), + e.setAttribute(o, i), + setTimeout(function() { + t.setAttribute("aria-live", n), + e.removeAttribute(o), + (e.textContent = i); + }, r)); + }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(13), + u = c.cssClasses.OPENING, + l = c.cssClasses.OPEN, + d = c.cssClasses.CLOSING, + p = c.strings.REASON_ACTION, + _ = c.strings.REASON_DISMISS, + f = + ((s = a.MDCFoundation), + r(h, s), + Object.defineProperty(h, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(h, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(h, "numbers", { + get: function() { + return c.numbers; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(h, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + announce: function() {}, + notifyClosed: function() {}, + notifyClosing: function() {}, + notifyOpened: function() {}, + notifyOpening: function() {}, + removeClass: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (h.prototype.destroy = function() { + this.clearAutoDismissTimer_(), + cancelAnimationFrame(this.animationFrame_), + (this.animationFrame_ = 0), + clearTimeout(this.animationTimer_), + (this.animationTimer_ = 0), + this.adapter_.removeClass(u), + this.adapter_.removeClass(l), + this.adapter_.removeClass(d); + }), + (h.prototype.open = function() { + var e = this; + this.clearAutoDismissTimer_(), + (this.isOpen_ = !0), + this.adapter_.notifyOpening(), + this.adapter_.removeClass(d), + this.adapter_.addClass(u), + this.adapter_.announce(), + this.runNextAnimationFrame_(function() { + e.adapter_.addClass(l), + (e.animationTimer_ = setTimeout(function() { + var t = e.getTimeoutMs(); + e.handleAnimationTimerEnd_(), + e.adapter_.notifyOpened(), + t !== c.numbers.INDETERMINATE && + (e.autoDismissTimer_ = setTimeout(function() { + e.close(_); + }, t)); + }, c.numbers.SNACKBAR_ANIMATION_OPEN_TIME_MS)); + }); + }), + (h.prototype.close = function(t) { + var e = this; + void 0 === t && (t = ""), + this.isOpen_ && + (cancelAnimationFrame(this.animationFrame_), + (this.animationFrame_ = 0), + this.clearAutoDismissTimer_(), + (this.isOpen_ = !1), + this.adapter_.notifyClosing(t), + this.adapter_.addClass(c.cssClasses.CLOSING), + this.adapter_.removeClass(c.cssClasses.OPEN), + this.adapter_.removeClass(c.cssClasses.OPENING), + clearTimeout(this.animationTimer_), + (this.animationTimer_ = setTimeout(function() { + e.handleAnimationTimerEnd_(), e.adapter_.notifyClosed(t); + }, c.numbers.SNACKBAR_ANIMATION_CLOSE_TIME_MS))); + }), + (h.prototype.isOpen = function() { + return this.isOpen_; + }), + (h.prototype.getTimeoutMs = function() { + return this.autoDismissTimeoutMs_; + }), + (h.prototype.setTimeoutMs = function(t) { + var e = c.numbers.MIN_AUTO_DISMISS_TIMEOUT_MS, + n = c.numbers.MAX_AUTO_DISMISS_TIMEOUT_MS, + i = c.numbers.INDETERMINATE; + if (!(t === c.numbers.INDETERMINATE || (t <= n && e <= t))) + throw new Error( + "\n timeoutMs must be an integer in the range " + + e + + "–" + + n + + "\n (or " + + i + + " to disable), but got '" + + t + + "'" + ); + this.autoDismissTimeoutMs_ = t; + }), + (h.prototype.getCloseOnEscape = function() { + return this.closeOnEscape_; + }), + (h.prototype.setCloseOnEscape = function(t) { + this.closeOnEscape_ = t; + }), + (h.prototype.handleKeyDown = function(t) { + ("Escape" !== t.key && 27 !== t.keyCode) || + !this.getCloseOnEscape() || + this.close(_); + }), + (h.prototype.handleActionButtonClick = function(t) { + this.close(p); + }), + (h.prototype.handleActionIconClick = function(t) { + this.close(_); + }), + (h.prototype.clearAutoDismissTimer_ = function() { + clearTimeout(this.autoDismissTimer_), + (this.autoDismissTimer_ = 0); + }), + (h.prototype.handleAnimationTimerEnd_ = function() { + (this.animationTimer_ = 0), + this.adapter_.removeClass(c.cssClasses.OPENING), + this.adapter_.removeClass(c.cssClasses.CLOSING); + }), + (h.prototype.runNextAnimationFrame_ = function(t) { + var e = this; + cancelAnimationFrame(this.animationFrame_), + (this.animationFrame_ = requestAnimationFrame(function() { + (e.animationFrame_ = 0), + clearTimeout(e.animationTimer_), + (e.animationTimer_ = setTimeout(t, 0)); + })); + }), + h); + function h(t) { + var e = s.call(this, o(o({}, h.defaultAdapter), t)) || this; + return ( + (e.isOpen_ = !1), + (e.animationFrame_ = 0), + (e.animationTimer_ = 0), + (e.autoDismissTimer_ = 0), + (e.autoDismissTimeoutMs_ = + c.numbers.DEFAULT_AUTO_DISMISS_TIMEOUT_MS), + (e.closeOnEscape_ = !0), + e + ); + } + (e.MDCSnackbarFoundation = f), (e.default = f); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(83), + u = + ((s = a.MDCFoundation), + r(l, s), + Object.defineProperty(l, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(l, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + setNativeControlChecked: function() {}, + setNativeControlDisabled: function() {}, + setNativeControlAttr: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (l.prototype.setChecked = function(t) { + this.adapter_.setNativeControlChecked(t), + this.updateAriaChecked_(t), + this.updateCheckedStyling_(t); + }), + (l.prototype.setDisabled = function(t) { + this.adapter_.setNativeControlDisabled(t), + t + ? this.adapter_.addClass(c.cssClasses.DISABLED) + : this.adapter_.removeClass(c.cssClasses.DISABLED); + }), + (l.prototype.handleChange = function(t) { + var e = t.target; + this.updateAriaChecked_(e.checked), + this.updateCheckedStyling_(e.checked); + }), + (l.prototype.updateCheckedStyling_ = function(t) { + t + ? this.adapter_.addClass(c.cssClasses.CHECKED) + : this.adapter_.removeClass(c.cssClasses.CHECKED); + }), + (l.prototype.updateAriaChecked_ = function(t) { + this.adapter_.setNativeControlAttr( + c.strings.ARIA_CHECKED_ATTR, + "" + !!t + ); + }), + l); + function l(t) { + return s.call(this, o(o({}, l.defaultAdapter), t)) || this; + } + (e.MDCSwitchFoundation = u), (e.default = u); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.cssClasses = { + CHECKED: "mdc-switch--checked", + DISABLED: "mdc-switch--disabled" + }; + e.strings = { + ARIA_CHECKED_ATTR: "aria-checked", + NATIVE_CONTROL_SELECTOR: ".mdc-switch__native-control", + RIPPLE_SURFACE_SELECTOR: ".mdc-switch__thumb-underlay" + }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(1), + c = n(5), + u = n(2), + l = n(85), + d = o(n(86)), + p = + ((s = a.MDCComponent), + r(_, s), + (_.attachTo = function(t) { + return new _(t); + }), + (_.prototype.initialize = function() { + (this.area_ = this.root_.querySelector( + l.MDCTabScrollerFoundation.strings.AREA_SELECTOR + )), + (this.content_ = this.root_.querySelector( + l.MDCTabScrollerFoundation.strings.CONTENT_SELECTOR + )); + }), + (_.prototype.initialSyncWithDOM = function() { + var e = this; + (this.handleInteraction_ = function() { + return e.foundation_.handleInteraction(); + }), + (this.handleTransitionEnd_ = function(t) { + return e.foundation_.handleTransitionEnd(t); + }), + this.area_.addEventListener( + "wheel", + this.handleInteraction_, + c.applyPassive() + ), + this.area_.addEventListener( + "touchstart", + this.handleInteraction_, + c.applyPassive() + ), + this.area_.addEventListener( + "pointerdown", + this.handleInteraction_, + c.applyPassive() + ), + this.area_.addEventListener( + "mousedown", + this.handleInteraction_, + c.applyPassive() + ), + this.area_.addEventListener( + "keydown", + this.handleInteraction_, + c.applyPassive() + ), + this.content_.addEventListener( + "transitionend", + this.handleTransitionEnd_ + ); + }), + (_.prototype.destroy = function() { + s.prototype.destroy.call(this), + this.area_.removeEventListener( + "wheel", + this.handleInteraction_, + c.applyPassive() + ), + this.area_.removeEventListener( + "touchstart", + this.handleInteraction_, + c.applyPassive() + ), + this.area_.removeEventListener( + "pointerdown", + this.handleInteraction_, + c.applyPassive() + ), + this.area_.removeEventListener( + "mousedown", + this.handleInteraction_, + c.applyPassive() + ), + this.area_.removeEventListener( + "keydown", + this.handleInteraction_, + c.applyPassive() + ), + this.content_.removeEventListener( + "transitionend", + this.handleTransitionEnd_ + ); + }), + (_.prototype.getDefaultFoundation = function() { + var n = this, + t = { + eventTargetMatchesSelector: function(t, e) { + return u.matches(t, e); + }, + addClass: function(t) { + return n.root_.classList.add(t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + addScrollAreaClass: function(t) { + return n.area_.classList.add(t); + }, + setScrollAreaStyleProperty: function(t, e) { + return n.area_.style.setProperty(t, e); + }, + setScrollContentStyleProperty: function(t, e) { + return n.content_.style.setProperty(t, e); + }, + getScrollContentStyleValue: function(t) { + return window + .getComputedStyle(n.content_) + .getPropertyValue(t); + }, + setScrollAreaScrollLeft: function(t) { + return (n.area_.scrollLeft = t); + }, + getScrollAreaScrollLeft: function() { + return n.area_.scrollLeft; + }, + getScrollContentOffsetWidth: function() { + return n.content_.offsetWidth; + }, + getScrollAreaOffsetWidth: function() { + return n.area_.offsetWidth; + }, + computeScrollAreaClientRect: function() { + return n.area_.getBoundingClientRect(); + }, + computeScrollContentClientRect: function() { + return n.content_.getBoundingClientRect(); + }, + computeHorizontalScrollbarHeight: function() { + return d.computeHorizontalScrollbarHeight(document); + } + }; + return new l.MDCTabScrollerFoundation(t); + }), + (_.prototype.getScrollPosition = function() { + return this.foundation_.getScrollPosition(); + }), + (_.prototype.getScrollContentWidth = function() { + return this.content_.offsetWidth; + }), + (_.prototype.incrementScroll = function(t) { + this.foundation_.incrementScroll(t); + }), + (_.prototype.scrollTo = function(t) { + this.foundation_.scrollTo(t); + }), + _); + function _() { + return (null !== s && s.apply(this, arguments)) || this; + } + e.MDCTabScroller = p; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }, + s = + (this && this.__read) || + function(t, e) { + var n = "function" == typeof Symbol && t[Symbol.iterator]; + if (!n) return t; + var i, + r, + o = n.call(t), + s = []; + try { + for (; (void 0 === e || 0 < e--) && !(i = o.next()).done; ) + s.push(i.value); + } catch (t) { + r = { error: t }; + } finally { + try { + i && !i.done && (n = o.return) && n.call(o); + } finally { + if (r) throw r.error; + } + } + return s; + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var a, + c = n(0), + u = n(31), + l = n(153), + d = n(154), + p = n(155), + _ = + ((a = c.MDCFoundation), + r(f, a), + Object.defineProperty(f, "cssClasses", { + get: function() { + return u.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f, "strings", { + get: function() { + return u.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f, "defaultAdapter", { + get: function() { + return { + eventTargetMatchesSelector: function() { + return !1; + }, + addClass: function() {}, + removeClass: function() {}, + addScrollAreaClass: function() {}, + setScrollAreaStyleProperty: function() {}, + setScrollContentStyleProperty: function() {}, + getScrollContentStyleValue: function() { + return ""; + }, + setScrollAreaScrollLeft: function() {}, + getScrollAreaScrollLeft: function() { + return 0; + }, + getScrollContentOffsetWidth: function() { + return 0; + }, + getScrollAreaOffsetWidth: function() { + return 0; + }, + computeScrollAreaClientRect: function() { + return { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 0, + height: 0 + }; + }, + computeScrollContentClientRect: function() { + return { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 0, + height: 0 + }; + }, + computeHorizontalScrollbarHeight: function() { + return 0; + } + }; + }, + enumerable: !0, + configurable: !0 + }), + (f.prototype.init = function() { + var t = this.adapter_.computeHorizontalScrollbarHeight(); + this.adapter_.setScrollAreaStyleProperty( + "margin-bottom", + -t + "px" + ), + this.adapter_.addScrollAreaClass( + f.cssClasses.SCROLL_AREA_SCROLL + ); + }), + (f.prototype.getScrollPosition = function() { + if (this.isRTL_()) return this.computeCurrentScrollPositionRTL_(); + var t = this.calculateCurrentTranslateX_(); + return this.adapter_.getScrollAreaScrollLeft() - t; + }), + (f.prototype.handleInteraction = function() { + this.isAnimating_ && this.stopScrollAnimation_(); + }), + (f.prototype.handleTransitionEnd = function(t) { + var e = t.target; + this.isAnimating_ && + this.adapter_.eventTargetMatchesSelector( + e, + f.strings.CONTENT_SELECTOR + ) && + ((this.isAnimating_ = !1), + this.adapter_.removeClass(f.cssClasses.ANIMATING)); + }), + (f.prototype.incrementScroll = function(t) { + 0 !== t && this.animate_(this.getIncrementScrollOperation_(t)); + }), + (f.prototype.incrementScrollImmediate = function(t) { + if (0 !== t) { + var e = this.getIncrementScrollOperation_(t); + 0 !== e.scrollDelta && + (this.stopScrollAnimation_(), + this.adapter_.setScrollAreaScrollLeft(e.finalScrollPosition)); + } + }), + (f.prototype.scrollTo = function(t) { + if (this.isRTL_()) return this.scrollToRTL_(t); + this.scrollTo_(t); + }), + (f.prototype.getRTLScroller = function() { + return ( + this.rtlScrollerInstance_ || + (this.rtlScrollerInstance_ = this.rtlScrollerFactory_()), + this.rtlScrollerInstance_ + ); + }), + (f.prototype.calculateCurrentTranslateX_ = function() { + var t = this.adapter_.getScrollContentStyleValue("transform"); + if ("none" === t) return 0; + var e = /\((.+?)\)/.exec(t); + if (!e) return 0; + var n = e[1], + i = s(n.split(","), 6), + r = (i[0], i[1], i[2], i[3], i[4]); + return i[5], parseFloat(r); + }), + (f.prototype.clampScrollValue_ = function(t) { + var e = this.calculateScrollEdges_(); + return Math.min(Math.max(e.left, t), e.right); + }), + (f.prototype.computeCurrentScrollPositionRTL_ = function() { + var t = this.calculateCurrentTranslateX_(); + return this.getRTLScroller().getScrollPositionRTL(t); + }), + (f.prototype.calculateScrollEdges_ = function() { + return { + left: 0, + right: + this.adapter_.getScrollContentOffsetWidth() - + this.adapter_.getScrollAreaOffsetWidth() + }; + }), + (f.prototype.scrollTo_ = function(t) { + var e = this.getScrollPosition(), + n = this.clampScrollValue_(t), + i = n - e; + this.animate_({ finalScrollPosition: n, scrollDelta: i }); + }), + (f.prototype.scrollToRTL_ = function(t) { + var e = this.getRTLScroller().scrollToRTL(t); + this.animate_(e); + }), + (f.prototype.getIncrementScrollOperation_ = function(t) { + if (this.isRTL_()) + return this.getRTLScroller().incrementScrollRTL(t); + var e = this.getScrollPosition(), + n = t + e, + i = this.clampScrollValue_(n); + return { finalScrollPosition: i, scrollDelta: i - e }; + }), + (f.prototype.animate_ = function(t) { + var e = this; + 0 !== t.scrollDelta && + (this.stopScrollAnimation_(), + this.adapter_.setScrollAreaScrollLeft(t.finalScrollPosition), + this.adapter_.setScrollContentStyleProperty( + "transform", + "translateX(" + t.scrollDelta + "px)" + ), + this.adapter_.computeScrollAreaClientRect(), + requestAnimationFrame(function() { + e.adapter_.addClass(f.cssClasses.ANIMATING), + e.adapter_.setScrollContentStyleProperty( + "transform", + "none" + ); + }), + (this.isAnimating_ = !0)); + }), + (f.prototype.stopScrollAnimation_ = function() { + this.isAnimating_ = !1; + var t = this.getAnimatingScrollPosition_(); + this.adapter_.removeClass(f.cssClasses.ANIMATING), + this.adapter_.setScrollContentStyleProperty( + "transform", + "translateX(0px)" + ), + this.adapter_.setScrollAreaScrollLeft(t); + }), + (f.prototype.getAnimatingScrollPosition_ = function() { + var t = this.calculateCurrentTranslateX_(), + e = this.adapter_.getScrollAreaScrollLeft(); + return this.isRTL_() + ? this.getRTLScroller().getAnimatingScrollPosition(e, t) + : e - t; + }), + (f.prototype.rtlScrollerFactory_ = function() { + var t = this.adapter_.getScrollAreaScrollLeft(); + this.adapter_.setScrollAreaScrollLeft(t - 1); + var e = this.adapter_.getScrollAreaScrollLeft(); + if (e < 0) + return ( + this.adapter_.setScrollAreaScrollLeft(t), + new d.MDCTabScrollerRTLNegative(this.adapter_) + ); + var n = this.adapter_.computeScrollAreaClientRect(), + i = this.adapter_.computeScrollContentClientRect(), + r = Math.round(i.right - n.right); + return ( + this.adapter_.setScrollAreaScrollLeft(t), + r === e + ? new p.MDCTabScrollerRTLReverse(this.adapter_) + : new l.MDCTabScrollerRTLDefault(this.adapter_) + ); + }), + (f.prototype.isRTL_ = function() { + return ( + "rtl" === this.adapter_.getScrollContentStyleValue("direction") + ); + }), + f); + function f(t) { + var e = a.call(this, o(o({}, f.defaultAdapter), t)) || this; + return (e.isAnimating_ = !1), e; + } + (e.MDCTabScrollerFoundation = _), (e.default = _); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + var r, + o = n(31); + e.computeHorizontalScrollbarHeight = function(t, e) { + if ((void 0 === e && (e = !0), e && void 0 !== r)) return r; + var n = t.createElement("div"); + n.classList.add(o.cssClasses.SCROLL_TEST), t.body.appendChild(n); + var i = n.offsetHeight - n.clientHeight; + return t.body.removeChild(n), e && (r = i), i; + }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + s = + (this && this.__assign) || + function() { + return (s = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + a = n(1), + c = n(3), + u = n(4), + l = n(88), + d = n(33), + p = + ((o = a.MDCComponent), + r(_, o), + (_.attachTo = function(t) { + return new _(t); + }), + (_.prototype.initialize = function(t, e) { + void 0 === t && + (t = function(t, e) { + return new c.MDCRipple(t, e); + }), + void 0 === e && + (e = function(t) { + return new l.MDCTabIndicator(t); + }), + (this.id = this.root_.id); + var n = this.root_.querySelector( + d.MDCTabFoundation.strings.RIPPLE_SELECTOR + ), + i = s(s({}, c.MDCRipple.createAdapter(this)), { + addClass: function(t) { + return n.classList.add(t); + }, + removeClass: function(t) { + return n.classList.remove(t); + }, + updateCssVariable: function(t, e) { + return n.style.setProperty(t, e); + } + }), + r = new u.MDCRippleFoundation(i); + this.ripple_ = t(this.root_, r); + var o = this.root_.querySelector( + d.MDCTabFoundation.strings.TAB_INDICATOR_SELECTOR + ); + (this.tabIndicator_ = e(o)), + (this.content_ = this.root_.querySelector( + d.MDCTabFoundation.strings.CONTENT_SELECTOR + )); + }), + (_.prototype.initialSyncWithDOM = function() { + var t = this; + (this.handleClick_ = function() { + return t.foundation_.handleClick(); + }), + this.listen("click", this.handleClick_); + }), + (_.prototype.destroy = function() { + this.unlisten("click", this.handleClick_), + this.ripple_.destroy(), + o.prototype.destroy.call(this); + }), + (_.prototype.getDefaultFoundation = function() { + var n = this, + t = { + setAttr: function(t, e) { + return n.root_.setAttribute(t, e); + }, + addClass: function(t) { + return n.root_.classList.add(t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + hasClass: function(t) { + return n.root_.classList.contains(t); + }, + activateIndicator: function(t) { + return n.tabIndicator_.activate(t); + }, + deactivateIndicator: function() { + return n.tabIndicator_.deactivate(); + }, + notifyInteracted: function() { + return n.emit( + d.MDCTabFoundation.strings.INTERACTED_EVENT, + { tabId: n.id }, + !0 + ); + }, + getOffsetLeft: function() { + return n.root_.offsetLeft; + }, + getOffsetWidth: function() { + return n.root_.offsetWidth; + }, + getContentOffsetLeft: function() { + return n.content_.offsetLeft; + }, + getContentOffsetWidth: function() { + return n.content_.offsetWidth; + }, + focus: function() { + return n.root_.focus(); + } + }; + return new d.MDCTabFoundation(t); + }), + Object.defineProperty(_.prototype, "active", { + get: function() { + return this.foundation_.isActive(); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(_.prototype, "focusOnActivate", { + set: function(t) { + this.foundation_.setFocusOnActivate(t); + }, + enumerable: !0, + configurable: !0 + }), + (_.prototype.activate = function(t) { + this.foundation_.activate(t); + }), + (_.prototype.deactivate = function() { + this.foundation_.deactivate(); + }), + (_.prototype.computeIndicatorClientRect = function() { + return this.tabIndicator_.computeContentClientRect(); + }), + (_.prototype.computeDimensions = function() { + return this.foundation_.computeDimensions(); + }), + (_.prototype.focus = function() { + this.root_.focus(); + }), + _); + function _() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCTab = p; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(89), + c = n(14), + u = n(91), + l = + ((o = s.MDCComponent), + r(d, o), + (d.attachTo = function(t) { + return new d(t); + }), + (d.prototype.initialize = function() { + this.content_ = this.root_.querySelector( + c.MDCTabIndicatorFoundation.strings.CONTENT_SELECTOR + ); + }), + (d.prototype.computeContentClientRect = function() { + return this.foundation_.computeContentClientRect(); + }), + (d.prototype.getDefaultFoundation = function() { + var n = this, + t = { + addClass: function(t) { + return n.root_.classList.add(t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + computeContentClientRect: function() { + return n.content_.getBoundingClientRect(); + }, + setContentStyleProperty: function(t, e) { + return n.content_.style.setProperty(t, e); + } + }; + return this.root_.classList.contains( + c.MDCTabIndicatorFoundation.cssClasses.FADE + ) + ? new a.MDCFadingTabIndicatorFoundation(t) + : new u.MDCSlidingTabIndicatorFoundation(t); + }), + (d.prototype.activate = function(t) { + this.foundation_.activate(t); + }), + (d.prototype.deactivate = function() { + this.foundation_.deactivate(); + }), + d); + function d() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCTabIndicator = l; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(14), + a = + ((o = s.MDCTabIndicatorFoundation), + r(c, o), + (c.prototype.activate = function() { + this.adapter_.addClass( + s.MDCTabIndicatorFoundation.cssClasses.ACTIVE + ); + }), + (c.prototype.deactivate = function() { + this.adapter_.removeClass( + s.MDCTabIndicatorFoundation.cssClasses.ACTIVE + ); + }), + c); + function c() { + return (null !== o && o.apply(this, arguments)) || this; + } + (e.MDCFadingTabIndicatorFoundation = a), (e.default = a); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.cssClasses = { + ACTIVE: "mdc-tab-indicator--active", + FADE: "mdc-tab-indicator--fade", + NO_TRANSITION: "mdc-tab-indicator--no-transition" + }; + e.strings = { CONTENT_SELECTOR: ".mdc-tab-indicator__content" }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(14), + a = + ((o = s.MDCTabIndicatorFoundation), + r(c, o), + (c.prototype.activate = function(t) { + if (t) { + var e = this.computeContentClientRect(), + n = t.width / e.width, + i = t.left - e.left; + this.adapter_.addClass( + s.MDCTabIndicatorFoundation.cssClasses.NO_TRANSITION + ), + this.adapter_.setContentStyleProperty( + "transform", + "translateX(" + i + "px) scaleX(" + n + ")" + ), + this.computeContentClientRect(), + this.adapter_.removeClass( + s.MDCTabIndicatorFoundation.cssClasses.NO_TRANSITION + ), + this.adapter_.addClass( + s.MDCTabIndicatorFoundation.cssClasses.ACTIVE + ), + this.adapter_.setContentStyleProperty("transform", ""); + } else + this.adapter_.addClass( + s.MDCTabIndicatorFoundation.cssClasses.ACTIVE + ); + }), + (c.prototype.deactivate = function() { + this.adapter_.removeClass( + s.MDCTabIndicatorFoundation.cssClasses.ACTIVE + ); + }), + c); + function c() { + return (null !== o && o.apply(this, arguments)) || this; + } + (e.MDCSlidingTabIndicatorFoundation = a), (e.default = a); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.cssClasses = { ACTIVE: "mdc-tab--active" }; + e.strings = { + ARIA_SELECTED: "aria-selected", + CONTENT_SELECTOR: ".mdc-tab__content", + INTERACTED_EVENT: "MDCTab:interacted", + RIPPLE_SELECTOR: ".mdc-tab__ripple", + TABINDEX: "tabIndex", + TAB_INDICATOR_SELECTOR: ".mdc-tab-indicator" + }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s = n(0), + u = n(94), + a = new Set(); + a.add(u.strings.ARROW_LEFT_KEY), + a.add(u.strings.ARROW_RIGHT_KEY), + a.add(u.strings.END_KEY), + a.add(u.strings.HOME_KEY), + a.add(u.strings.ENTER_KEY), + a.add(u.strings.SPACE_KEY); + var c = new Map(); + c.set(u.numbers.ARROW_LEFT_KEYCODE, u.strings.ARROW_LEFT_KEY), + c.set(u.numbers.ARROW_RIGHT_KEYCODE, u.strings.ARROW_RIGHT_KEY), + c.set(u.numbers.END_KEYCODE, u.strings.END_KEY), + c.set(u.numbers.HOME_KEYCODE, u.strings.HOME_KEY), + c.set(u.numbers.ENTER_KEYCODE, u.strings.ENTER_KEY), + c.set(u.numbers.SPACE_KEYCODE, u.strings.SPACE_KEY); + var l, + d = + ((l = s.MDCFoundation), + r(p, l), + Object.defineProperty(p, "strings", { + get: function() { + return u.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(p, "numbers", { + get: function() { + return u.numbers; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(p, "defaultAdapter", { + get: function() { + return { + scrollTo: function() {}, + incrementScroll: function() {}, + getScrollPosition: function() { + return 0; + }, + getScrollContentWidth: function() { + return 0; + }, + getOffsetWidth: function() { + return 0; + }, + isRTL: function() { + return !1; + }, + setActiveTab: function() {}, + activateTabAtIndex: function() {}, + deactivateTabAtIndex: function() {}, + focusTabAtIndex: function() {}, + getTabIndicatorClientRectAtIndex: function() { + return { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 0, + height: 0 + }; + }, + getTabDimensionsAtIndex: function() { + return { + rootLeft: 0, + rootRight: 0, + contentLeft: 0, + contentRight: 0 + }; + }, + getPreviousActiveTabIndex: function() { + return -1; + }, + getFocusedTabIndex: function() { + return -1; + }, + getIndexOfTabById: function() { + return -1; + }, + getTabListLength: function() { + return 0; + }, + notifyTabActivated: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (p.prototype.setUseAutomaticActivation = function(t) { + this.useAutomaticActivation_ = t; + }), + (p.prototype.activateTab = function(t) { + var e, + n = this.adapter_.getPreviousActiveTabIndex(); + this.indexIsInRange_(t) && + t !== n && + (-1 !== n && + (this.adapter_.deactivateTabAtIndex(n), + (e = this.adapter_.getTabIndicatorClientRectAtIndex(n))), + this.adapter_.activateTabAtIndex(t, e), + this.scrollIntoView(t), + this.adapter_.notifyTabActivated(t)); + }), + (p.prototype.handleKeyDown = function(t) { + var e = this.getKeyFromEvent_(t); + if (void 0 !== e) + if ( + (this.isActivationKey_(e) || t.preventDefault(), + this.useAutomaticActivation_) + ) { + if (this.isActivationKey_(e)) return; + var n = this.determineTargetFromKey_( + this.adapter_.getPreviousActiveTabIndex(), + e + ); + this.adapter_.setActiveTab(n), this.scrollIntoView(n); + } else { + var i = this.adapter_.getFocusedTabIndex(); + this.isActivationKey_(e) + ? this.adapter_.setActiveTab(i) + : ((n = this.determineTargetFromKey_(i, e)), + this.adapter_.focusTabAtIndex(n), + this.scrollIntoView(n)); + } + }), + (p.prototype.handleTabInteraction = function(t) { + this.adapter_.setActiveTab( + this.adapter_.getIndexOfTabById(t.detail.tabId) + ); + }), + (p.prototype.scrollIntoView = function(t) { + if (this.indexIsInRange_(t)) + return 0 === t + ? this.adapter_.scrollTo(0) + : t === this.adapter_.getTabListLength() - 1 + ? this.adapter_.scrollTo( + this.adapter_.getScrollContentWidth() + ) + : this.isRTL_() + ? this.scrollIntoViewRTL_(t) + : void this.scrollIntoView_(t); + }), + (p.prototype.determineTargetFromKey_ = function(t, e) { + var n = this.isRTL_(), + i = this.adapter_.getTabListLength() - 1, + r = e === u.strings.END_KEY, + o = + (e === u.strings.ARROW_LEFT_KEY && !n) || + (e === u.strings.ARROW_RIGHT_KEY && n), + s = + (e === u.strings.ARROW_RIGHT_KEY && !n) || + (e === u.strings.ARROW_LEFT_KEY && n), + a = t; + return ( + r ? (a = i) : o ? (a -= 1) : s ? (a += 1) : (a = 0), + a < 0 ? (a = i) : i < a && (a = 0), + a + ); + }), + (p.prototype.calculateScrollIncrement_ = function(t, e, n, i) { + var r = this.adapter_.getTabDimensionsAtIndex(e), + o = r.contentLeft - n - i, + s = r.contentRight - n - u.numbers.EXTRA_SCROLL_AMOUNT, + a = o + u.numbers.EXTRA_SCROLL_AMOUNT; + return e < t ? Math.min(s, 0) : Math.max(a, 0); + }), + (p.prototype.calculateScrollIncrementRTL_ = function( + t, + e, + n, + i, + r + ) { + var o = this.adapter_.getTabDimensionsAtIndex(e), + s = r - o.contentLeft - n, + a = r - o.contentRight - n - i + u.numbers.EXTRA_SCROLL_AMOUNT, + c = s - u.numbers.EXTRA_SCROLL_AMOUNT; + return t < e ? Math.max(a, 0) : Math.min(c, 0); + }), + (p.prototype.findAdjacentTabIndexClosestToEdge_ = function( + t, + e, + n, + i + ) { + var r = e.rootLeft - n, + o = e.rootRight - n - i, + s = r + o; + return r < 0 || s < 0 ? t - 1 : 0 < o || 0 < s ? t + 1 : -1; + }), + (p.prototype.findAdjacentTabIndexClosestToEdgeRTL_ = function( + t, + e, + n, + i, + r + ) { + var o = r - e.rootLeft - i - n, + s = r - e.rootRight - n, + a = o + s; + return 0 < o || 0 < a ? t + 1 : s < 0 || a < 0 ? t - 1 : -1; + }), + (p.prototype.getKeyFromEvent_ = function(t) { + return a.has(t.key) ? t.key : c.get(t.keyCode); + }), + (p.prototype.isActivationKey_ = function(t) { + return t === u.strings.SPACE_KEY || t === u.strings.ENTER_KEY; + }), + (p.prototype.indexIsInRange_ = function(t) { + return 0 <= t && t < this.adapter_.getTabListLength(); + }), + (p.prototype.isRTL_ = function() { + return this.adapter_.isRTL(); + }), + (p.prototype.scrollIntoView_ = function(t) { + var e = this.adapter_.getScrollPosition(), + n = this.adapter_.getOffsetWidth(), + i = this.adapter_.getTabDimensionsAtIndex(t), + r = this.findAdjacentTabIndexClosestToEdge_(t, i, e, n); + if (this.indexIsInRange_(r)) { + var o = this.calculateScrollIncrement_(t, r, e, n); + this.adapter_.incrementScroll(o); + } + }), + (p.prototype.scrollIntoViewRTL_ = function(t) { + var e = this.adapter_.getScrollPosition(), + n = this.adapter_.getOffsetWidth(), + i = this.adapter_.getTabDimensionsAtIndex(t), + r = this.adapter_.getScrollContentWidth(), + o = this.findAdjacentTabIndexClosestToEdgeRTL_(t, i, e, n, r); + if (this.indexIsInRange_(o)) { + var s = this.calculateScrollIncrementRTL_(t, o, e, n, r); + this.adapter_.incrementScroll(s); + } + }), + p); + function p(t) { + var e = l.call(this, o(o({}, p.defaultAdapter), t)) || this; + return (e.useAutomaticActivation_ = !1), e; + } + (e.MDCTabBarFoundation = d), (e.default = d); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.strings = { + ARROW_LEFT_KEY: "ArrowLeft", + ARROW_RIGHT_KEY: "ArrowRight", + END_KEY: "End", + ENTER_KEY: "Enter", + HOME_KEY: "Home", + SPACE_KEY: "Space", + TAB_ACTIVATED_EVENT: "MDCTabBar:activated", + TAB_SCROLLER_SELECTOR: ".mdc-tab-scroller", + TAB_SELECTOR: ".mdc-tab" + }; + e.numbers = { + ARROW_LEFT_KEYCODE: 37, + ARROW_RIGHT_KEYCODE: 39, + END_KEYCODE: 35, + ENTER_KEYCODE: 13, + EXTRA_SCROLL_AMOUNT: 20, + HOME_KEYCODE: 36, + SPACE_KEYCODE: 32 + }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(34), + c = + ((o = s.MDCComponent), + r(u, o), + (u.attachTo = function(t) { + return new u(t); + }), + Object.defineProperty(u.prototype, "foundation", { + get: function() { + return this.foundation_; + }, + enumerable: !0, + configurable: !0 + }), + (u.prototype.getDefaultFoundation = function() { + var e = this, + t = { + setContent: function(t) { + e.root_.textContent = t; + } + }; + return new a.MDCTextFieldCharacterCounterFoundation(t); + }), + u); + function u() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCTextFieldCharacterCounter = c; + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + var i = { ROOT: "mdc-text-field-character-counter" }, + r = { ROOT_SELECTOR: "." + (e.cssClasses = i).ROOT }; + e.strings = r; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(35), + u = ["mousedown", "touchstart"], + l = ["click", "keydown"], + d = + ((s = a.MDCFoundation), + r(p, s), + Object.defineProperty(p, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(p, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(p, "numbers", { + get: function() { + return c.numbers; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(p.prototype, "shouldAlwaysFloat_", { + get: function() { + var t = this.getNativeInput_().type; + return 0 <= c.ALWAYS_FLOAT_TYPES.indexOf(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(p.prototype, "shouldFloat", { + get: function() { + return ( + this.shouldAlwaysFloat_ || + this.isFocused_ || + !!this.getValue() || + this.isBadInput_() + ); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(p.prototype, "shouldShake", { + get: function() { + return !this.isFocused_ && !this.isValid() && !!this.getValue(); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(p, "defaultAdapter", { + get: function() { + return { + addClass: function() {}, + removeClass: function() {}, + hasClass: function() { + return !0; + }, + registerTextFieldInteractionHandler: function() {}, + deregisterTextFieldInteractionHandler: function() {}, + registerInputInteractionHandler: function() {}, + deregisterInputInteractionHandler: function() {}, + registerValidationAttributeChangeHandler: function() { + return new MutationObserver(function() {}); + }, + deregisterValidationAttributeChangeHandler: function() {}, + getNativeInput: function() { + return null; + }, + isFocused: function() { + return !1; + }, + activateLineRipple: function() {}, + deactivateLineRipple: function() {}, + setLineRippleTransformOrigin: function() {}, + shakeLabel: function() {}, + floatLabel: function() {}, + hasLabel: function() { + return !1; + }, + getLabelWidth: function() { + return 0; + }, + hasOutline: function() { + return !1; + }, + notchOutline: function() {}, + closeOutline: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (p.prototype.init = function() { + var e = this; + this.adapter_.isFocused() + ? this.inputFocusHandler_() + : this.adapter_.hasLabel() && + this.shouldFloat && + (this.notchOutline(!0), + this.adapter_.floatLabel(!0), + this.styleFloating_(!0)), + this.adapter_.registerInputInteractionHandler( + "focus", + this.inputFocusHandler_ + ), + this.adapter_.registerInputInteractionHandler( + "blur", + this.inputBlurHandler_ + ), + this.adapter_.registerInputInteractionHandler( + "input", + this.inputInputHandler_ + ), + u.forEach(function(t) { + e.adapter_.registerInputInteractionHandler( + t, + e.setPointerXOffset_ + ); + }), + l.forEach(function(t) { + e.adapter_.registerTextFieldInteractionHandler( + t, + e.textFieldInteractionHandler_ + ); + }), + (this.validationObserver_ = this.adapter_.registerValidationAttributeChangeHandler( + this.validationAttributeChangeHandler_ + )), + this.setCharacterCounter_(this.getValue().length); + }), + (p.prototype.destroy = function() { + var e = this; + this.adapter_.deregisterInputInteractionHandler( + "focus", + this.inputFocusHandler_ + ), + this.adapter_.deregisterInputInteractionHandler( + "blur", + this.inputBlurHandler_ + ), + this.adapter_.deregisterInputInteractionHandler( + "input", + this.inputInputHandler_ + ), + u.forEach(function(t) { + e.adapter_.deregisterInputInteractionHandler( + t, + e.setPointerXOffset_ + ); + }), + l.forEach(function(t) { + e.adapter_.deregisterTextFieldInteractionHandler( + t, + e.textFieldInteractionHandler_ + ); + }), + this.adapter_.deregisterValidationAttributeChangeHandler( + this.validationObserver_ + ); + }), + (p.prototype.handleTextFieldInteraction = function() { + var t = this.adapter_.getNativeInput(); + (t && t.disabled) || (this.receivedUserInput_ = !0); + }), + (p.prototype.handleValidationAttributeChange = function(t) { + var e = this; + t.some(function(t) { + return ( + -1 < c.VALIDATION_ATTR_WHITELIST.indexOf(t) && + (e.styleValidity_(!0), !0) + ); + }), + -1 < t.indexOf("maxlength") && + this.setCharacterCounter_(this.getValue().length); + }), + (p.prototype.notchOutline = function(t) { + if (this.adapter_.hasOutline()) + if (t) { + var e = this.adapter_.getLabelWidth() * c.numbers.LABEL_SCALE; + this.adapter_.notchOutline(e); + } else this.adapter_.closeOutline(); + }), + (p.prototype.activateFocus = function() { + (this.isFocused_ = !0), + this.styleFocused_(this.isFocused_), + this.adapter_.activateLineRipple(), + this.adapter_.hasLabel() && + (this.notchOutline(this.shouldFloat), + this.adapter_.floatLabel(this.shouldFloat), + this.styleFloating_(this.shouldFloat), + this.adapter_.shakeLabel(this.shouldShake)), + this.helperText_ && this.helperText_.showToScreenReader(); + }), + (p.prototype.setTransformOrigin = function(t) { + var e = t.touches, + n = e ? e[0] : t, + i = n.target.getBoundingClientRect(), + r = n.clientX - i.left; + this.adapter_.setLineRippleTransformOrigin(r); + }), + (p.prototype.handleInput = function() { + this.autoCompleteFocus(), + this.setCharacterCounter_(this.getValue().length); + }), + (p.prototype.autoCompleteFocus = function() { + this.receivedUserInput_ || this.activateFocus(); + }), + (p.prototype.deactivateFocus = function() { + (this.isFocused_ = !1), this.adapter_.deactivateLineRipple(); + var t = this.isValid(); + this.styleValidity_(t), + this.styleFocused_(this.isFocused_), + this.adapter_.hasLabel() && + (this.notchOutline(this.shouldFloat), + this.adapter_.floatLabel(this.shouldFloat), + this.styleFloating_(this.shouldFloat), + this.adapter_.shakeLabel(this.shouldShake)), + this.shouldFloat || (this.receivedUserInput_ = !1); + }), + (p.prototype.getValue = function() { + return this.getNativeInput_().value; + }), + (p.prototype.setValue = function(t) { + this.getValue() !== t && (this.getNativeInput_().value = t), + this.setCharacterCounter_(t.length); + var e = this.isValid(); + this.styleValidity_(e), + this.adapter_.hasLabel() && + (this.notchOutline(this.shouldFloat), + this.adapter_.floatLabel(this.shouldFloat), + this.styleFloating_(this.shouldFloat), + this.adapter_.shakeLabel(this.shouldShake)); + }), + (p.prototype.isValid = function() { + return this.useNativeValidation_ + ? this.isNativeInputValid_() + : this.isValid_; + }), + (p.prototype.setValid = function(t) { + (this.isValid_ = t), this.styleValidity_(t); + var e = !t && !this.isFocused_ && !!this.getValue(); + this.adapter_.hasLabel() && this.adapter_.shakeLabel(e); + }), + (p.prototype.setUseNativeValidation = function(t) { + this.useNativeValidation_ = t; + }), + (p.prototype.isDisabled = function() { + return this.getNativeInput_().disabled; + }), + (p.prototype.setDisabled = function(t) { + (this.getNativeInput_().disabled = t), this.styleDisabled_(t); + }), + (p.prototype.setHelperTextContent = function(t) { + this.helperText_ && this.helperText_.setContent(t); + }), + (p.prototype.setLeadingIconAriaLabel = function(t) { + this.leadingIcon_ && this.leadingIcon_.setAriaLabel(t); + }), + (p.prototype.setLeadingIconContent = function(t) { + this.leadingIcon_ && this.leadingIcon_.setContent(t); + }), + (p.prototype.setTrailingIconAriaLabel = function(t) { + this.trailingIcon_ && this.trailingIcon_.setAriaLabel(t); + }), + (p.prototype.setTrailingIconContent = function(t) { + this.trailingIcon_ && this.trailingIcon_.setContent(t); + }), + (p.prototype.setCharacterCounter_ = function(t) { + if (this.characterCounter_) { + var e = this.getNativeInput_().maxLength; + if (-1 === e) + throw new Error( + "MDCTextFieldFoundation: Expected maxlength html property on text input or textarea." + ); + this.characterCounter_.setCounterValue(t, e); + } + }), + (p.prototype.isBadInput_ = function() { + return this.getNativeInput_().validity.badInput || !1; + }), + (p.prototype.isNativeInputValid_ = function() { + return this.getNativeInput_().validity.valid; + }), + (p.prototype.styleValidity_ = function(t) { + var e = p.cssClasses.INVALID; + t ? this.adapter_.removeClass(e) : this.adapter_.addClass(e), + this.helperText_ && this.helperText_.setValidity(t); + }), + (p.prototype.styleFocused_ = function(t) { + var e = p.cssClasses.FOCUSED; + t ? this.adapter_.addClass(e) : this.adapter_.removeClass(e); + }), + (p.prototype.styleDisabled_ = function(t) { + var e = p.cssClasses, + n = e.DISABLED, + i = e.INVALID; + t + ? (this.adapter_.addClass(n), this.adapter_.removeClass(i)) + : this.adapter_.removeClass(n), + this.leadingIcon_ && this.leadingIcon_.setDisabled(t), + this.trailingIcon_ && this.trailingIcon_.setDisabled(t); + }), + (p.prototype.styleFloating_ = function(t) { + var e = p.cssClasses.LABEL_FLOATING; + t ? this.adapter_.addClass(e) : this.adapter_.removeClass(e); + }), + (p.prototype.getNativeInput_ = function() { + return ( + (this.adapter_ ? this.adapter_.getNativeInput() : null) || { + disabled: !1, + maxLength: -1, + type: "input", + validity: { badInput: !1, valid: !0 }, + value: "" + } + ); + }), + p); + function p(t, e) { + void 0 === e && (e = {}); + var n = s.call(this, o(o({}, p.defaultAdapter), t)) || this; + return ( + (n.isFocused_ = !1), + (n.receivedUserInput_ = !1), + (n.isValid_ = !0), + (n.useNativeValidation_ = !0), + (n.helperText_ = e.helperText), + (n.characterCounter_ = e.characterCounter), + (n.leadingIcon_ = e.leadingIcon), + (n.trailingIcon_ = e.trailingIcon), + (n.inputFocusHandler_ = function() { + return n.activateFocus(); + }), + (n.inputBlurHandler_ = function() { + return n.deactivateFocus(); + }), + (n.inputInputHandler_ = function() { + return n.handleInput(); + }), + (n.setPointerXOffset_ = function(t) { + return n.setTransformOrigin(t); + }), + (n.textFieldInteractionHandler_ = function() { + return n.handleTextFieldInteraction(); + }), + (n.validationAttributeChangeHandler_ = function(t) { + return n.handleValidationAttributeChange(t); + }), + n + ); + } + (e.MDCTextFieldFoundation = d), (e.default = d); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(36), + c = + ((o = s.MDCComponent), + r(u, o), + (u.attachTo = function(t) { + return new u(t); + }), + Object.defineProperty(u.prototype, "foundation", { + get: function() { + return this.foundation_; + }, + enumerable: !0, + configurable: !0 + }), + (u.prototype.getDefaultFoundation = function() { + var n = this, + t = { + addClass: function(t) { + return n.root_.classList.add(t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + hasClass: function(t) { + return n.root_.classList.contains(t); + }, + setAttr: function(t, e) { + return n.root_.setAttribute(t, e); + }, + removeAttr: function(t) { + return n.root_.removeAttribute(t); + }, + setContent: function(t) { + n.root_.textContent = t; + } + }; + return new a.MDCTextFieldHelperTextFoundation(t); + }), + u); + function u() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCTextFieldHelperText = c; + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + var i = { + HELPER_TEXT_PERSISTENT: "mdc-text-field-helper-text--persistent", + HELPER_TEXT_VALIDATION_MSG: + "mdc-text-field-helper-text--validation-msg", + ROOT: "mdc-text-field-helper-text" + }, + r = { + ARIA_HIDDEN: "aria-hidden", + ROLE: "role", + ROOT_SELECTOR: "." + (e.cssClasses = i).ROOT + }; + e.strings = r; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(101), + c = + ((o = s.MDCComponent), + r(u, o), + (u.attachTo = function(t) { + return new u(t); + }), + Object.defineProperty(u.prototype, "foundation", { + get: function() { + return this.foundation_; + }, + enumerable: !0, + configurable: !0 + }), + (u.prototype.getDefaultFoundation = function() { + var n = this, + t = { + getAttr: function(t) { + return n.root_.getAttribute(t); + }, + setAttr: function(t, e) { + return n.root_.setAttribute(t, e); + }, + removeAttr: function(t) { + return n.root_.removeAttribute(t); + }, + setContent: function(t) { + n.root_.textContent = t; + }, + registerInteractionHandler: function(t, e) { + return n.listen(t, e); + }, + deregisterInteractionHandler: function(t, e) { + return n.unlisten(t, e); + }, + notifyIconAction: function() { + return n.emit( + a.MDCTextFieldIconFoundation.strings.ICON_EVENT, + {}, + !0 + ); + } + }; + return new a.MDCTextFieldIconFoundation(t); + }), + u); + function u() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCTextFieldIcon = c; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(0), + c = n(102), + u = ["click", "keydown"], + l = + ((s = a.MDCFoundation), + r(d, s), + Object.defineProperty(d, "strings", { + get: function() { + return c.strings; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d, "cssClasses", { + get: function() { + return c.cssClasses; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d, "defaultAdapter", { + get: function() { + return { + getAttr: function() { + return null; + }, + setAttr: function() {}, + removeAttr: function() {}, + setContent: function() {}, + registerInteractionHandler: function() {}, + deregisterInteractionHandler: function() {}, + notifyIconAction: function() {} + }; + }, + enumerable: !0, + configurable: !0 + }), + (d.prototype.init = function() { + var e = this; + (this.savedTabIndex_ = this.adapter_.getAttr("tabindex")), + u.forEach(function(t) { + e.adapter_.registerInteractionHandler( + t, + e.interactionHandler_ + ); + }); + }), + (d.prototype.destroy = function() { + var e = this; + u.forEach(function(t) { + e.adapter_.deregisterInteractionHandler( + t, + e.interactionHandler_ + ); + }); + }), + (d.prototype.setDisabled = function(t) { + this.savedTabIndex_ && + (t + ? (this.adapter_.setAttr("tabindex", "-1"), + this.adapter_.removeAttr("role")) + : (this.adapter_.setAttr("tabindex", this.savedTabIndex_), + this.adapter_.setAttr("role", c.strings.ICON_ROLE))); + }), + (d.prototype.setAriaLabel = function(t) { + this.adapter_.setAttr("aria-label", t); + }), + (d.prototype.setContent = function(t) { + this.adapter_.setContent(t); + }), + (d.prototype.handleInteraction = function(t) { + var e = "Enter" === t.key || 13 === t.keyCode; + ("click" !== t.type && !e) || + (t.preventDefault(), this.adapter_.notifyIconAction()); + }), + d); + function d(t) { + var e = s.call(this, o(o({}, d.defaultAdapter), t)) || this; + return ( + (e.savedTabIndex_ = null), + (e.interactionHandler_ = function(t) { + return e.handleInteraction(t); + }), + e + ); + } + (e.MDCTextFieldIconFoundation = l), (e.default = l); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }); + e.strings = { ICON_EVENT: "MDCTextField:icon", ICON_ROLE: "button" }; + e.cssClasses = { ROOT: "mdc-text-field__icon" }; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(7), + a = n(37), + c = + ((o = a.MDCTopAppBarFoundation), + r(u, o), + (u.prototype.handleTargetScroll = function() { + this.adapter_.getViewportScrollY() <= 0 + ? this.wasScrolled_ && + (this.adapter_.removeClass(s.cssClasses.FIXED_SCROLLED_CLASS), + (this.wasScrolled_ = !1)) + : this.wasScrolled_ || + (this.adapter_.addClass(s.cssClasses.FIXED_SCROLLED_CLASS), + (this.wasScrolled_ = !0)); + }), + u); + function u() { + var t = (null !== o && o.apply(this, arguments)) || this; + return (t.wasScrolled_ = !1), t; + } + (e.MDCFixedTopAppBarFoundation = c), (e.default = c); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(7), + a = n(38), + c = + ((o = a.MDCTopAppBarBaseFoundation), + r(u, o), + Object.defineProperty(u.prototype, "isCollapsed", { + get: function() { + return this.isCollapsed_; + }, + enumerable: !0, + configurable: !0 + }), + (u.prototype.init = function() { + o.prototype.init.call(this), + 0 < this.adapter_.getTotalActionItems() && + this.adapter_.addClass( + s.cssClasses.SHORT_HAS_ACTION_ITEM_CLASS + ), + this.setAlwaysCollapsed( + this.adapter_.hasClass(s.cssClasses.SHORT_COLLAPSED_CLASS) + ); + }), + (u.prototype.setAlwaysCollapsed = function(t) { + (this.isAlwaysCollapsed_ = !!t), + this.isAlwaysCollapsed_ + ? this.collapse_() + : this.maybeCollapseBar_(); + }), + (u.prototype.getAlwaysCollapsed = function() { + return this.isAlwaysCollapsed_; + }), + (u.prototype.handleTargetScroll = function() { + this.maybeCollapseBar_(); + }), + (u.prototype.maybeCollapseBar_ = function() { + this.isAlwaysCollapsed_ || + (this.adapter_.getViewportScrollY() <= 0 + ? this.isCollapsed_ && this.uncollapse_() + : this.isCollapsed_ || this.collapse_()); + }), + (u.prototype.uncollapse_ = function() { + this.adapter_.removeClass(s.cssClasses.SHORT_COLLAPSED_CLASS), + (this.isCollapsed_ = !1); + }), + (u.prototype.collapse_ = function() { + this.adapter_.addClass(s.cssClasses.SHORT_COLLAPSED_CLASS), + (this.isCollapsed_ = !0); + }), + u); + function u(t) { + var e = o.call(this, t) || this; + return (e.isCollapsed_ = !1), (e.isAlwaysCollapsed_ = !1), e; + } + (e.MDCShortTopAppBarFoundation = c), (e.default = c); + }, + function(t, e, n) { + "use strict"; + var i = + (this && this.__importDefault) || + function(t) { + return t && t.__esModule ? t : { default: t }; + }, + r = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var o = i(n(106)); + e.autoInit = o.default; + var s = r(n(108)); + e.base = s; + var a = r(n(109)); + e.checkbox = a; + var c = r(n(110)); + e.chips = c; + var u = r(n(117)); + e.circularProgress = u; + var l = r(n(119)); + e.dataTable = l; + var d = r(n(121)); + e.dialog = d; + var p = r(n(123)); + e.dom = p; + var _ = r(n(124)); + e.drawer = _; + var f = r(n(126)); + e.floatingLabel = f; + var h = r(n(127)); + e.formField = h; + var C = r(n(129)); + e.iconButton = C; + var y = r(n(131)); + e.lineRipple = y; + var E = r(n(132)); + e.linearProgress = E; + var g = r(n(134)); + e.list = g; + var m = r(n(135)); + e.menuSurface = m; + var A = r(n(136)); + e.menu = A; + var v = r(n(137)); + e.notchedOutline = v; + var b = r(n(138)); + e.radio = b; + var O = r(n(140)); + e.ripple = O; + var T = r(n(141)); + e.select = T; + var I = r(n(145)); + e.slider = I; + var S = r(n(147)); + e.snackbar = S; + var R = r(n(149)); + e.switchControl = R; + var L = r(n(151)); + e.tabBar = L; + var D = r(n(156)); + e.tabIndicator = D; + var P = r(n(157)); + e.tabScroller = P; + var M = r(n(158)); + e.tab = M; + var N = r(n(159)); + e.textField = N; + var w = r(n(164)); + (e.topAppBar = w), + o.default.register("MDCCheckbox", a.MDCCheckbox), + o.default.register("MDCChip", c.MDCChip), + o.default.register("MDCChipSet", c.MDCChipSet), + o.default.register("MDCCircularProgress", u.MDCCircularProgress), + o.default.register("MDCDataTable", l.MDCDataTable), + o.default.register("MDCDialog", d.MDCDialog), + o.default.register("MDCDrawer", _.MDCDrawer), + o.default.register("MDCFloatingLabel", f.MDCFloatingLabel), + o.default.register("MDCFormField", h.MDCFormField), + o.default.register("MDCIconButtonToggle", C.MDCIconButtonToggle), + o.default.register("MDCLineRipple", y.MDCLineRipple), + o.default.register("MDCLinearProgress", E.MDCLinearProgress), + o.default.register("MDCList", g.MDCList), + o.default.register("MDCMenu", A.MDCMenu), + o.default.register("MDCMenuSurface", m.MDCMenuSurface), + o.default.register("MDCNotchedOutline", v.MDCNotchedOutline), + o.default.register("MDCRadio", b.MDCRadio), + o.default.register("MDCRipple", O.MDCRipple), + o.default.register("MDCSelect", T.MDCSelect), + o.default.register("MDCSlider", I.MDCSlider), + o.default.register("MDCSnackbar", S.MDCSnackbar), + o.default.register("MDCSwitch", R.MDCSwitch), + o.default.register("MDCTabBar", L.MDCTabBar), + o.default.register("MDCTextField", N.MDCTextField), + o.default.register("MDCTopAppBar", w.MDCTopAppBar); + }, + function(t, e, n) { + "use strict"; + var d = + (this && this.__values) || + function(t) { + var e = "function" == typeof Symbol && Symbol.iterator, + n = e && t[e], + i = 0; + if (n) return n.call(t); + if (t && "number" == typeof t.length) + return { + next: function() { + return ( + t && i >= t.length && (t = void 0), + { value: t && t[i++], done: !t } + ); + } + }; + throw new TypeError( + e ? "Object is not iterable." : "Symbol.iterator is not defined." + ); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var i = n(107), + p = i.strings.AUTO_INIT_ATTR, + _ = i.strings.AUTO_INIT_STATE_ATTR, + f = i.strings.INITIALIZED_STATE, + h = {}, + r = console.warn.bind(console); + function o(t) { + var e, n; + void 0 === t && (t = document); + var i = [], + r = [].slice.call(t.querySelectorAll("[" + p + "]")); + r = r.filter(function(t) { + return t.getAttribute(_) !== f; + }); + try { + for (var o = d(r), s = o.next(); !s.done; s = o.next()) { + var a = s.value, + c = a.getAttribute(p); + if (!c) + throw new Error( + "(mdc-auto-init) Constructor name must be given." + ); + var u = h[c]; + if ("function" != typeof u) + throw new Error( + "(mdc-auto-init) Could not find constructor in registry for " + + c + ); + var l = u.attachTo(a); + Object.defineProperty(a, c, { + configurable: !0, + enumerable: !1, + value: l, + writable: !1 + }), + i.push(l), + a.setAttribute(_, f); + } + } catch (t) { + e = { error: t }; + } finally { + try { + s && !s.done && (n = o.return) && n.call(o); + } finally { + if (e) throw e.error; + } + } + return ( + (function(t, e, n) { + var i; + void 0 === n && (n = !1), + "function" == typeof CustomEvent + ? (i = new CustomEvent(t, { bubbles: n, detail: e })) + : (i = document.createEvent("CustomEvent")).initCustomEvent( + t, + n, + !1, + e + ), + document.dispatchEvent(i); + })("MDCAutoInit:End", {}), + i + ); + } + ((e.mdcAutoInit = o).register = function(t, e, n) { + if ((void 0 === n && (n = r), "function" != typeof e)) + throw new Error( + "(mdc-auto-init) Invalid Constructor value: " + + e + + ". Expected function." + ); + var i = h[t]; + i && + n( + "(mdc-auto-init) Overriding registration for " + + t + + " with " + + e + + ". Was: " + + i + ), + (h[t] = e); + }), + (o.deregister = function(t) { + delete h[t]; + }), + (o.deregisterAll = function() { + Object.keys(h).forEach(this.deregister, this); + }), + (e.default = o); + }, + function(t, e, n) { + "use strict"; + Object.defineProperty(e, "__esModule", { value: !0 }), + (e.strings = { + AUTO_INIT_ATTR: "data-mdc-auto-init", + AUTO_INIT_STATE_ATTR: "data-mdc-auto-init-state", + INITIALIZED_STATE: "initialized" + }); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), i(e(1)), i(e(0)); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(39)), + i(e(17)), + i(e(41)); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(111)), + i(e(113)), + i(e(114)); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(112)), + i(e(42)); + var r = e(18); + n.trailingActionStrings = r.strings; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(3), + c = n(4), + u = n(18), + l = n(42), + d = + ((o = s.MDCComponent), + r(p, o), + Object.defineProperty(p.prototype, "ripple", { + get: function() { + return this.ripple_; + }, + enumerable: !0, + configurable: !0 + }), + (p.attachTo = function(t) { + return new p(t); + }), + (p.prototype.initialize = function(t) { + void 0 === t && + (t = function(t, e) { + return new a.MDCRipple(t, e); + }); + var e = a.MDCRipple.createAdapter(this); + this.ripple_ = t(this.root_, new c.MDCRippleFoundation(e)); + }), + (p.prototype.initialSyncWithDOM = function() { + var e = this; + (this.handleClick_ = function(t) { + e.foundation_.handleClick(t); + }), + (this.handleKeydown_ = function(t) { + e.foundation_.handleKeydown(t); + }), + this.listen("click", this.handleClick_), + this.listen("keydown", this.handleKeydown_); + }), + (p.prototype.destroy = function() { + this.ripple_.destroy(), + this.unlisten("click", this.handleClick_), + this.unlisten("keydown", this.handleKeydown_), + o.prototype.destroy.call(this); + }), + (p.prototype.getDefaultFoundation = function() { + var n = this, + t = { + focus: function() { + n.root_.focus(); + }, + getAttribute: function(t) { + return n.root_.getAttribute(t); + }, + notifyInteraction: function(t) { + return n.emit( + u.strings.INTERACTION_EVENT, + { trigger: t }, + !0 + ); + }, + notifyNavigation: function(t) { + n.emit(u.strings.NAVIGATION_EVENT, { key: t }, !0); + }, + setAttribute: function(t, e) { + n.root_.setAttribute(t, e); + } + }; + return new l.MDCChipTrailingActionFoundation(t); + }), + (p.prototype.isNavigable = function() { + return this.foundation_.isNavigable(); + }), + (p.prototype.focus = function() { + this.foundation_.focus(); + }), + (p.prototype.removeFocus = function() { + this.foundation_.removeFocus(); + }), + p); + function p() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCChipTrailingAction = d; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(44)), + i(e(19)); + var r = e(8); + (n.chipCssClasses = r.cssClasses), (n.chipStrings = r.strings); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(115)), + i(e(45)); + var r = e(46); + (n.chipSetCssClasses = r.cssClasses), (n.chipSetStrings = r.strings); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(116), + c = n(44), + u = n(19), + l = n(45), + d = u.MDCChipFoundation.strings, + p = d.INTERACTION_EVENT, + _ = d.SELECTION_EVENT, + f = d.REMOVAL_EVENT, + h = d.NAVIGATION_EVENT, + C = l.MDCChipSetFoundation.strings.CHIP_SELECTOR, + y = 0, + E = + ((o = s.MDCComponent), + r(g, o), + (g.attachTo = function(t) { + return new g(t); + }), + Object.defineProperty(g.prototype, "chips", { + get: function() { + return this.chips_.slice(); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(g.prototype, "selectedChipIds", { + get: function() { + return this.foundation_.getSelectedChipIds(); + }, + enumerable: !0, + configurable: !0 + }), + (g.prototype.initialize = function(t) { + void 0 === t && + (t = function(t) { + return new c.MDCChip(t); + }), + (this.chipFactory_ = t), + (this.chips_ = this.instantiateChips_(this.chipFactory_)); + }), + (g.prototype.initialSyncWithDOM = function() { + var e = this; + this.chips_.forEach(function(t) { + t.id && t.selected && e.foundation_.select(t.id); + }), + (this.handleChipInteraction_ = function(t) { + return e.foundation_.handleChipInteraction(t.detail); + }), + (this.handleChipSelection_ = function(t) { + return e.foundation_.handleChipSelection(t.detail); + }), + (this.handleChipRemoval_ = function(t) { + return e.foundation_.handleChipRemoval(t.detail); + }), + (this.handleChipNavigation_ = function(t) { + return e.foundation_.handleChipNavigation(t.detail); + }), + this.listen(p, this.handleChipInteraction_), + this.listen(_, this.handleChipSelection_), + this.listen(f, this.handleChipRemoval_), + this.listen(h, this.handleChipNavigation_); + }), + (g.prototype.destroy = function() { + this.chips_.forEach(function(t) { + t.destroy(); + }), + this.unlisten(p, this.handleChipInteraction_), + this.unlisten(_, this.handleChipSelection_), + this.unlisten(f, this.handleChipRemoval_), + this.unlisten(h, this.handleChipNavigation_), + o.prototype.destroy.call(this); + }), + (g.prototype.addChip = function(t) { + (t.id = t.id || "mdc-chip-" + ++y), + this.chips_.push(this.chipFactory_(t)); + }), + (g.prototype.getDefaultFoundation = function() { + var i = this, + t = { + announceMessage: function(t) { + a.announce(t); + }, + focusChipPrimaryActionAtIndex: function(t) { + i.chips_[t].focusPrimaryAction(); + }, + focusChipTrailingActionAtIndex: function(t) { + i.chips_[t].focusTrailingAction(); + }, + getChipListCount: function() { + return i.chips_.length; + }, + getIndexOfChipById: function(t) { + return i.findChipIndex_(t); + }, + hasClass: function(t) { + return i.root_.classList.contains(t); + }, + isRTL: function() { + return ( + "rtl" === + window + .getComputedStyle(i.root_) + .getPropertyValue("direction") + ); + }, + removeChipAtIndex: function(t) { + 0 <= t && + t < i.chips_.length && + (i.chips_[t].destroy(), + i.chips_[t].remove(), + i.chips_.splice(t, 1)); + }, + removeFocusFromChipAtIndex: function(t) { + i.chips_[t].removeFocus(); + }, + selectChipAtIndex: function(t, e, n) { + 0 <= t && + t < i.chips_.length && + i.chips_[t].setSelectedFromChipSet(e, n); + } + }; + return new l.MDCChipSetFoundation(t); + }), + (g.prototype.instantiateChips_ = function(e) { + return [].slice + .call(this.root_.querySelectorAll(C)) + .map(function(t) { + return (t.id = t.id || "mdc-chip-" + ++y), e(t); + }); + }), + (g.prototype.findChipIndex_ = function(t) { + for (var e = 0; e < this.chips_.length; e++) + if (this.chips_[e].id === t) return e; + return -1; + }), + g); + function g() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCChipSet = E; + }, + function(t, e, n) { + "use strict"; + var i, r; + Object.defineProperty(e, "__esModule", { value: !0 }), + ((r = i = e.AnnouncerPriority || (e.AnnouncerPriority = {})).POLITE = + "polite"), + (r.ASSERTIVE = "assertive"), + (e.announce = function(t, e) { + o.getInstance().say(t, e); + }); + var o = + ((s.getInstance = function() { + return s.instance || (s.instance = new s()), s.instance; + }), + (s.prototype.say = function(t, e) { + void 0 === e && (e = i.POLITE); + var n = this.getLiveRegion(e); + (n.textContent = ""), + setTimeout(function() { + n.textContent = t; + }, 1); + }), + (s.prototype.getLiveRegion = function(t) { + var e = this.liveRegions.get(t); + if (e && document.body.contains(e)) return e; + var n = this.createLiveRegion(t); + return this.liveRegions.set(t, n), n; + }), + (s.prototype.createLiveRegion = function(t) { + var e = document.createElement("div"); + return ( + (e.style.position = "absolute"), + (e.style.top = "-9999px"), + (e.style.left = "-9999px"), + (e.style.height = "1px"), + (e.style.overflow = "hidden"), + e.setAttribute("aria-atomic", "true"), + e.setAttribute("aria-live", t), + document.body.appendChild(e), + e + ); + }), + s); + function s() { + this.liveRegions = new Map(); + } + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(118)), + i(e(48)), + i(e(47)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(47), + c = + ((o = s.MDCComponent), + r(u, o), + (u.prototype.initialize = function() { + this.determinateCircle_ = this.root_.querySelector( + a.MDCCircularProgressFoundation.strings + .DETERMINATE_CIRCLE_SELECTOR + ); + }), + (u.attachTo = function(t) { + return new u(t); + }), + Object.defineProperty(u.prototype, "determinate", { + set: function(t) { + this.foundation_.setDeterminate(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(u.prototype, "progress", { + set: function(t) { + this.foundation_.setProgress(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(u.prototype, "isClosed", { + get: function() { + return this.foundation_.isClosed(); + }, + enumerable: !0, + configurable: !0 + }), + (u.prototype.open = function() { + this.foundation_.open(); + }), + (u.prototype.close = function() { + this.foundation_.close(); + }), + (u.prototype.getDefaultFoundation = function() { + var n = this, + t = { + addClass: function(t) { + return n.root_.classList.add(t); + }, + getDeterminateCircleAttribute: function(t) { + return n.determinateCircle_.getAttribute(t); + }, + hasClass: function(t) { + return n.root_.classList.contains(t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + removeAttribute: function(t) { + return n.root_.removeAttribute(t); + }, + setAttribute: function(t, e) { + return n.root_.setAttribute(t, e); + }, + setDeterminateCircleAttribute: function(t, e) { + return n.determinateCircle_.setAttribute(t, e); + } + }; + return new a.MDCCircularProgressFoundation(t); + }), + u); + function u() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCCircularProgress = c; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(120)), + i(e(49)), + i(e(20)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(39), + c = n(2), + u = n(20), + l = n(49), + d = + ((o = s.MDCComponent), + r(p, o), + (p.attachTo = function(t) { + return new p(t); + }), + (p.prototype.initialize = function(t) { + void 0 === t && + (t = function(t) { + return new a.MDCCheckbox(t); + }), + (this.checkboxFactory_ = t); + }), + (p.prototype.initialSyncWithDOM = function() { + var e = this; + (this.headerRow_ = this.root_.querySelector( + "." + u.cssClasses.HEADER_ROW + )), + (this.handleHeaderRowCheckboxChange_ = function() { + return e.foundation_.handleHeaderRowCheckboxChange(); + }), + this.headerRow_.addEventListener( + "change", + this.handleHeaderRowCheckboxChange_ + ), + (this.content_ = this.root_.querySelector( + "." + u.cssClasses.CONTENT + )), + (this.handleRowCheckboxChange_ = function(t) { + return e.foundation_.handleRowCheckboxChange(t); + }), + this.content_.addEventListener( + "change", + this.handleRowCheckboxChange_ + ), + this.layout(); + }), + (p.prototype.layout = function() { + this.foundation_.layout(); + }), + (p.prototype.getRows = function() { + return this.foundation_.getRows(); + }), + (p.prototype.getSelectedRowIds = function() { + return this.foundation_.getSelectedRowIds(); + }), + (p.prototype.setSelectedRowIds = function(t) { + this.foundation_.setSelectedRowIds(t); + }), + (p.prototype.destroy = function() { + this.headerRow_.removeEventListener( + "change", + this.handleHeaderRowCheckboxChange_ + ), + this.content_.removeEventListener( + "change", + this.handleRowCheckboxChange_ + ), + this.headerRowCheckbox_.destroy(), + this.rowCheckboxList_.forEach(function(t) { + return t.destroy(); + }); + }), + (p.prototype.getDefaultFoundation = function() { + var i = this, + t = { + addClassAtRowIndex: function(t, e) { + i.getRows()[t].classList.add(e); + }, + getRowCount: function() { + return i.getRows().length; + }, + getRowElements: function() { + return [].slice.call( + i.root_.querySelectorAll(u.strings.ROW_SELECTOR) + ); + }, + getRowIdAtIndex: function(t) { + return i + .getRows() + [t].getAttribute(u.strings.DATA_ROW_ID_ATTR); + }, + getRowIndexByChildElement: function(t) { + return i + .getRows() + .indexOf(c.closest(t, u.strings.ROW_SELECTOR)); + }, + getSelectedRowCount: function() { + return i.root_.querySelectorAll( + u.strings.ROW_SELECTED_SELECTOR + ).length; + }, + isCheckboxAtRowIndexChecked: function(t) { + return i.rowCheckboxList_[t].checked; + }, + isHeaderRowCheckboxChecked: function() { + return i.headerRowCheckbox_.checked; + }, + isRowsSelectable: function() { + return !!i.root_.querySelector( + u.strings.ROW_CHECKBOX_SELECTOR + ); + }, + notifyRowSelectionChanged: function(t) { + i.emit( + u.events.ROW_SELECTION_CHANGED, + { + row: i.getRowByIndex_(t.rowIndex), + rowId: i.getRowIdByIndex_(t.rowIndex), + rowIndex: t.rowIndex, + selected: t.selected + }, + !0 + ); + }, + notifySelectedAll: function() { + i.emit(u.events.SELECTED_ALL, {}, !0); + }, + notifyUnselectedAll: function() { + i.emit(u.events.UNSELECTED_ALL, {}, !0); + }, + registerHeaderRowCheckbox: function() { + i.headerRowCheckbox_ && i.headerRowCheckbox_.destroy(); + var t = i.root_.querySelector( + u.strings.HEADER_ROW_CHECKBOX_SELECTOR + ); + i.headerRowCheckbox_ = i.checkboxFactory_(t); + }, + registerRowCheckboxes: function() { + i.rowCheckboxList_ && + i.rowCheckboxList_.forEach(function(t) { + return t.destroy(); + }), + (i.rowCheckboxList_ = []), + i.getRows().forEach(function(t) { + var e = i.checkboxFactory_( + t.querySelector(u.strings.ROW_CHECKBOX_SELECTOR) + ); + i.rowCheckboxList_.push(e); + }); + }, + removeClassAtRowIndex: function(t, e) { + i.getRows()[t].classList.remove(e); + }, + setAttributeAtRowIndex: function(t, e, n) { + i.getRows()[t].setAttribute(e, n); + }, + setHeaderRowCheckboxChecked: function(t) { + i.headerRowCheckbox_.checked = t; + }, + setHeaderRowCheckboxIndeterminate: function(t) { + i.headerRowCheckbox_.indeterminate = t; + }, + setRowCheckboxCheckedAtIndex: function(t, e) { + i.rowCheckboxList_[t].checked = e; + } + }; + return new l.MDCDataTableFoundation(t); + }), + (p.prototype.getRowByIndex_ = function(t) { + return this.getRows()[t]; + }), + (p.prototype.getRowIdByIndex_ = function(t) { + return this.getRowByIndex_(t).getAttribute( + u.strings.DATA_ROW_ID_ATTR + ); + }), + p); + function p() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCDataTable = d; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + var r = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(n, "__esModule", { value: !0 }); + var o = r(e(50)); + (n.util = o), i(e(122)), i(e(52)), i(e(51)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + a = + (this && this.__values) || + function(t) { + var e = "function" == typeof Symbol && Symbol.iterator, + n = e && t[e], + i = 0; + if (n) return n.call(t); + if (t && "number" == typeof t.length) + return { + next: function() { + return ( + t && i >= t.length && (t = void 0), + { value: t && t[i++], done: !t } + ); + } + }; + throw new TypeError( + e + ? "Object is not iterable." + : "Symbol.iterator is not defined." + ); + }, + o = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + c = n(1), + u = n(21), + l = n(2), + d = n(3), + p = n(51), + _ = o(n(50)), + f = p.MDCDialogFoundation.strings, + h = + ((s = c.MDCComponent), + r(C, s), + Object.defineProperty(C.prototype, "isOpen", { + get: function() { + return this.foundation_.isOpen(); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(C.prototype, "escapeKeyAction", { + get: function() { + return this.foundation_.getEscapeKeyAction(); + }, + set: function(t) { + this.foundation_.setEscapeKeyAction(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(C.prototype, "scrimClickAction", { + get: function() { + return this.foundation_.getScrimClickAction(); + }, + set: function(t) { + this.foundation_.setScrimClickAction(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(C.prototype, "autoStackButtons", { + get: function() { + return this.foundation_.getAutoStackButtons(); + }, + set: function(t) { + this.foundation_.setAutoStackButtons(t); + }, + enumerable: !0, + configurable: !0 + }), + (C.attachTo = function(t) { + return new C(t); + }), + (C.prototype.initialize = function(t) { + var e, n; + void 0 === t && + (t = function(t, e) { + return new u.FocusTrap(t, e); + }); + var i = this.root_.querySelector(f.CONTAINER_SELECTOR); + if (!i) + throw new Error( + "Dialog component requires a " + + f.CONTAINER_SELECTOR + + " container element" + ); + (this.container_ = i), + (this.content_ = this.root_.querySelector(f.CONTENT_SELECTOR)), + (this.buttons_ = [].slice.call( + this.root_.querySelectorAll(f.BUTTON_SELECTOR) + )), + (this.defaultButton_ = this.root_.querySelector( + "[" + f.BUTTON_DEFAULT_ATTRIBUTE + "]" + )), + (this.focusTrapFactory_ = t), + (this.buttonRipples_ = []); + try { + for ( + var r = a(this.buttons_), o = r.next(); + !o.done; + o = r.next() + ) { + var s = o.value; + this.buttonRipples_.push(new d.MDCRipple(s)); + } + } catch (t) { + e = { error: t }; + } finally { + try { + o && !o.done && (n = r.return) && n.call(r); + } finally { + if (e) throw e.error; + } + } + }), + (C.prototype.initialSyncWithDOM = function() { + var e = this; + (this.focusTrap_ = _.createFocusTrapInstance( + this.container_, + this.focusTrapFactory_, + this.getInitialFocusEl_() || void 0 + )), + (this.handleClick_ = this.foundation_.handleClick.bind( + this.foundation_ + )), + (this.handleKeydown_ = this.foundation_.handleKeydown.bind( + this.foundation_ + )), + (this.handleDocumentKeydown_ = this.foundation_.handleDocumentKeydown.bind( + this.foundation_ + )), + (this.handleLayout_ = this.layout.bind(this)); + var t = ["resize", "orientationchange"]; + (this.handleOpening_ = function() { + t.forEach(function(t) { + return window.addEventListener(t, e.handleLayout_); + }), + document.addEventListener( + "keydown", + e.handleDocumentKeydown_ + ); + }), + (this.handleClosing_ = function() { + t.forEach(function(t) { + return window.removeEventListener(t, e.handleLayout_); + }), + document.removeEventListener( + "keydown", + e.handleDocumentKeydown_ + ); + }), + this.listen("click", this.handleClick_), + this.listen("keydown", this.handleKeydown_), + this.listen(f.OPENING_EVENT, this.handleOpening_), + this.listen(f.CLOSING_EVENT, this.handleClosing_); + }), + (C.prototype.destroy = function() { + this.unlisten("click", this.handleClick_), + this.unlisten("keydown", this.handleKeydown_), + this.unlisten(f.OPENING_EVENT, this.handleOpening_), + this.unlisten(f.CLOSING_EVENT, this.handleClosing_), + this.handleClosing_(), + this.buttonRipples_.forEach(function(t) { + return t.destroy(); + }), + s.prototype.destroy.call(this); + }), + (C.prototype.layout = function() { + this.foundation_.layout(); + }), + (C.prototype.open = function() { + this.foundation_.open(); + }), + (C.prototype.close = function(t) { + void 0 === t && (t = ""), this.foundation_.close(t); + }), + (C.prototype.getDefaultFoundation = function() { + var e = this, + t = { + addBodyClass: function(t) { + return document.body.classList.add(t); + }, + addClass: function(t) { + return e.root_.classList.add(t); + }, + areButtonsStacked: function() { + return _.areTopsMisaligned(e.buttons_); + }, + clickDefaultButton: function() { + return e.defaultButton_ && e.defaultButton_.click(); + }, + eventTargetMatches: function(t, e) { + return !!t && l.matches(t, e); + }, + getActionFromEvent: function(t) { + if (!t.target) return ""; + var e = l.closest(t.target, "[" + f.ACTION_ATTRIBUTE + "]"); + return e && e.getAttribute(f.ACTION_ATTRIBUTE); + }, + getInitialFocusEl: function() { + return e.getInitialFocusEl_(); + }, + hasClass: function(t) { + return e.root_.classList.contains(t); + }, + isContentScrollable: function() { + return _.isScrollable(e.content_); + }, + notifyClosed: function(t) { + return e.emit(f.CLOSED_EVENT, t ? { action: t } : {}); + }, + notifyClosing: function(t) { + return e.emit(f.CLOSING_EVENT, t ? { action: t } : {}); + }, + notifyOpened: function() { + return e.emit(f.OPENED_EVENT, {}); + }, + notifyOpening: function() { + return e.emit(f.OPENING_EVENT, {}); + }, + releaseFocus: function() { + return e.focusTrap_.releaseFocus(); + }, + removeBodyClass: function(t) { + return document.body.classList.remove(t); + }, + removeClass: function(t) { + return e.root_.classList.remove(t); + }, + reverseButtons: function() { + e.buttons_.reverse(), + e.buttons_.forEach(function(t) { + t.parentElement.appendChild(t); + }); + }, + trapFocus: function() { + return e.focusTrap_.trapFocus(); + } + }; + return new p.MDCDialogFoundation(t); + }), + (C.prototype.getInitialFocusEl_ = function() { + return document.querySelector( + "[" + f.INITIAL_FOCUS_ATTRIBUTE + "]" + ); + }), + C); + function C() { + return (null !== s && s.apply(this, arguments)) || this; + } + e.MDCDialog = h; + }, + function(t, e, n) { + "use strict"; + var i = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var r = i(n(5)); + e.events = r; + var o = i(n(21)); + e.focusTrap = o; + var s = i(n(43)); + e.keyboard = s; + var a = i(n(2)); + e.ponyfill = a; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + var r = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(n, "__esModule", { value: !0 }); + var o = r(e(53)); + (n.util = o), i(e(125)), i(e(54)), i(e(23)), i(e(55)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(1), + c = n(21), + u = n(22), + l = n(10), + d = n(23), + p = n(55), + _ = o(n(53)), + f = d.MDCDismissibleDrawerFoundation.cssClasses, + h = d.MDCDismissibleDrawerFoundation.strings, + C = + ((s = a.MDCComponent), + r(y, s), + (y.attachTo = function(t) { + return new y(t); + }), + Object.defineProperty(y.prototype, "open", { + get: function() { + return this.foundation_.isOpen(); + }, + set: function(t) { + t ? this.foundation_.open() : this.foundation_.close(); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(y.prototype, "list", { + get: function() { + return this.list_; + }, + enumerable: !0, + configurable: !0 + }), + (y.prototype.initialize = function(t, e) { + void 0 === t && + (t = function(t) { + return new c.FocusTrap(t); + }), + void 0 === e && + (e = function(t) { + return new u.MDCList(t); + }); + var n = this.root_.querySelector( + "." + l.MDCListFoundation.cssClasses.ROOT + ); + n && ((this.list_ = e(n)), (this.list_.wrapFocus = !0)), + (this.focusTrapFactory_ = t); + }), + (y.prototype.initialSyncWithDOM = function() { + var e = this, + t = f.MODAL, + n = h.SCRIM_SELECTOR; + (this.scrim_ = this.root_.parentNode.querySelector(n)), + this.scrim_ && + this.root_.classList.contains(t) && + ((this.handleScrimClick_ = function() { + return e.foundation_.handleScrimClick(); + }), + this.scrim_.addEventListener("click", this.handleScrimClick_), + (this.focusTrap_ = _.createFocusTrapInstance( + this.root_, + this.focusTrapFactory_ + ))), + (this.handleKeydown_ = function(t) { + return e.foundation_.handleKeydown(t); + }), + (this.handleTransitionEnd_ = function(t) { + return e.foundation_.handleTransitionEnd(t); + }), + this.listen("keydown", this.handleKeydown_), + this.listen("transitionend", this.handleTransitionEnd_); + }), + (y.prototype.destroy = function() { + this.unlisten("keydown", this.handleKeydown_), + this.unlisten("transitionend", this.handleTransitionEnd_), + this.list_ && this.list_.destroy(); + var t = f.MODAL; + this.scrim_ && + this.handleScrimClick_ && + this.root_.classList.contains(t) && + (this.scrim_.removeEventListener( + "click", + this.handleScrimClick_ + ), + (this.open = !1)); + }), + (y.prototype.getDefaultFoundation = function() { + var e = this, + t = { + addClass: function(t) { + return e.root_.classList.add(t); + }, + removeClass: function(t) { + return e.root_.classList.remove(t); + }, + hasClass: function(t) { + return e.root_.classList.contains(t); + }, + elementHasClass: function(t, e) { + return t.classList.contains(e); + }, + saveFocus: function() { + return (e.previousFocus_ = document.activeElement); + }, + restoreFocus: function() { + var t = e.previousFocus_; + t && + t.focus && + e.root_.contains(document.activeElement) && + t.focus(); + }, + focusActiveNavigationItem: function() { + var t = e.root_.querySelector( + "." + + l.MDCListFoundation.cssClasses.LIST_ITEM_ACTIVATED_CLASS + ); + t && t.focus(); + }, + notifyClose: function() { + return e.emit(h.CLOSE_EVENT, {}, !0); + }, + notifyOpen: function() { + return e.emit(h.OPEN_EVENT, {}, !0); + }, + trapFocus: function() { + return e.focusTrap_.trapFocus(); + }, + releaseFocus: function() { + return e.focusTrap_.releaseFocus(); + } + }, + n = f.DISMISSIBLE, + i = f.MODAL; + if (this.root_.classList.contains(n)) + return new d.MDCDismissibleDrawerFoundation(t); + if (this.root_.classList.contains(i)) + return new p.MDCModalDrawerFoundation(t); + throw new Error( + "MDCDrawer: Failed to instantiate component. Supported variants are " + + n + + " and " + + i + + "." + ); + }), + y); + function y() { + return (null !== s && s.apply(this, arguments)) || this; + } + e.MDCDrawer = C; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(24)), + i(e(56)), + i(e(25)); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(128)), + i(e(58)), + i(e(57)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(57), + c = + ((o = s.MDCComponent), + r(u, o), + (u.attachTo = function(t) { + return new u(t); + }), + Object.defineProperty(u.prototype, "input", { + get: function() { + return this.input_; + }, + set: function(t) { + this.input_ = t; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(u.prototype, "label_", { + get: function() { + var t = a.MDCFormFieldFoundation.strings.LABEL_SELECTOR; + return this.root_.querySelector(t); + }, + enumerable: !0, + configurable: !0 + }), + (u.prototype.getDefaultFoundation = function() { + var n = this, + t = { + activateInputRipple: function() { + n.input_ && n.input_.ripple && n.input_.ripple.activate(); + }, + deactivateInputRipple: function() { + n.input_ && n.input_.ripple && n.input_.ripple.deactivate(); + }, + deregisterInteractionHandler: function(t, e) { + n.label_ && n.label_.removeEventListener(t, e); + }, + registerInteractionHandler: function(t, e) { + n.label_ && n.label_.addEventListener(t, e); + } + }; + return new a.MDCFormFieldFoundation(t); + }), + u); + function u() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCFormField = c; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(130)), + i(e(60)), + i(e(59)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(3), + c = n(59), + u = c.MDCIconButtonToggleFoundation.strings, + l = + ((o = s.MDCComponent), + r(d, o), + (d.attachTo = function(t) { + return new d(t); + }), + (d.prototype.initialSyncWithDOM = function() { + var t = this; + (this.handleClick_ = function() { + return t.foundation_.handleClick(); + }), + this.listen("click", this.handleClick_); + }), + (d.prototype.destroy = function() { + this.unlisten("click", this.handleClick_), + this.ripple_.destroy(), + o.prototype.destroy.call(this); + }), + (d.prototype.getDefaultFoundation = function() { + var n = this, + t = { + addClass: function(t) { + return n.root_.classList.add(t); + }, + hasClass: function(t) { + return n.root_.classList.contains(t); + }, + notifyChange: function(t) { + n.emit(u.CHANGE_EVENT, t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + getAttr: function(t) { + return n.root_.getAttribute(t); + }, + setAttr: function(t, e) { + return n.root_.setAttribute(t, e); + } + }; + return new c.MDCIconButtonToggleFoundation(t); + }), + Object.defineProperty(d.prototype, "ripple", { + get: function() { + return this.ripple_; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d.prototype, "on", { + get: function() { + return this.foundation_.isOn(); + }, + set: function(t) { + this.foundation_.toggle(t); + }, + enumerable: !0, + configurable: !0 + }), + (d.prototype.createRipple_ = function() { + var t = new a.MDCRipple(this.root_); + return (t.unbounded = !0), t; + }), + d); + function d() { + var t = (null !== o && o.apply(this, arguments)) || this; + return (t.ripple_ = t.createRipple_()), t; + } + e.MDCIconButtonToggle = l; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(26)), + i(e(62)), + i(e(61)); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(133)), + i(e(64)), + i(e(63)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(63), + c = + ((o = s.MDCComponent), + r(u, o), + (u.attachTo = function(t) { + return new u(t); + }), + Object.defineProperty(u.prototype, "determinate", { + set: function(t) { + this.foundation_.setDeterminate(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(u.prototype, "progress", { + set: function(t) { + this.foundation_.setProgress(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(u.prototype, "buffer", { + set: function(t) { + this.foundation_.setBuffer(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(u.prototype, "reverse", { + set: function(t) { + this.foundation_.setReverse(t); + }, + enumerable: !0, + configurable: !0 + }), + (u.prototype.open = function() { + this.foundation_.open(); + }), + (u.prototype.close = function() { + this.foundation_.close(); + }), + (u.prototype.getDefaultFoundation = function() { + var n = this, + t = { + addClass: function(t) { + return n.root_.classList.add(t); + }, + forceLayout: function() { + return n.root_.offsetWidth; + }, + setBufferBarStyle: function(t, e) { + n.root_ + .querySelector( + a.MDCLinearProgressFoundation.strings + .BUFFER_BAR_SELECTOR + ) + .style.setProperty(t, e); + }, + setPrimaryBarStyle: function(t, e) { + n.root_ + .querySelector( + a.MDCLinearProgressFoundation.strings + .PRIMARY_BAR_SELECTOR + ) + .style.setProperty(t, e); + }, + hasClass: function(t) { + return n.root_.classList.contains(t); + }, + removeAttribute: function(t) { + n.root_.removeAttribute(t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + setAttribute: function(t, e) { + n.root_.setAttribute(t, e); + } + }; + return new a.MDCLinearProgressFoundation(t); + }), + u); + function u() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCLinearProgress = c; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(22)), + i(e(9)), + i(e(10)); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + var r = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(n, "__esModule", { value: !0 }); + var o = r(e(65)); + (n.util = o), i(e(66)), i(e(6)), i(e(11)); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }); + var r = e(6); + (n.Corner = r.Corner), i(e(67)), i(e(12)), i(e(68)); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(27)), + i(e(28)), + i(e(69)); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(139)), + i(e(71)), + i(e(70)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(1), + c = n(5), + u = n(3), + l = n(4), + d = n(70), + p = + ((s = a.MDCComponent), + r(_, s), + (_.attachTo = function(t) { + return new _(t); + }), + Object.defineProperty(_.prototype, "checked", { + get: function() { + return this.nativeControl_.checked; + }, + set: function(t) { + this.nativeControl_.checked = t; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(_.prototype, "disabled", { + get: function() { + return this.nativeControl_.disabled; + }, + set: function(t) { + this.foundation_.setDisabled(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(_.prototype, "value", { + get: function() { + return this.nativeControl_.value; + }, + set: function(t) { + this.nativeControl_.value = t; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(_.prototype, "ripple", { + get: function() { + return this.ripple_; + }, + enumerable: !0, + configurable: !0 + }), + (_.prototype.destroy = function() { + this.ripple_.destroy(), s.prototype.destroy.call(this); + }), + (_.prototype.getDefaultFoundation = function() { + var e = this, + t = { + addClass: function(t) { + return e.root_.classList.add(t); + }, + removeClass: function(t) { + return e.root_.classList.remove(t); + }, + setNativeControlDisabled: function(t) { + return (e.nativeControl_.disabled = t); + } + }; + return new d.MDCRadioFoundation(t); + }), + (_.prototype.createRipple_ = function() { + var n = this, + t = o(o({}, u.MDCRipple.createAdapter(this)), { + registerInteractionHandler: function(t, e) { + return n.nativeControl_.addEventListener( + t, + e, + c.applyPassive() + ); + }, + deregisterInteractionHandler: function(t, e) { + return n.nativeControl_.removeEventListener( + t, + e, + c.applyPassive() + ); + }, + isSurfaceActive: function() { + return !1; + }, + isUnbounded: function() { + return !0; + } + }); + return new u.MDCRipple(this.root_, new l.MDCRippleFoundation(t)); + }), + Object.defineProperty(_.prototype, "nativeControl_", { + get: function() { + var t = d.MDCRadioFoundation.strings.NATIVE_CONTROL_SELECTOR, + e = this.root_.querySelector(t); + if (!e) + throw new Error( + "Radio component requires a " + t + " element" + ); + return e; + }, + enumerable: !0, + configurable: !0 + }), + _); + function _() { + var t = (null !== s && s.apply(this, arguments)) || this; + return (t.ripple_ = t.createRipple_()), t; + } + e.MDCRadio = p; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + var r = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(n, "__esModule", { value: !0 }); + var o = r(e(16)); + (n.util = o), i(e(3)), i(e(40)), i(e(4)); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(142)), + i(e(29)), + i(e(72)), + i(e(143)), + i(e(144)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }, + s = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var a, + c = n(1), + d = n(24), + p = n(26), + u = s(n(6)), + _ = n(67), + l = s(n(12)), + f = n(27), + h = n(3), + C = n(4), + y = n(29), + E = n(72), + g = n(73), + m = n(76), + A = + ((a = c.MDCComponent), + r(v, a), + (v.attachTo = function(t) { + return new v(t); + }), + (v.prototype.initialize = function(t, e, n, i, r, o) { + if ( + (void 0 === t && + (t = function(t) { + return new d.MDCFloatingLabel(t); + }), + void 0 === e && + (e = function(t) { + return new p.MDCLineRipple(t); + }), + void 0 === n && + (n = function(t) { + return new f.MDCNotchedOutline(t); + }), + void 0 === i && + (i = function(t) { + return new _.MDCMenu(t); + }), + void 0 === r && + (r = function(t) { + return new m.MDCSelectIcon(t); + }), + void 0 === o && + (o = function(t) { + return new g.MDCSelectHelperText(t); + }), + (this.selectAnchor = this.root_.querySelector( + y.strings.SELECT_ANCHOR_SELECTOR + )), + (this.selectedText = this.root_.querySelector( + y.strings.SELECTED_TEXT_SELECTOR + )), + !this.selectedText) + ) + throw new Error( + "MDCSelect: Missing required element: The following selector must be present: '" + + y.strings.SELECTED_TEXT_SELECTOR + + "'" + ); + if (this.selectAnchor.hasAttribute(y.strings.ARIA_CONTROLS)) { + var s = document.getElementById( + this.selectAnchor.getAttribute(y.strings.ARIA_CONTROLS) + ); + s && (this.helperText = o(s)); + } + this.menuSetup(i); + var a = this.root_.querySelector(y.strings.LABEL_SELECTOR); + this.label = a ? t(a) : null; + var c = this.root_.querySelector(y.strings.LINE_RIPPLE_SELECTOR); + this.lineRipple = c ? e(c) : null; + var u = this.root_.querySelector(y.strings.OUTLINE_SELECTOR); + this.outline = u ? n(u) : null; + var l = this.root_.querySelector(y.strings.LEADING_ICON_SELECTOR); + l && (this.leadingIcon = r(l)), + this.root_.classList.contains(y.cssClasses.OUTLINED) || + (this.ripple = this.createRipple()); + }), + (v.prototype.initialSyncWithDOM = function() { + var e = this; + (this.handleChange = function() { + e.foundation_.handleChange(); + }), + (this.handleFocus = function() { + e.foundation_.handleFocus(); + }), + (this.handleBlur = function() { + e.foundation_.handleBlur(); + }), + (this.handleClick = function(t) { + e.selectAnchor.focus(), + e.foundation_.handleClick(e.getNormalizedXCoordinate(t)); + }), + (this.handleKeydown = function(t) { + e.foundation_.handleKeydown(t); + }), + (this.handleMenuItemAction = function(t) { + e.foundation_.handleMenuItemAction(t.detail.index); + }), + (this.handleMenuOpened = function() { + e.foundation_.handleMenuOpened(); + }), + (this.handleMenuClosed = function() { + e.foundation_.handleMenuClosed(); + }), + this.selectAnchor.addEventListener("focus", this.handleFocus), + this.selectAnchor.addEventListener("blur", this.handleBlur), + this.selectAnchor.addEventListener("click", this.handleClick), + this.selectAnchor.addEventListener( + "keydown", + this.handleKeydown + ), + this.menu.listen(u.strings.CLOSED_EVENT, this.handleMenuClosed), + this.menu.listen(u.strings.OPENED_EVENT, this.handleMenuOpened), + this.menu.listen( + l.strings.SELECTED_EVENT, + this.handleMenuItemAction + ), + this.foundation_.init(); + }), + (v.prototype.destroy = function() { + this.selectAnchor.removeEventListener( + "change", + this.handleChange + ), + this.selectAnchor.removeEventListener( + "focus", + this.handleFocus + ), + this.selectAnchor.removeEventListener("blur", this.handleBlur), + this.selectAnchor.removeEventListener( + "keydown", + this.handleKeydown + ), + this.selectAnchor.removeEventListener( + "click", + this.handleClick + ), + this.menu.unlisten( + u.strings.CLOSED_EVENT, + this.handleMenuClosed + ), + this.menu.unlisten( + u.strings.OPENED_EVENT, + this.handleMenuOpened + ), + this.menu.unlisten( + l.strings.SELECTED_EVENT, + this.handleMenuItemAction + ), + this.menu.destroy(), + this.ripple && this.ripple.destroy(), + this.outline && this.outline.destroy(), + this.leadingIcon && this.leadingIcon.destroy(), + this.helperText && this.helperText.destroy(), + a.prototype.destroy.call(this); + }), + Object.defineProperty(v.prototype, "value", { + get: function() { + return this.foundation_.getValue(); + }, + set: function(t) { + this.foundation_.setValue(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(v.prototype, "selectedIndex", { + get: function() { + return this.foundation_.getSelectedIndex(); + }, + set: function(t) { + this.foundation_.setSelectedIndex(t, !0); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(v.prototype, "disabled", { + get: function() { + return this.foundation_.getDisabled(); + }, + set: function(t) { + this.foundation_.setDisabled(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(v.prototype, "leadingIconAriaLabel", { + set: function(t) { + this.foundation_.setLeadingIconAriaLabel(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(v.prototype, "leadingIconContent", { + set: function(t) { + this.foundation_.setLeadingIconContent(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(v.prototype, "helperTextContent", { + set: function(t) { + this.foundation_.setHelperTextContent(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(v.prototype, "valid", { + get: function() { + return this.foundation_.isValid(); + }, + set: function(t) { + this.foundation_.setValid(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(v.prototype, "required", { + get: function() { + return this.foundation_.getRequired(); + }, + set: function(t) { + this.foundation_.setRequired(t); + }, + enumerable: !0, + configurable: !0 + }), + (v.prototype.layout = function() { + this.foundation_.layout(); + }), + (v.prototype.getDefaultFoundation = function() { + var t = o( + o( + o( + o({}, this.getSelectAdapterMethods()), + this.getCommonAdapterMethods() + ), + this.getOutlineAdapterMethods() + ), + this.getLabelAdapterMethods() + ); + return new E.MDCSelectFoundation(t, this.getFoundationMap()); + }), + (v.prototype.menuSetup = function(t) { + (this.menuElement = this.root_.querySelector( + y.strings.MENU_SELECTOR + )), + (this.menu = t(this.menuElement)); + }), + (v.prototype.createRipple = function() { + var n = this, + t = o( + o( + {}, + h.MDCRipple.createAdapter({ root_: this.selectAnchor }) + ), + { + registerInteractionHandler: function(t, e) { + n.selectAnchor.addEventListener(t, e); + }, + deregisterInteractionHandler: function(t, e) { + n.selectAnchor.removeEventListener(t, e); + } + } + ); + return new h.MDCRipple( + this.selectAnchor, + new C.MDCRippleFoundation(t) + ); + }), + (v.prototype.getSelectAdapterMethods = function() { + var i = this; + return { + getSelectedMenuItem: function() { + return i.menuElement.querySelector( + y.strings.SELECTED_ITEM_SELECTOR + ); + }, + getMenuItemAttr: function(t, e) { + return t.getAttribute(e); + }, + setSelectedText: function(t) { + i.selectedText.value = t; + }, + isSelectAnchorFocused: function() { + return document.activeElement === i.selectAnchor; + }, + getSelectAnchorAttr: function(t) { + return i.selectAnchor.getAttribute(t); + }, + setSelectAnchorAttr: function(t, e) { + i.selectAnchor.setAttribute(t, e); + }, + openMenu: function() { + i.menu.open = !0; + }, + closeMenu: function() { + i.menu.open = !1; + }, + getAnchorElement: function() { + return i.root_.querySelector( + y.strings.SELECT_ANCHOR_SELECTOR + ); + }, + setMenuAnchorElement: function(t) { + i.menu.setAnchorElement(t); + }, + setMenuAnchorCorner: function(t) { + i.menu.setAnchorCorner(t); + }, + setMenuWrapFocus: function(t) { + i.menu.wrapFocus = t; + }, + setAttributeAtIndex: function(t, e, n) { + i.menu.items[t].setAttribute(e, n); + }, + removeAttributeAtIndex: function(t, e) { + i.menu.items[t].removeAttribute(e); + }, + focusMenuItemAtIndex: function(t) { + i.menu.items[t].focus(); + }, + getMenuItemCount: function() { + return i.menu.items.length; + }, + getMenuItemValues: function() { + return i.menu.items.map(function(t) { + return t.getAttribute(y.strings.VALUE_ATTR) || ""; + }); + }, + getMenuItemTextAtIndex: function(t) { + return i.menu.items[t].textContent; + }, + addClassAtIndex: function(t, e) { + i.menu.items[t].classList.add(e); + }, + removeClassAtIndex: function(t, e) { + i.menu.items[t].classList.remove(e); + } + }; + }), + (v.prototype.getCommonAdapterMethods = function() { + var n = this; + return { + addClass: function(t) { + n.root_.classList.add(t); + }, + removeClass: function(t) { + n.root_.classList.remove(t); + }, + hasClass: function(t) { + return n.root_.classList.contains(t); + }, + setRippleCenter: function(t) { + n.lineRipple && n.lineRipple.setRippleCenter(t); + }, + activateBottomLine: function() { + n.lineRipple && n.lineRipple.activate(); + }, + deactivateBottomLine: function() { + n.lineRipple && n.lineRipple.deactivate(); + }, + notifyChange: function(t) { + var e = n.selectedIndex; + n.emit(y.strings.CHANGE_EVENT, { value: t, index: e }, !0); + } + }; + }), + (v.prototype.getOutlineAdapterMethods = function() { + var e = this; + return { + hasOutline: function() { + return Boolean(e.outline); + }, + notchOutline: function(t) { + e.outline && e.outline.notch(t); + }, + closeOutline: function() { + e.outline && e.outline.closeNotch(); + } + }; + }), + (v.prototype.getLabelAdapterMethods = function() { + var e = this; + return { + hasLabel: function() { + return !!e.label; + }, + floatLabel: function(t) { + e.label && e.label.float(t); + }, + getLabelWidth: function() { + return e.label ? e.label.getWidth() : 0; + } + }; + }), + (v.prototype.getNormalizedXCoordinate = function(t) { + var e = t.target.getBoundingClientRect(); + return ( + (this.isTouchEvent(t) ? t.touches[0].clientX : t.clientX) - + e.left + ); + }), + (v.prototype.isTouchEvent = function(t) { + return Boolean(t.touches); + }), + (v.prototype.getFoundationMap = function() { + return { + helperText: this.helperText + ? this.helperText.foundation + : void 0, + leadingIcon: this.leadingIcon + ? this.leadingIcon.foundation + : void 0 + }; + }), + v); + function v() { + return (null !== a && a.apply(this, arguments)) || this; + } + e.MDCSelect = A; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(73)), + i(e(74)); + var r = e(75); + (n.helperTextCssClasses = r.cssClasses), + (n.helperTextStrings = r.strings); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(76)), + i(e(77)); + var r = e(78); + n.iconStrings = r.strings; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(146)), + i(e(30)), + i(e(79)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(5), + c = n(30), + u = n(79), + l = + ((o = s.MDCComponent), + r(d, o), + (d.attachTo = function(t) { + return new d(t); + }), + Object.defineProperty(d.prototype, "value", { + get: function() { + return this.foundation_.getValue(); + }, + set: function(t) { + this.foundation_.setValue(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d.prototype, "min", { + get: function() { + return this.foundation_.getMin(); + }, + set: function(t) { + this.foundation_.setMin(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d.prototype, "max", { + get: function() { + return this.foundation_.getMax(); + }, + set: function(t) { + this.foundation_.setMax(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d.prototype, "step", { + get: function() { + return this.foundation_.getStep(); + }, + set: function(t) { + this.foundation_.setStep(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(d.prototype, "disabled", { + get: function() { + return this.foundation_.isDisabled(); + }, + set: function(t) { + this.foundation_.setDisabled(t); + }, + enumerable: !0, + configurable: !0 + }), + (d.prototype.initialize = function() { + (this.thumbContainer_ = this.root_.querySelector( + c.strings.THUMB_CONTAINER_SELECTOR + )), + (this.track_ = this.root_.querySelector( + c.strings.TRACK_SELECTOR + )), + (this.pinValueMarker_ = this.root_.querySelector( + c.strings.PIN_VALUE_MARKER_SELECTOR + )), + (this.trackMarkerContainer_ = this.root_.querySelector( + c.strings.TRACK_MARKER_CONTAINER_SELECTOR + )); + }), + (d.prototype.getDefaultFoundation = function() { + var o = this, + t = { + hasClass: function(t) { + return o.root_.classList.contains(t); + }, + addClass: function(t) { + return o.root_.classList.add(t); + }, + removeClass: function(t) { + return o.root_.classList.remove(t); + }, + getAttribute: function(t) { + return o.root_.getAttribute(t); + }, + setAttribute: function(t, e) { + return o.root_.setAttribute(t, e); + }, + removeAttribute: function(t) { + return o.root_.removeAttribute(t); + }, + computeBoundingRect: function() { + return o.root_.getBoundingClientRect(); + }, + getTabIndex: function() { + return o.root_.tabIndex; + }, + registerInteractionHandler: function(t, e) { + return o.listen(t, e, a.applyPassive()); + }, + deregisterInteractionHandler: function(t, e) { + return o.unlisten(t, e, a.applyPassive()); + }, + registerThumbContainerInteractionHandler: function(t, e) { + o.thumbContainer_.addEventListener(t, e, a.applyPassive()); + }, + deregisterThumbContainerInteractionHandler: function(t, e) { + o.thumbContainer_.removeEventListener( + t, + e, + a.applyPassive() + ); + }, + registerBodyInteractionHandler: function(t, e) { + return document.body.addEventListener(t, e); + }, + deregisterBodyInteractionHandler: function(t, e) { + return document.body.removeEventListener(t, e); + }, + registerResizeHandler: function(t) { + return window.addEventListener("resize", t); + }, + deregisterResizeHandler: function(t) { + return window.removeEventListener("resize", t); + }, + notifyInput: function() { + return o.emit(c.strings.INPUT_EVENT, o); + }, + notifyChange: function() { + return o.emit(c.strings.CHANGE_EVENT, o); + }, + setThumbContainerStyleProperty: function(t, e) { + o.thumbContainer_.style.setProperty(t, e); + }, + setTrackStyleProperty: function(t, e) { + return o.track_.style.setProperty(t, e); + }, + setMarkerValue: function(t) { + return (o.pinValueMarker_.innerText = t.toLocaleString()); + }, + setTrackMarkers: function(t, e, n) { + var i = t.toLocaleString(), + r = + "linear-gradient(to right, currentColor 2px, transparent 0) 0 center / calc((100% - 2px) / ((" + + e.toLocaleString() + + " - " + + n.toLocaleString() + + ") / " + + i + + ")) 100% repeat-x"; + o.trackMarkerContainer_.style.setProperty("background", r); + }, + isRTL: function() { + return "rtl" === getComputedStyle(o.root_).direction; + } + }; + return new u.MDCSliderFoundation(t); + }), + (d.prototype.initialSyncWithDOM = function() { + var t = this.parseFloat_( + this.root_.getAttribute(c.strings.ARIA_VALUENOW), + this.value + ), + e = this.parseFloat_( + this.root_.getAttribute(c.strings.ARIA_VALUEMIN), + this.min + ), + n = this.parseFloat_( + this.root_.getAttribute(c.strings.ARIA_VALUEMAX), + this.max + ); + e >= this.max + ? ((this.max = n), (this.min = e)) + : ((this.min = e), (this.max = n)), + (this.step = this.parseFloat_( + this.root_.getAttribute(c.strings.STEP_DATA_ATTR), + this.step + )), + (this.value = t), + (this.disabled = + this.root_.hasAttribute(c.strings.ARIA_DISABLED) && + "false" !== this.root_.getAttribute(c.strings.ARIA_DISABLED)), + this.foundation_.setupTrackMarker(); + }), + (d.prototype.layout = function() { + this.foundation_.layout(); + }), + (d.prototype.stepUp = function(t) { + void 0 === t && (t = this.step || 1), (this.value += t); + }), + (d.prototype.stepDown = function(t) { + void 0 === t && (t = this.step || 1), (this.value -= t); + }), + (d.prototype.parseFloat_ = function(t, e) { + var n = parseFloat(t); + return "number" == typeof n && isFinite(n) ? n : e; + }), + d); + function d() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCSlider = l; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + var r = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(n, "__esModule", { value: !0 }); + var o = r(e(80)); + (n.util = o), i(e(148)), i(e(13)), i(e(81)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var s, + a = n(1), + c = n(2), + u = n(13), + l = n(81), + d = o(n(80)), + p = u.strings.SURFACE_SELECTOR, + _ = u.strings.LABEL_SELECTOR, + f = u.strings.ACTION_SELECTOR, + h = u.strings.DISMISS_SELECTOR, + C = u.strings.OPENING_EVENT, + y = u.strings.OPENED_EVENT, + E = u.strings.CLOSING_EVENT, + g = u.strings.CLOSED_EVENT, + m = + ((s = a.MDCComponent), + r(A, s), + (A.attachTo = function(t) { + return new A(t); + }), + (A.prototype.initialize = function(t) { + void 0 === t && + (t = function() { + return d.announce; + }), + (this.announce_ = t()); + }), + (A.prototype.initialSyncWithDOM = function() { + var n = this; + (this.surfaceEl_ = this.root_.querySelector(p)), + (this.labelEl_ = this.root_.querySelector(_)), + (this.actionEl_ = this.root_.querySelector(f)), + (this.handleKeyDown_ = function(t) { + return n.foundation_.handleKeyDown(t); + }), + (this.handleSurfaceClick_ = function(t) { + var e = t.target; + n.isActionButton_(e) + ? n.foundation_.handleActionButtonClick(t) + : n.isActionIcon_(e) && + n.foundation_.handleActionIconClick(t); + }), + this.registerKeyDownHandler_(this.handleKeyDown_), + this.registerSurfaceClickHandler_(this.handleSurfaceClick_); + }), + (A.prototype.destroy = function() { + s.prototype.destroy.call(this), + this.deregisterKeyDownHandler_(this.handleKeyDown_), + this.deregisterSurfaceClickHandler_(this.handleSurfaceClick_); + }), + (A.prototype.open = function() { + this.foundation_.open(); + }), + (A.prototype.close = function(t) { + void 0 === t && (t = ""), this.foundation_.close(t); + }), + (A.prototype.getDefaultFoundation = function() { + var e = this, + t = { + addClass: function(t) { + return e.root_.classList.add(t); + }, + announce: function() { + return e.announce_(e.labelEl_); + }, + notifyClosed: function(t) { + return e.emit(g, t ? { reason: t } : {}); + }, + notifyClosing: function(t) { + return e.emit(E, t ? { reason: t } : {}); + }, + notifyOpened: function() { + return e.emit(y, {}); + }, + notifyOpening: function() { + return e.emit(C, {}); + }, + removeClass: function(t) { + return e.root_.classList.remove(t); + } + }; + return new l.MDCSnackbarFoundation(t); + }), + Object.defineProperty(A.prototype, "timeoutMs", { + get: function() { + return this.foundation_.getTimeoutMs(); + }, + set: function(t) { + this.foundation_.setTimeoutMs(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(A.prototype, "closeOnEscape", { + get: function() { + return this.foundation_.getCloseOnEscape(); + }, + set: function(t) { + this.foundation_.setCloseOnEscape(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(A.prototype, "isOpen", { + get: function() { + return this.foundation_.isOpen(); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(A.prototype, "labelText", { + get: function() { + return this.labelEl_.textContent; + }, + set: function(t) { + this.labelEl_.textContent = t; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(A.prototype, "actionButtonText", { + get: function() { + return this.actionEl_.textContent; + }, + set: function(t) { + this.actionEl_.textContent = t; + }, + enumerable: !0, + configurable: !0 + }), + (A.prototype.registerKeyDownHandler_ = function(t) { + this.listen("keydown", t); + }), + (A.prototype.deregisterKeyDownHandler_ = function(t) { + this.unlisten("keydown", t); + }), + (A.prototype.registerSurfaceClickHandler_ = function(t) { + this.surfaceEl_.addEventListener("click", t); + }), + (A.prototype.deregisterSurfaceClickHandler_ = function(t) { + this.surfaceEl_.removeEventListener("click", t); + }), + (A.prototype.isActionButton_ = function(t) { + return Boolean(c.closest(t, f)); + }), + (A.prototype.isActionIcon_ = function(t) { + return Boolean(c.closest(t, h)); + }), + A); + function A() { + return (null !== s && s.apply(this, arguments)) || this; + } + e.MDCSnackbar = m; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(150)), + i(e(83)), + i(e(82)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }, + s = + (this && this.__read) || + function(t, e) { + var n = "function" == typeof Symbol && t[Symbol.iterator]; + if (!n) return t; + var i, + r, + o = n.call(t), + s = []; + try { + for (; (void 0 === e || 0 < e--) && !(i = o.next()).done; ) + s.push(i.value); + } catch (t) { + r = { error: t }; + } finally { + try { + i && !i.done && (n = o.return) && n.call(o); + } finally { + if (r) throw r.error; + } + } + return s; + }, + a = + (this && this.__spread) || + function() { + for (var t = [], e = 0; e < arguments.length; e++) + t = t.concat(s(arguments[e])); + return t; + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var c, + u = n(1), + l = n(5), + d = n(2), + p = n(3), + _ = n(4), + f = n(82), + h = + ((c = u.MDCComponent), + r(C, c), + (C.attachTo = function(t) { + return new C(t); + }), + (C.prototype.destroy = function() { + c.prototype.destroy.call(this), + this.ripple_.destroy(), + this.nativeControl_.removeEventListener( + "change", + this.changeHandler_ + ); + }), + (C.prototype.initialSyncWithDOM = function() { + var i = this; + (this.changeHandler_ = function() { + for (var t, e = [], n = 0; n < arguments.length; n++) + e[n] = arguments[n]; + return (t = i.foundation_).handleChange.apply(t, a(e)); + }), + this.nativeControl_.addEventListener( + "change", + this.changeHandler_ + ), + (this.checked = this.checked); + }), + (C.prototype.getDefaultFoundation = function() { + var n = this, + t = { + addClass: function(t) { + return n.root_.classList.add(t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + setNativeControlChecked: function(t) { + return (n.nativeControl_.checked = t); + }, + setNativeControlDisabled: function(t) { + return (n.nativeControl_.disabled = t); + }, + setNativeControlAttr: function(t, e) { + return n.nativeControl_.setAttribute(t, e); + } + }; + return new f.MDCSwitchFoundation(t); + }), + Object.defineProperty(C.prototype, "ripple", { + get: function() { + return this.ripple_; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(C.prototype, "checked", { + get: function() { + return this.nativeControl_.checked; + }, + set: function(t) { + this.foundation_.setChecked(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(C.prototype, "disabled", { + get: function() { + return this.nativeControl_.disabled; + }, + set: function(t) { + this.foundation_.setDisabled(t); + }, + enumerable: !0, + configurable: !0 + }), + (C.prototype.createRipple_ = function() { + var n = this, + t = f.MDCSwitchFoundation.strings.RIPPLE_SURFACE_SELECTOR, + i = this.root_.querySelector(t), + e = o(o({}, p.MDCRipple.createAdapter(this)), { + addClass: function(t) { + return i.classList.add(t); + }, + computeBoundingRect: function() { + return i.getBoundingClientRect(); + }, + deregisterInteractionHandler: function(t, e) { + n.nativeControl_.removeEventListener( + t, + e, + l.applyPassive() + ); + }, + isSurfaceActive: function() { + return d.matches(n.nativeControl_, ":active"); + }, + isUnbounded: function() { + return !0; + }, + registerInteractionHandler: function(t, e) { + n.nativeControl_.addEventListener(t, e, l.applyPassive()); + }, + removeClass: function(t) { + i.classList.remove(t); + }, + updateCssVariable: function(t, e) { + i.style.setProperty(t, e); + } + }); + return new p.MDCRipple(this.root_, new _.MDCRippleFoundation(e)); + }), + Object.defineProperty(C.prototype, "nativeControl_", { + get: function() { + var t = f.MDCSwitchFoundation.strings.NATIVE_CONTROL_SELECTOR; + return this.root_.querySelector(t); + }, + enumerable: !0, + configurable: !0 + }), + C); + function C() { + var t = (null !== c && c.apply(this, arguments)) || this; + return (t.ripple_ = t.createRipple_()), t; + } + e.MDCSwitch = h; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(152)), + i(e(94)), + i(e(93)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(84), + c = n(87), + u = n(33), + l = n(93), + d = l.MDCTabBarFoundation.strings, + p = 0, + _ = + ((o = s.MDCComponent), + r(f, o), + (f.attachTo = function(t) { + return new f(t); + }), + Object.defineProperty(f.prototype, "focusOnActivate", { + set: function(e) { + this.tabList_.forEach(function(t) { + return (t.focusOnActivate = e); + }); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "useAutomaticActivation", { + set: function(t) { + this.foundation_.setUseAutomaticActivation(t); + }, + enumerable: !0, + configurable: !0 + }), + (f.prototype.initialize = function(t, e) { + void 0 === t && + (t = function(t) { + return new c.MDCTab(t); + }), + void 0 === e && + (e = function(t) { + return new a.MDCTabScroller(t); + }), + (this.tabList_ = this.instantiateTabs_(t)), + (this.tabScroller_ = this.instantiateTabScroller_(e)); + }), + (f.prototype.initialSyncWithDOM = function() { + var e = this; + (this.handleTabInteraction_ = function(t) { + return e.foundation_.handleTabInteraction(t); + }), + (this.handleKeyDown_ = function(t) { + return e.foundation_.handleKeyDown(t); + }), + this.listen( + u.MDCTabFoundation.strings.INTERACTED_EVENT, + this.handleTabInteraction_ + ), + this.listen("keydown", this.handleKeyDown_); + for (var t = 0; t < this.tabList_.length; t++) + if (this.tabList_[t].active) { + this.scrollIntoView(t); + break; + } + }), + (f.prototype.destroy = function() { + o.prototype.destroy.call(this), + this.unlisten( + u.MDCTabFoundation.strings.INTERACTED_EVENT, + this.handleTabInteraction_ + ), + this.unlisten("keydown", this.handleKeyDown_), + this.tabList_.forEach(function(t) { + return t.destroy(); + }), + this.tabScroller_ && this.tabScroller_.destroy(); + }), + (f.prototype.getDefaultFoundation = function() { + var n = this, + t = { + scrollTo: function(t) { + return n.tabScroller_.scrollTo(t); + }, + incrementScroll: function(t) { + return n.tabScroller_.incrementScroll(t); + }, + getScrollPosition: function() { + return n.tabScroller_.getScrollPosition(); + }, + getScrollContentWidth: function() { + return n.tabScroller_.getScrollContentWidth(); + }, + getOffsetWidth: function() { + return n.root_.offsetWidth; + }, + isRTL: function() { + return ( + "rtl" === + window + .getComputedStyle(n.root_) + .getPropertyValue("direction") + ); + }, + setActiveTab: function(t) { + return n.foundation_.activateTab(t); + }, + activateTabAtIndex: function(t, e) { + return n.tabList_[t].activate(e); + }, + deactivateTabAtIndex: function(t) { + return n.tabList_[t].deactivate(); + }, + focusTabAtIndex: function(t) { + return n.tabList_[t].focus(); + }, + getTabIndicatorClientRectAtIndex: function(t) { + return n.tabList_[t].computeIndicatorClientRect(); + }, + getTabDimensionsAtIndex: function(t) { + return n.tabList_[t].computeDimensions(); + }, + getPreviousActiveTabIndex: function() { + for (var t = 0; t < n.tabList_.length; t++) + if (n.tabList_[t].active) return t; + return -1; + }, + getFocusedTabIndex: function() { + var t = n.getTabElements_(), + e = document.activeElement; + return t.indexOf(e); + }, + getIndexOfTabById: function(t) { + for (var e = 0; e < n.tabList_.length; e++) + if (n.tabList_[e].id === t) return e; + return -1; + }, + getTabListLength: function() { + return n.tabList_.length; + }, + notifyTabActivated: function(t) { + return n.emit(d.TAB_ACTIVATED_EVENT, { index: t }, !0); + } + }; + return new l.MDCTabBarFoundation(t); + }), + (f.prototype.activateTab = function(t) { + this.foundation_.activateTab(t); + }), + (f.prototype.scrollIntoView = function(t) { + this.foundation_.scrollIntoView(t); + }), + (f.prototype.getTabElements_ = function() { + return [].slice.call(this.root_.querySelectorAll(d.TAB_SELECTOR)); + }), + (f.prototype.instantiateTabs_ = function(e) { + return this.getTabElements_().map(function(t) { + return (t.id = t.id || "mdc-tab-" + ++p), e(t); + }); + }), + (f.prototype.instantiateTabScroller_ = function(t) { + var e = this.root_.querySelector(d.TAB_SCROLLER_SELECTOR); + return e ? t(e) : null; + }), + f); + function f() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCTabBar = _; + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(32), + a = + ((o = s.MDCTabScrollerRTL), + r(c, o), + (c.prototype.getScrollPositionRTL = function() { + var t = this.adapter_.getScrollAreaScrollLeft(), + e = this.calculateScrollEdges_().right; + return Math.round(e - t); + }), + (c.prototype.scrollToRTL = function(t) { + var e = this.calculateScrollEdges_(), + n = this.adapter_.getScrollAreaScrollLeft(), + i = this.clampScrollValue_(e.right - t); + return { finalScrollPosition: i, scrollDelta: i - n }; + }), + (c.prototype.incrementScrollRTL = function(t) { + var e = this.adapter_.getScrollAreaScrollLeft(), + n = this.clampScrollValue_(e - t); + return { finalScrollPosition: n, scrollDelta: n - e }; + }), + (c.prototype.getAnimatingScrollPosition = function(t) { + return t; + }), + (c.prototype.calculateScrollEdges_ = function() { + return { + left: 0, + right: + this.adapter_.getScrollContentOffsetWidth() - + this.adapter_.getScrollAreaOffsetWidth() + }; + }), + (c.prototype.clampScrollValue_ = function(t) { + var e = this.calculateScrollEdges_(); + return Math.min(Math.max(e.left, t), e.right); + }), + c); + function c() { + return (null !== o && o.apply(this, arguments)) || this; + } + (e.MDCTabScrollerRTLDefault = a), (e.default = a); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(32), + a = + ((o = s.MDCTabScrollerRTL), + r(c, o), + (c.prototype.getScrollPositionRTL = function(t) { + var e = this.adapter_.getScrollAreaScrollLeft(); + return Math.round(t - e); + }), + (c.prototype.scrollToRTL = function(t) { + var e = this.adapter_.getScrollAreaScrollLeft(), + n = this.clampScrollValue_(-t); + return { finalScrollPosition: n, scrollDelta: n - e }; + }), + (c.prototype.incrementScrollRTL = function(t) { + var e = this.adapter_.getScrollAreaScrollLeft(), + n = this.clampScrollValue_(e - t); + return { finalScrollPosition: n, scrollDelta: n - e }; + }), + (c.prototype.getAnimatingScrollPosition = function(t, e) { + return t - e; + }), + (c.prototype.calculateScrollEdges_ = function() { + var t = this.adapter_.getScrollContentOffsetWidth(); + return { + left: this.adapter_.getScrollAreaOffsetWidth() - t, + right: 0 + }; + }), + (c.prototype.clampScrollValue_ = function(t) { + var e = this.calculateScrollEdges_(); + return Math.max(Math.min(e.right, t), e.left); + }), + c); + function c() { + return (null !== o && o.apply(this, arguments)) || this; + } + (e.MDCTabScrollerRTLNegative = a), (e.default = a); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(32), + a = + ((o = s.MDCTabScrollerRTL), + r(c, o), + (c.prototype.getScrollPositionRTL = function(t) { + var e = this.adapter_.getScrollAreaScrollLeft(); + return Math.round(e - t); + }), + (c.prototype.scrollToRTL = function(t) { + var e = this.adapter_.getScrollAreaScrollLeft(), + n = this.clampScrollValue_(t); + return { finalScrollPosition: n, scrollDelta: e - n }; + }), + (c.prototype.incrementScrollRTL = function(t) { + var e = this.adapter_.getScrollAreaScrollLeft(), + n = this.clampScrollValue_(e + t); + return { finalScrollPosition: n, scrollDelta: e - n }; + }), + (c.prototype.getAnimatingScrollPosition = function(t, e) { + return t + e; + }), + (c.prototype.calculateScrollEdges_ = function() { + return { + left: + this.adapter_.getScrollContentOffsetWidth() - + this.adapter_.getScrollAreaOffsetWidth(), + right: 0 + }; + }), + (c.prototype.clampScrollValue_ = function(t) { + var e = this.calculateScrollEdges_(); + return Math.min(Math.max(e.right, t), e.left); + }), + c); + function c() { + return (null !== o && o.apply(this, arguments)) || this; + } + (e.MDCTabScrollerRTLReverse = a), (e.default = a); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(88)), + i(e(90)), + i(e(14)), + i(e(89)), + i(e(91)); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + var r = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(n, "__esModule", { value: !0 }); + var o = r(e(86)); + (n.util = o), i(e(84)), i(e(31)), i(e(85)); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(87)), + i(e(92)), + i(e(33)); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(160)), + i(e(35)), + i(e(97)), + i(e(161)), + i(e(162)), + i(e(163)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }), + o = + (this && this.__assign) || + function() { + return (o = + Object.assign || + function(t) { + for (var e, n = 1, i = arguments.length; n < i; n++) + for (var r in (e = arguments[n])) + Object.prototype.hasOwnProperty.call(e, r) && + (t[r] = e[r]); + return t; + }).apply(this, arguments); + }, + s = + (this && this.__importStar) || + function(t) { + if (t && t.__esModule) return t; + var e = {}; + if (null != t) + for (var n in t) + Object.hasOwnProperty.call(t, n) && (e[n] = t[n]); + return (e.default = t), e; + }; + Object.defineProperty(e, "__esModule", { value: !0 }); + var a, + c = n(1), + u = n(5), + l = s(n(2)), + E = n(24), + g = n(26), + m = n(27), + A = n(3), + d = n(4), + v = n(95), + b = n(34), + O = n(35), + p = n(97), + T = n(98), + I = n(36), + S = n(100), + _ = + ((a = c.MDCComponent), + r(f, a), + (f.attachTo = function(t) { + return new f(t); + }), + (f.prototype.initialize = function(t, e, n, i, r, o, s) { + void 0 === t && + (t = function(t, e) { + return new A.MDCRipple(t, e); + }), + void 0 === e && + (e = function(t) { + return new g.MDCLineRipple(t); + }), + void 0 === n && + (n = function(t) { + return new T.MDCTextFieldHelperText(t); + }), + void 0 === i && + (i = function(t) { + return new v.MDCTextFieldCharacterCounter(t); + }), + void 0 === r && + (r = function(t) { + return new S.MDCTextFieldIcon(t); + }), + void 0 === o && + (o = function(t) { + return new E.MDCFloatingLabel(t); + }), + void 0 === s && + (s = function(t) { + return new m.MDCNotchedOutline(t); + }), + (this.input_ = this.root_.querySelector( + O.strings.INPUT_SELECTOR + )); + var a = this.root_.querySelector(O.strings.LABEL_SELECTOR); + this.label_ = a ? o(a) : null; + var c = this.root_.querySelector(O.strings.LINE_RIPPLE_SELECTOR); + this.lineRipple_ = c ? e(c) : null; + var u = this.root_.querySelector(O.strings.OUTLINE_SELECTOR); + this.outline_ = u ? s(u) : null; + var l = I.MDCTextFieldHelperTextFoundation.strings, + d = this.root_.nextElementSibling, + p = d && d.classList.contains(O.cssClasses.HELPER_LINE), + _ = p && d && d.querySelector(l.ROOT_SELECTOR); + this.helperText_ = _ ? n(_) : null; + var f = b.MDCTextFieldCharacterCounterFoundation.strings, + h = this.root_.querySelector(f.ROOT_SELECTOR); + !h && p && d && (h = d.querySelector(f.ROOT_SELECTOR)), + (this.characterCounter_ = h ? i(h) : null); + var C = this.root_.querySelector(O.strings.LEADING_ICON_SELECTOR); + this.leadingIcon_ = C ? r(C) : null; + var y = this.root_.querySelector( + O.strings.TRAILING_ICON_SELECTOR + ); + (this.trailingIcon_ = y ? r(y) : null), + (this.prefix_ = this.root_.querySelector( + O.strings.PREFIX_SELECTOR + )), + (this.suffix_ = this.root_.querySelector( + O.strings.SUFFIX_SELECTOR + )), + (this.ripple = this.createRipple_(t)); + }), + (f.prototype.destroy = function() { + this.ripple && this.ripple.destroy(), + this.lineRipple_ && this.lineRipple_.destroy(), + this.helperText_ && this.helperText_.destroy(), + this.characterCounter_ && this.characterCounter_.destroy(), + this.leadingIcon_ && this.leadingIcon_.destroy(), + this.trailingIcon_ && this.trailingIcon_.destroy(), + this.label_ && this.label_.destroy(), + this.outline_ && this.outline_.destroy(), + a.prototype.destroy.call(this); + }), + (f.prototype.initialSyncWithDOM = function() { + this.disabled = this.input_.disabled; + }), + Object.defineProperty(f.prototype, "value", { + get: function() { + return this.foundation_.getValue(); + }, + set: function(t) { + this.foundation_.setValue(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "disabled", { + get: function() { + return this.foundation_.isDisabled(); + }, + set: function(t) { + this.foundation_.setDisabled(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "valid", { + get: function() { + return this.foundation_.isValid(); + }, + set: function(t) { + this.foundation_.setValid(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "required", { + get: function() { + return this.input_.required; + }, + set: function(t) { + this.input_.required = t; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "pattern", { + get: function() { + return this.input_.pattern; + }, + set: function(t) { + this.input_.pattern = t; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "minLength", { + get: function() { + return this.input_.minLength; + }, + set: function(t) { + this.input_.minLength = t; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "maxLength", { + get: function() { + return this.input_.maxLength; + }, + set: function(t) { + t < 0 + ? this.input_.removeAttribute("maxLength") + : (this.input_.maxLength = t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "min", { + get: function() { + return this.input_.min; + }, + set: function(t) { + this.input_.min = t; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "max", { + get: function() { + return this.input_.max; + }, + set: function(t) { + this.input_.max = t; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "step", { + get: function() { + return this.input_.step; + }, + set: function(t) { + this.input_.step = t; + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "helperTextContent", { + set: function(t) { + this.foundation_.setHelperTextContent(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "leadingIconAriaLabel", { + set: function(t) { + this.foundation_.setLeadingIconAriaLabel(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "leadingIconContent", { + set: function(t) { + this.foundation_.setLeadingIconContent(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "trailingIconAriaLabel", { + set: function(t) { + this.foundation_.setTrailingIconAriaLabel(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "trailingIconContent", { + set: function(t) { + this.foundation_.setTrailingIconContent(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "useNativeValidation", { + set: function(t) { + this.foundation_.setUseNativeValidation(t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "prefixText", { + get: function() { + return this.prefix_ ? this.prefix_.textContent : null; + }, + set: function(t) { + this.prefix_ && (this.prefix_.textContent = t); + }, + enumerable: !0, + configurable: !0 + }), + Object.defineProperty(f.prototype, "suffixText", { + get: function() { + return this.suffix_ ? this.suffix_.textContent : null; + }, + set: function(t) { + this.suffix_ && (this.suffix_.textContent = t); + }, + enumerable: !0, + configurable: !0 + }), + (f.prototype.focus = function() { + this.input_.focus(); + }), + (f.prototype.layout = function() { + var t = this.foundation_.shouldFloat; + this.foundation_.notchOutline(t); + }), + (f.prototype.getDefaultFoundation = function() { + var t = o( + o( + o( + o( + o({}, this.getRootAdapterMethods_()), + this.getInputAdapterMethods_() + ), + this.getLabelAdapterMethods_() + ), + this.getLineRippleAdapterMethods_() + ), + this.getOutlineAdapterMethods_() + ); + return new p.MDCTextFieldFoundation(t, this.getFoundationMap_()); + }), + (f.prototype.getRootAdapterMethods_ = function() { + var n = this; + return { + addClass: function(t) { + return n.root_.classList.add(t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + hasClass: function(t) { + return n.root_.classList.contains(t); + }, + registerTextFieldInteractionHandler: function(t, e) { + return n.listen(t, e); + }, + deregisterTextFieldInteractionHandler: function(t, e) { + return n.unlisten(t, e); + }, + registerValidationAttributeChangeHandler: function(e) { + var t = new MutationObserver(function(t) { + return e( + (function(t) { + return t + .map(function(t) { + return t.attributeName; + }) + .filter(function(t) { + return t; + }); + })(t) + ); + }); + return t.observe(n.input_, { attributes: !0 }), t; + }, + deregisterValidationAttributeChangeHandler: function(t) { + return t.disconnect(); + } + }; + }), + (f.prototype.getInputAdapterMethods_ = function() { + var n = this; + return { + getNativeInput: function() { + return n.input_; + }, + isFocused: function() { + return document.activeElement === n.input_; + }, + registerInputInteractionHandler: function(t, e) { + return n.input_.addEventListener(t, e, u.applyPassive()); + }, + deregisterInputInteractionHandler: function(t, e) { + return n.input_.removeEventListener(t, e, u.applyPassive()); + } + }; + }), + (f.prototype.getLabelAdapterMethods_ = function() { + var e = this; + return { + floatLabel: function(t) { + return e.label_ && e.label_.float(t); + }, + getLabelWidth: function() { + return e.label_ ? e.label_.getWidth() : 0; + }, + hasLabel: function() { + return Boolean(e.label_); + }, + shakeLabel: function(t) { + return e.label_ && e.label_.shake(t); + } + }; + }), + (f.prototype.getLineRippleAdapterMethods_ = function() { + var e = this; + return { + activateLineRipple: function() { + e.lineRipple_ && e.lineRipple_.activate(); + }, + deactivateLineRipple: function() { + e.lineRipple_ && e.lineRipple_.deactivate(); + }, + setLineRippleTransformOrigin: function(t) { + e.lineRipple_ && e.lineRipple_.setRippleCenter(t); + } + }; + }), + (f.prototype.getOutlineAdapterMethods_ = function() { + var e = this; + return { + closeOutline: function() { + return e.outline_ && e.outline_.closeNotch(); + }, + hasOutline: function() { + return Boolean(e.outline_); + }, + notchOutline: function(t) { + return e.outline_ && e.outline_.notch(t); + } + }; + }), + (f.prototype.getFoundationMap_ = function() { + return { + characterCounter: this.characterCounter_ + ? this.characterCounter_.foundation + : void 0, + helperText: this.helperText_ + ? this.helperText_.foundation + : void 0, + leadingIcon: this.leadingIcon_ + ? this.leadingIcon_.foundation + : void 0, + trailingIcon: this.trailingIcon_ + ? this.trailingIcon_.foundation + : void 0 + }; + }), + (f.prototype.createRipple_ = function(t) { + var n = this, + e = this.root_.classList.contains(O.cssClasses.TEXTAREA), + i = this.root_.classList.contains(O.cssClasses.OUTLINED); + if (e || i) return null; + var r = o(o({}, A.MDCRipple.createAdapter(this)), { + isSurfaceActive: function() { + return l.matches(n.input_, ":active"); + }, + registerInteractionHandler: function(t, e) { + return n.input_.addEventListener(t, e, u.applyPassive()); + }, + deregisterInteractionHandler: function(t, e) { + return n.input_.removeEventListener(t, e, u.applyPassive()); + } + }); + return t(this.root_, new d.MDCRippleFoundation(r)); + }), + f); + function f() { + return (null !== a && a.apply(this, arguments)) || this; + } + e.MDCTextField = _; + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(95)), + i(e(34)); + var r = e(96); + (n.characterCountCssClasses = r.cssClasses), + (n.characterCountStrings = r.strings); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(98)), + i(e(36)); + var r = e(99); + (n.helperTextCssClasses = r.cssClasses), + (n.helperTextStrings = r.strings); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(100)), + i(e(101)); + var r = e(102); + (n.iconCssClasses = r.cssClasses), (n.iconStrings = r.strings); + }, + function(t, n, e) { + "use strict"; + function i(t) { + for (var e in t) n.hasOwnProperty(e) || (n[e] = t[e]); + } + Object.defineProperty(n, "__esModule", { value: !0 }), + i(e(165)), + i(e(7)), + i(e(38)), + i(e(103)), + i(e(104)), + i(e(37)); + }, + function(t, e, n) { + "use strict"; + var i, + r = + (this && this.__extends) || + ((i = function(t, e) { + return (i = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(t, e) { + t.__proto__ = e; + }) || + function(t, e) { + for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); + })(t, e); + }), + function(t, e) { + function n() { + this.constructor = t; + } + i(t, e), + (t.prototype = + null === e + ? Object.create(e) + : ((n.prototype = e.prototype), new n())); + }); + Object.defineProperty(e, "__esModule", { value: !0 }); + var o, + s = n(1), + a = n(3), + c = n(7), + u = n(103), + l = n(104), + d = n(37), + p = + ((o = s.MDCComponent), + r(_, o), + (_.attachTo = function(t) { + return new _(t); + }), + (_.prototype.initialize = function(n) { + void 0 === n && + (n = function(t) { + return a.MDCRipple.attachTo(t); + }), + (this.navIcon_ = this.root_.querySelector( + c.strings.NAVIGATION_ICON_SELECTOR + )); + var t = [].slice.call( + this.root_.querySelectorAll(c.strings.ACTION_ITEM_SELECTOR) + ); + this.navIcon_ && t.push(this.navIcon_), + (this.iconRipples_ = t.map(function(t) { + var e = n(t); + return (e.unbounded = !0), e; + })), + (this.scrollTarget_ = window); + }), + (_.prototype.initialSyncWithDOM = function() { + (this.handleNavigationClick_ = this.foundation_.handleNavigationClick.bind( + this.foundation_ + )), + (this.handleWindowResize_ = this.foundation_.handleWindowResize.bind( + this.foundation_ + )), + (this.handleTargetScroll_ = this.foundation_.handleTargetScroll.bind( + this.foundation_ + )), + this.scrollTarget_.addEventListener( + "scroll", + this.handleTargetScroll_ + ), + this.navIcon_ && + this.navIcon_.addEventListener( + "click", + this.handleNavigationClick_ + ); + var t = this.root_.classList.contains(c.cssClasses.FIXED_CLASS); + this.root_.classList.contains(c.cssClasses.SHORT_CLASS) || + t || + window.addEventListener("resize", this.handleWindowResize_); + }), + (_.prototype.destroy = function() { + this.iconRipples_.forEach(function(t) { + return t.destroy(); + }), + this.scrollTarget_.removeEventListener( + "scroll", + this.handleTargetScroll_ + ), + this.navIcon_ && + this.navIcon_.removeEventListener( + "click", + this.handleNavigationClick_ + ); + var t = this.root_.classList.contains(c.cssClasses.FIXED_CLASS); + this.root_.classList.contains(c.cssClasses.SHORT_CLASS) || + t || + window.removeEventListener("resize", this.handleWindowResize_), + o.prototype.destroy.call(this); + }), + (_.prototype.setScrollTarget = function(t) { + this.scrollTarget_.removeEventListener( + "scroll", + this.handleTargetScroll_ + ), + (this.scrollTarget_ = t), + (this.handleTargetScroll_ = this.foundation_.handleTargetScroll.bind( + this.foundation_ + )), + this.scrollTarget_.addEventListener( + "scroll", + this.handleTargetScroll_ + ); + }), + (_.prototype.getDefaultFoundation = function() { + var n = this, + t = { + hasClass: function(t) { + return n.root_.classList.contains(t); + }, + addClass: function(t) { + return n.root_.classList.add(t); + }, + removeClass: function(t) { + return n.root_.classList.remove(t); + }, + setStyle: function(t, e) { + return n.root_.style.setProperty(t, e); + }, + getTopAppBarHeight: function() { + return n.root_.clientHeight; + }, + notifyNavigationIconClicked: function() { + return n.emit(c.strings.NAVIGATION_EVENT, {}); + }, + getViewportScrollY: function() { + var t = n.scrollTarget_, + e = n.scrollTarget_; + return void 0 !== t.pageYOffset + ? t.pageYOffset + : e.scrollTop; + }, + getTotalActionItems: function() { + return n.root_.querySelectorAll( + c.strings.ACTION_ITEM_SELECTOR + ).length; + } + }; + return this.root_.classList.contains(c.cssClasses.SHORT_CLASS) + ? new l.MDCShortTopAppBarFoundation(t) + : this.root_.classList.contains(c.cssClasses.FIXED_CLASS) + ? new u.MDCFixedTopAppBarFoundation(t) + : new d.MDCTopAppBarFoundation(t); + }), + _); + function _() { + return (null !== o && o.apply(this, arguments)) || this; + } + e.MDCTopAppBar = p; + } + ]), + (r.c = i), + (r.d = function(t, e, n) { + r.o(t, e) || Object.defineProperty(t, e, { enumerable: !0, get: n }); + }), + (r.r = function(t) { + "undefined" != typeof Symbol && + Symbol.toStringTag && + Object.defineProperty(t, Symbol.toStringTag, { value: "Module" }), + Object.defineProperty(t, "__esModule", { value: !0 }); + }), + (r.t = function(e, t) { + if ((1 & t && (e = r(e)), 8 & t)) return e; + if (4 & t && "object" == typeof e && e && e.__esModule) return e; + var n = Object.create(null); + if ( + (r.r(n), + Object.defineProperty(n, "default", { enumerable: !0, value: e }), + 2 & t && "string" != typeof e) + ) + for (var i in e) + r.d( + n, + i, + function(t) { + return e[t]; + }.bind(null, i) + ); + return n; + }), + (r.n = function(t) { + var e = + t && t.__esModule + ? function() { + return t.default; + } + : function() { + return t; + }; + return r.d(e, "a", e), e; + }), + (r.o = function(t, e) { + return Object.prototype.hasOwnProperty.call(t, e); + }), + (r.p = ""), + r((r.s = 105)) + ); + function r(t) { + if (i[t]) return i[t].exports; + var e = (i[t] = { i: t, l: !1, exports: {} }); + return n[t].call(e.exports, e, e.exports, r), (e.l = !0), e.exports; + } + var n, i; +}); diff --git a/backend/src/main/resources/templates/accountdeleted.html b/backend/src/main/resources/templates/accountdeleted.html new file mode 100644 index 000000000..5de4f9d78 --- /dev/null +++ b/backend/src/main/resources/templates/accountdeleted.html @@ -0,0 +1,8 @@ + + + + +Your account was successfully deleted. Please contact ita@chalmers.it if you want to ensure that all information that +the student division has about you is deleted. + + diff --git a/backend/src/main/resources/templates/common-error.html b/backend/src/main/resources/templates/common-error.html new file mode 100644 index 000000000..18576d383 --- /dev/null +++ b/backend/src/main/resources/templates/common-error.html @@ -0,0 +1,20 @@ + + + + + Error Occurred + + +

+

+ An error occurred +

+

+ This error does not have an internal mapping +

+ Status:
+ Message:
+ Original Url:
+
+ + \ No newline at end of file diff --git a/backend/src/main/resources/templates/consent.html b/backend/src/main/resources/templates/consent.html new file mode 100644 index 000000000..22cdcdf16 --- /dev/null +++ b/backend/src/main/resources/templates/consent.html @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + +
+
+ +
+ +
+

+ Gamma - IT account +

+
+ +
+ +
+

+
+ +
+
    +
  • First and last name
  • +
  • Email
  • +
  • Nickname
  • +
  • Preferred language
  • +
  • Cid
  • +
  • Authorities
  • +
  • Groups that you're apart of
  • +
+
+ +
+ +
+ + + +
+ +
+ +
+ +
+
+
+ + + + + + diff --git a/backend/src/main/resources/templates/error-404.html b/backend/src/main/resources/templates/error-404.html new file mode 100644 index 000000000..b4ab3eab7 --- /dev/null +++ b/backend/src/main/resources/templates/error-404.html @@ -0,0 +1,24 @@ + + + + + Not Found + + + +
+

+ 404
+

+

+ This is not the page you're looking for +

+ This image did not load, but imagine it did :) +
+
+ + does not exist +
+
+ + \ No newline at end of file diff --git a/backend/src/main/resources/templates/error-422.html b/backend/src/main/resources/templates/error-422.html new file mode 100644 index 000000000..3eec9670b --- /dev/null +++ b/backend/src/main/resources/templates/error-422.html @@ -0,0 +1,21 @@ + + + + + Unprocessable Entity + + +
+

+ 422
+

+

+ Unprocessable Entity +

+
+ The internal reason was: + +
+
+ + \ No newline at end of file diff --git a/backend/src/main/resources/templates/error-5xx.html b/backend/src/main/resources/templates/error-5xx.html new file mode 100644 index 000000000..c119d50cc --- /dev/null +++ b/backend/src/main/resources/templates/error-5xx.html @@ -0,0 +1,23 @@ + + + + + Internal Server Error + + +
+

+ 500
+

+

+ An internal server error occurred
+

+ +

+ + Click here to add an issue on + This should be a github logo + +
+ + \ No newline at end of file diff --git a/backend/src/main/resources/templates/login.html b/backend/src/main/resources/templates/login.html new file mode 100644 index 000000000..06db57a5c --- /dev/null +++ b/backend/src/main/resources/templates/login.html @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ Invalid cid/email or password. +
+
+
+ You have been logged out. +
+
+ + +
+ +
+

+ Gamma - IT account +

+
+ +
+ +
+

+ Before deciding on authorizing the client, please verify your identity +

+
+ +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+
+
+ + + + + + diff --git a/backend/src/test/java/it/chalmers/gamma/ArchitectureTest.java b/backend/src/test/java/it/chalmers/gamma/ArchitectureTest.java new file mode 100644 index 000000000..727a65dff --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/ArchitectureTest.java @@ -0,0 +1,38 @@ +package it.chalmers.gamma; + +import com.tngtech.archunit.core.domain.JavaClasses; + +public class ArchitectureTest { + + private static JavaClasses classes; +// +// @BeforeAll +// public static void setUp() { +// classes = new ClassFileImporter() +// .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) +// .importPackages("it.chalmers.gamma"); +// } +// +// @Test +// public void testArchitecture() { +// } +// +// //Add test checking that primary adapter only uses *Facade. +// +// @Test +// public void layersShouldBeFreeOfCycles() { +// SliceRule rule = SlicesRuleDefinition.slices() +// .matching("it.chalmers.gamma.(*)..") +// .should().beFreeOfCycles(); +// rule.check(classes); +// } +// +// @Test +// public void adaptersShouldNotDependOnEachOther() { +// SliceRule rule = SlicesRuleDefinition.slices() +// .matching("it.chalmers.gamma.adapter.(**)") +// .should().notDependOnEachOther(); +// rule.check(classes); +// } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/primary/AbstractApiControllerTest.java b/backend/src/test/java/it/chalmers/gamma/adapter/primary/AbstractApiControllerTest.java new file mode 100644 index 000000000..62eecdfe6 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/primary/AbstractApiControllerTest.java @@ -0,0 +1,39 @@ +package it.chalmers.gamma.adapter.primary; + +import io.restassured.RestAssured; +import io.restassured.config.CsrfConfig; +import io.restassured.config.SessionConfig; +import io.restassured.filter.log.RequestLoggingFilter; +import io.restassured.filter.log.ResponseLoggingFilter; +import io.restassured.parsing.Parser; +import io.restassured.spi.AuthFilter; +import it.chalmers.gamma.GammaApplication; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@SpringBootTest(classes = GammaApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public abstract class AbstractApiControllerTest { + + @LocalServerPort + protected int port; + + @BeforeEach + public void setUp() { + RestAssured.port = port; + RestAssured.defaultParser = Parser.JSON; + RestAssured.config = RestAssured.config() + .sessionConfig(new SessionConfig() + .sessionIdName("gamma") + ) + .csrfConfig(new CsrfConfig("/api/login") + .csrfHeaderName("X-XSRF-TOKEN") + ); + RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/primary/ApiTest.java b/backend/src/test/java/it/chalmers/gamma/adapter/primary/ApiTest.java new file mode 100644 index 000000000..6d6ae6575 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/primary/ApiTest.java @@ -0,0 +1,4 @@ +package it.chalmers.gamma.adapter.primary; + +public @interface ApiTest { +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/AbstractExternalApiControllerTest.java b/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/AbstractExternalApiControllerTest.java new file mode 100644 index 000000000..edb450667 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/AbstractExternalApiControllerTest.java @@ -0,0 +1,15 @@ +package it.chalmers.gamma.adapter.primary.external; + +import io.restassured.spi.AuthFilter; +import it.chalmers.gamma.adapter.primary.AbstractApiControllerTest; + +public class AbstractExternalApiControllerTest extends AbstractApiControllerTest { + + protected AuthFilter apiAuthFilter(String apiKey) { + return (requestSpec, responseSpec, context) -> { + requestSpec.header("Authorization", "pre-shared " + apiKey); + return context.next(requestSpec, responseSpec); + }; + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/AllowListV1ApiControllerTest.java b/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/AllowListV1ApiControllerTest.java new file mode 100644 index 000000000..00d90da41 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/AllowListV1ApiControllerTest.java @@ -0,0 +1,4 @@ +package it.chalmers.gamma.adapter.primary.external; + +public class AllowListV1ApiControllerTest { +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/ClientV1ApiControllerTest.java b/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/ClientV1ApiControllerTest.java new file mode 100644 index 000000000..00fe22308 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/ClientV1ApiControllerTest.java @@ -0,0 +1,4 @@ +package it.chalmers.gamma.adapter.primary.external; + +public class ClientV1ApiControllerTest { +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/GoldappsV1ApiControllerTest.java b/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/GoldappsV1ApiControllerTest.java new file mode 100644 index 000000000..2e8deb181 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/GoldappsV1ApiControllerTest.java @@ -0,0 +1,4 @@ +package it.chalmers.gamma.adapter.primary.external; + +public class GoldappsV1ApiControllerTest { +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/InfoV1ApiControllerTest.java b/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/InfoV1ApiControllerTest.java new file mode 100644 index 000000000..bb9d1347b --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/primary/external/InfoV1ApiControllerTest.java @@ -0,0 +1,98 @@ +package it.chalmers.gamma.adapter.primary.external; + +import it.chalmers.gamma.adapter.primary.AbstractApiControllerTest; +import it.chalmers.gamma.adapter.primary.internal.AbstractInternalApiControllerTest; +import it.chalmers.gamma.app.apikey.domain.ApiKeyRepository; +import it.chalmers.gamma.app.settings.domain.SettingsRepository; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupType; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("test-with-mock") +class InfoV1ApiControllerTest extends AbstractExternalApiControllerTest { + + @Autowired + private SettingsRepository settingsRepository; + + @Autowired + private ApiKeyRepository apiKeyRepository; + + + private static final String expected = """ + { + "groups": [ + { + "id": "2abe2264-fd61-4899-ba46-851279d85229", + "version": 1, + "name": "digit2023", + "prettyName": "digIT2023", + "groupMembers": [], + "superGroup": { + "id": "aed27030-ad90-4526-855c-1e909b1dcecb", + "version": 1, + "name": "digit", + "prettyName": "digIT", + "type": "committee", + "svDescription": "", + "enDescription": "" + } + }, + { + "id": "1ed91274-13c8-4d6d-ab75-37c9d732b51b", + "version": 1, + "name": "prit2023", + "prettyName": "P.R.I.T.2023", + "groupMembers": [], + "superGroup": { + "id": "b3bcbbcc-0b93-4c41-a3c7-1792448c6fc1", + "version": 1, + "name": "prit", + "prettyName": "P.R.I.T.", + "type": "committee", + "svDescription": "", + "enDescription": "" + } + } + ] + } + """; + + @Test + public void groups() { + System.out.println(apiKeyRepository.getAll()); + + settingsRepository.setSettings( + settings -> settings.withInfoSuperGroupTypes( + List.of(new SuperGroupType("committee")) + )); + + var response = given() + .filter(apiAuthFilter("INFO-super-secret-code")) + .and() + .get("/api/external/info/v1/groups") + .andReturn(); + + assertThat(response.print()).isEqualTo(expected.replaceAll("\\s+","")); + + given() + .filter(apiAuthFilter("bad-key")) + .and() + .get("/api/external/info/v1/groups") + .then() + .statusCode(401); + + given() + .filter(apiAuthFilter("ALLOW-LIST-super-secret-code")) + .and() + .get("/api/external/info/v1/groups") + .then() + .statusCode(403); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/primary/internal/AbstractInternalApiControllerTest.java b/backend/src/test/java/it/chalmers/gamma/adapter/primary/internal/AbstractInternalApiControllerTest.java new file mode 100644 index 000000000..fc29b0560 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/primary/internal/AbstractInternalApiControllerTest.java @@ -0,0 +1,33 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import io.restassured.authentication.FormAuthConfig; +import io.restassured.filter.session.SessionFilter; +import io.restassured.specification.RequestSpecification; +import it.chalmers.gamma.adapter.primary.AbstractApiControllerTest; + +import static io.restassured.RestAssured.given; + + +public class AbstractInternalApiControllerTest extends AbstractApiControllerTest { + + protected RequestSpecification givenAdminUser() { + return givenAdminUser(null); + } + + protected RequestSpecification givenAdminUser(SessionFilter sessionFilter) { + FormAuthConfig formAuthConfig = new FormAuthConfig("/api/login", "username", "password"); + var request = given() + .auth().form("admin", "password", formAuthConfig); + + if(sessionFilter != null) { + request.filter(sessionFilter); + } + + return request; + } + + protected RequestSpecification givenSignedInUser(SessionFilter sessionFilter) { + return given().filter(sessionFilter); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/primary/internal/AssureOnlyFormAuthenticationForInternalApiTest.java b/backend/src/test/java/it/chalmers/gamma/adapter/primary/internal/AssureOnlyFormAuthenticationForInternalApiTest.java new file mode 100644 index 000000000..d84a6ae58 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/primary/internal/AssureOnlyFormAuthenticationForInternalApiTest.java @@ -0,0 +1,38 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import io.restassured.authentication.FormAuthConfig; +import io.restassured.filter.session.SessionFilter; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +import static io.restassured.RestAssured.*; + +@ActiveProfiles("test-with-mock") +public class AssureOnlyFormAuthenticationForInternalApiTest extends AbstractInternalApiControllerTest{ + + @Test + public void basicAuthenticationShouldFail() { + given() + .auth().basic("admin", "password") + .get("/api/internal/users/me") + .then() + .statusCode(401); + } + + @Test + public void digestAuthenticationShouldFail() { + given() + .auth().digest("admin", "password") + .get("/api/internal/users/me") + .then() + .statusCode(401); + } + + @Test + public void formAuthenticationShouldSucceed() { + givenAdminUser() + .get("/api/internal/users/me").then().statusCode(200); + } + + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/primary/internal/InfoApiSettingsAdminControllerTest.java b/backend/src/test/java/it/chalmers/gamma/adapter/primary/internal/InfoApiSettingsAdminControllerTest.java new file mode 100644 index 000000000..8d5a8b19a --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/primary/internal/InfoApiSettingsAdminControllerTest.java @@ -0,0 +1,48 @@ +package it.chalmers.gamma.adapter.primary.internal; + +import it.chalmers.gamma.app.settings.domain.SettingsRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import static io.restassured.RestAssured.given; + +@ActiveProfiles("test-with-mock") +public class InfoApiSettingsAdminControllerTest extends AbstractInternalApiControllerTest { + + @Autowired + private SettingsRepository settingsRepository; + + @Test + public void update() { +// var currentLastUpdatedUserAgreement = settingsRepository.getSettings().lastUpdatedUserAgreement(); + + record Request(List superGroupTypes) { } + + var request = given() + .cookie("haha", "haha") + .header("omg", "omg") +// .filter(adminAuthFilter()) + .and() +// .body(new Request(List.of("committee", "board"))) + .get("/api/internal/admin/info-api-settings/super-group-types"); + + System.out.println("headers"); + System.out.println(request.getHeaders()); + + var response = request.thenReturn(); + + System.out.println("HELLO"); + System.out.println(response.getStatusCode()); + System.out.println(response.getBody().prettyPrint()); + +// var toCheckLastUpdatedUserAgreement = settingsRepository.getSettings().lastUpdatedUserAgreement(); +// +// assertThat(currentLastUpdatedUserAgreement) +// .isEqualTo(toCheckLastUpdatedUserAgreement); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/primary/oauth2/OAuth2CodeFlowTest.java b/backend/src/test/java/it/chalmers/gamma/adapter/primary/oauth2/OAuth2CodeFlowTest.java new file mode 100644 index 000000000..a982b1fc5 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/primary/oauth2/OAuth2CodeFlowTest.java @@ -0,0 +1,29 @@ +package it.chalmers.gamma.adapter.primary.oauth2; + +import it.chalmers.gamma.adapter.primary.AbstractApiControllerTest; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Map; + +import static io.restassured.RestAssured.given; + +@ActiveProfiles("test-with-mock") +public class OAuth2CodeFlowTest extends AbstractApiControllerTest { + + @Test + public void testHappyAuthorizationCodeFlow() { + //http://gamma:8081/api/oauth2/authorize?response_type=code&client_id=test&scope=openid profile&state=_RRya178Aii4_JcPADFCA7iBe5e3ihPMdyDD9S0V_t8=&redirect_uri=http://client:3001/login/oauth2/code/gamma&nonce=e-D773_CxPQy0-OQWeNMbQlMKF_5PXjg21n0EddTsTU + + String state = "mystate"; + String nonce = "mynonce"; + + Map parameters = Map.of("clientId", "test", "scope", "profile", "state", state, "redirect_uri", "http://client:3001/login/oauth2/code/gamma", "nonce", nonce); + + var response = given() + .get("/api/oauth2/authorize?response_type=code&client_id={clientId}&scope={scope}&state={state}&redirect_uri={redirect_uri}&nonce={nonce}", parameters) + .thenReturn(); + + System.out.println(response.getBody().prettyPrint()); + } +} \ No newline at end of file diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/AbstractEntityIntegrationTests.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/AbstractEntityIntegrationTests.java new file mode 100644 index 000000000..0b6491e33 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/AbstractEntityIntegrationTests.java @@ -0,0 +1,12 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.testcontainers.junit.jupiter.Testcontainers; + +@DataJpaTest +@Testcontainers +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public abstract class AbstractEntityIntegrationTests { + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/AllowListEntityIntegrationTests.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/AllowListEntityIntegrationTests.java new file mode 100644 index 000000000..f026526fc --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/AllowListEntityIntegrationTests.java @@ -0,0 +1,92 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +import it.chalmers.gamma.adapter.secondary.jpa.allowlist.AllowListRepositoryAdapter; +import it.chalmers.gamma.app.user.domain.Cid; +import it.chalmers.gamma.app.user.allowlist.AllowListRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.*; + +@ActiveProfiles("test") +@Import({AllowListRepositoryAdapter.class}) +public class AllowListEntityIntegrationTests extends AbstractEntityIntegrationTests { + + @Autowired + private AllowListRepositoryAdapter allowListRepositoryAdapter; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + @Test + public void Given_Cid_Expect_allow_To_Work() { + Cid cid = new Cid("asdf"); + + assertThatNoException() + .isThrownBy(() -> allowListRepositoryAdapter.allow(cid)); + } + + @Test + public void Given_TheSameCidTwice_Expect_allow_To_Throw() { + Cid cid = new Cid("asdf"); + + assertThatNoException() + .isThrownBy(() -> allowListRepositoryAdapter.allow(cid)); + assertThatExceptionOfType(AllowListRepository.AlreadyAllowedException.class) + .isThrownBy(() -> allowListRepositoryAdapter.allow(cid)); + } + + @Test + public void Given_AllowedCid_Expect_remove_To_Work() + throws AllowListRepository.AlreadyAllowedException { + Cid cid = new Cid("asdf"); + + allowListRepositoryAdapter.allow(cid); + assertThatNoException() + .isThrownBy(() -> allowListRepositoryAdapter.remove(cid)); + } + + @Test + public void Given_CidThatIsNotAllowed_Expect_remove_To_Throw() { + Cid cid = new Cid("fdsa"); + + assertThatExceptionOfType(AllowListRepository.NotOnAllowListException.class) + .isThrownBy(() -> allowListRepositoryAdapter.remove(cid)); + } + + @Test + public void Given_AllowedCid_Expect_isAllowed_To_Work() + throws AllowListRepository.AlreadyAllowedException { + Cid cid = new Cid("asdf"); + + allowListRepositoryAdapter.allow(cid); + + assertThat(allowListRepositoryAdapter.isAllowed(cid)) + .isTrue(); + } + + @Test + public void Given_ManyAllowedCids_Expect_getAllowList_To_Work() throws AllowListRepository.AlreadyAllowedException { + Cid cid1 = new Cid("asdf"); + Cid cid2 = new Cid("aasdf"); + Cid cid3 = new Cid("absdf"); + Cid cid4 = new Cid("acsdf"); + Cid cid5 = new Cid("adsdf"); + + allowListRepositoryAdapter.allow(cid1); + allowListRepositoryAdapter.allow(cid2); + allowListRepositoryAdapter.allow(cid3); + allowListRepositoryAdapter.allow(cid4); + allowListRepositoryAdapter.allow(cid5); + + assertThat(allowListRepositoryAdapter.getAllowList()) + .containsExactlyInAnyOrder(cid1, cid2, cid3, cid4, cid5); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/ApiKeyEntityIntegrationTests.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/ApiKeyEntityIntegrationTests.java new file mode 100644 index 000000000..47724d3f3 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/ApiKeyEntityIntegrationTests.java @@ -0,0 +1,239 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +import it.chalmers.gamma.adapter.secondary.jpa.apikey.ApiKeyEntity; +import it.chalmers.gamma.adapter.secondary.jpa.apikey.ApiKeyEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.apikey.ApiKeyJpaRepository; +import it.chalmers.gamma.adapter.secondary.jpa.apikey.ApiKeyRepositoryAdapter; +import it.chalmers.gamma.app.apikey.domain.*; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@ActiveProfiles("test") +@Import({ApiKeyRepositoryAdapter.class, ApiKeyEntityConverter.class}) +class ApiKeyEntityIntegrationTests extends AbstractEntityIntegrationTests { + + @Autowired + private ApiKeyRepositoryAdapter apiKeyRepositoryAdapter; + + @Autowired + private ApiKeyJpaRepository apiKeyJpaRepository; + + @Autowired + private ApiKeyEntityConverter apiKeyEntityConverter; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @Test + public void Given_TwoApiKeys_Expect_create_To_Work() throws ApiKeyRepository.ApiKeyAlreadyExistRuntimeException { + apiKeyRepositoryAdapter.create( + new ApiKey( + ApiKeyId.generate(), + new PrettyName("what"), + new Text( + "lmao", + "lmao" + ), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ) + ); + + apiKeyRepositoryAdapter.create( + new ApiKey( + ApiKeyId.generate(), + new PrettyName("what"), + new Text( + "lmao", + "lmao" + ), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ) + ); + + assertThat(apiKeyRepositoryAdapter.getAll()) + .hasSize(2); + } + + @Test + public void Given_TwoApiKeyWithSameId_Expect_create_To_Throw() throws ApiKeyRepository.ApiKeyAlreadyExistRuntimeException { + ApiKeyId id = ApiKeyId.generate(); + + apiKeyRepositoryAdapter.create( + new ApiKey( + id, + new PrettyName("what"), + new Text( + "lmao", + "lmao" + ), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ) + ); + + assertThatExceptionOfType(ApiKeyRepository.ApiKeyAlreadyExistRuntimeException.class) + .isThrownBy(() -> apiKeyRepositoryAdapter.create( + new ApiKey( + id, + new PrettyName("what"), + new Text( + "lmao", + "lmao" + ), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ) + )); + + assertThat(apiKeyRepositoryAdapter.getAll()) + .hasSize(1); + } + + @Test + public void Given_ApiKey_Expect_delete_To_Work() + throws ApiKeyRepository.ApiKeyAlreadyExistRuntimeException, ApiKeyRepository.ApiKeyNotFoundException { + ApiKeyId id = ApiKeyId.generate(); + + apiKeyRepositoryAdapter.create( + new ApiKey( + id, + new PrettyName("what"), + new Text( + "lmao", + "lmao" + ), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ) + ); + + assertThat(apiKeyRepositoryAdapter.getAll()) + .hasSize(1); + + apiKeyRepositoryAdapter.delete(id); + + assertThat(apiKeyRepositoryAdapter.getAll()) + .hasSize(0); + } + + @Test + public void Given_NoApiKeys_Expect_delete_To_Throw() { + assertThatExceptionOfType(ApiKeyRepository.ApiKeyNotFoundException.class) + .isThrownBy(() -> apiKeyRepositoryAdapter.delete(ApiKeyId.generate())); + } + + @Test + public void Given_NoApiKeys_Expect_resetApiKeyToken_To_Throw() { + assertThatExceptionOfType(ApiKeyRepository.ApiKeyNotFoundException.class) + .isThrownBy(() -> apiKeyRepositoryAdapter.resetApiKeyToken(ApiKeyId.generate())); + } + + @Test + public void Given_AValidApiKey_Expect_resetApiKeyToken_To_Work() + throws ApiKeyRepository.ApiKeyAlreadyExistRuntimeException, ApiKeyRepository.ApiKeyNotFoundException { + ApiKeyId id = ApiKeyId.generate(); + ApiKeyToken token = ApiKeyToken.generate(); + + apiKeyRepositoryAdapter.create( + new ApiKey( + id, + new PrettyName("what"), + new Text( + "lmao", + "lmao" + ), + ApiKeyType.CLIENT, + token + ) + ); + + ApiKeyToken newToken = this.apiKeyRepositoryAdapter.resetApiKeyToken(id); + + assertThat(newToken) + .isNotNull() + .isNotEqualTo(token); + + //Make sure that if we get the api key again, that the token is the same. + ApiKey apiKey = this.apiKeyRepositoryAdapter.getById(id).orElse(null); + assertThat(apiKey) + .isNotNull() + .extracting(ApiKey::apiKeyToken) + .isEqualTo(newToken); + } + + @Test + public void Given_NoApiKeys_Expect_getAll_To_BeEmpty() { + assertThat(apiKeyRepositoryAdapter.getAll()) + .isEmpty(); + } + + @Test + public void Given_NoApiKeys_Expect_getById_To_BeEmpty() { + assertThat(apiKeyRepositoryAdapter.getById(ApiKeyId.generate())) + .isEmpty(); + } + + @Test + public void Given_NoApiKeys_Expect_getByToken_To_BeEmpty() { + assertThat(apiKeyRepositoryAdapter.getByToken(ApiKeyToken.generate())) + .isEmpty(); + } + + @Test + public void Given_AValidApiKey_Expect_getByToken_To_Work() throws ApiKeyRepository.ApiKeyAlreadyExistRuntimeException { + ApiKeyToken token = ApiKeyToken.generate(); + ApiKey apiKey = new ApiKey( + ApiKeyId.generate(), + new PrettyName("what"), + new Text( + "lmao", + "lmao" + ), + ApiKeyType.CLIENT, + token + ); + + apiKeyRepositoryAdapter.create(apiKey); + + assertThat(apiKeyRepositoryAdapter.getByToken(token)) + .isNotEmpty() + .get().isEqualTo(apiKey); + } + + @Test + public void Given_AApiKey_Expect_toDomain_To_Work() throws ApiKeyRepository.ApiKeyAlreadyExistRuntimeException { + ApiKey apiKey = new ApiKey( + ApiKeyId.generate(), + new PrettyName("what"), + new Text( + "lmao", + "lmao" + ), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ); + + this.apiKeyRepositoryAdapter.create(apiKey); + + ApiKeyEntity retrievedEntity = this.apiKeyJpaRepository.getById(apiKey.id().value()); + ApiKey converted = this.apiKeyEntityConverter.toDomain(retrievedEntity); + + assertThat(converted) + .isEqualTo(apiKey); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/AuthorityEntityIntegrationTests.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/AuthorityEntityIntegrationTests.java new file mode 100644 index 000000000..62d2c9fcf --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/AuthorityEntityIntegrationTests.java @@ -0,0 +1,366 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.group.GroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.group.GroupRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.settings.SettingsRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupTypeRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserRepositoryAdapter; +import it.chalmers.gamma.app.authentication.UserAccessGuard; +import it.chalmers.gamma.security.user.PasswordConfiguration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@Import({ClientAuthorityRepositoryAdapter.class, + SuperGroupRepositoryAdapter.class, + SuperGroupEntityConverter.class, + UserRepositoryAdapter.class, + PasswordConfiguration.class, + UserEntityConverter.class, + UserAccessGuard.class, + PostRepositoryAdapter.class, + PostEntityConverter.class, + GroupRepositoryAdapter.class, + GroupEntityConverter.class, + SuperGroupTypeRepositoryAdapter.class, + ClientAuthorityEntityConverter.class, + SuperGroupEntityConverter.class, + UserEntityConverter.class, + UserAccessGuard.class, + PostEntityConverter.class, + SettingsRepositoryAdapter.class}) +public class AuthorityEntityIntegrationTests extends AbstractEntityIntegrationTests { + + /* + + @Autowired + private ClientAuthorityRepositoryAdapter authorityLevelRepositoryAdapter; + @Autowired + private SuperGroupRepository superGroupRepository; + @Autowired + private SuperGroupTypeRepository superGroupTypeRepository; + @Autowired + private GroupRepository groupRepository; + @Autowired + private PostRepository postRepository; + @Autowired + private UserRepository userRepository; + @Autowired + private SettingsRepository settingsRepository; + + private static UserAuthority ua(AuthorityName name, AuthorityType authorityType) { + return new UserAuthority(name, authorityType); + } + + @BeforeEach + public void setSettings() { + this.settingsRepository.setSettings(defaultSettings); + } + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + // This is a very important test! If it fails, please stop. + @Test + public void getByUser_SuperTest() throws ClientAuthorityRepository.AuthorityLevelAlreadyExistsException, SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + addAll(userRepository, u0, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u11); + addAll(superGroupTypeRepository, committee, alumni, board); + addAll(postRepository, chair, treasurer, member); + addAll(superGroupRepository, digit, didit, prit, sprit, styrit, emeritus); + addAll(groupRepository, digit18, digit19, prit18, prit19, styrit18, styrit19); + + AuthorityName adminName = new AuthorityName("admin"); + Authority adminLevel = new Authority( + adminName, + List.of(new Authority.SuperGroupPost(digit, chair), new Authority.SuperGroupPost(didit, chair)), + List.of(styrit), + List.of(u1) + ); + + AuthorityName matName = new AuthorityName("mat"); + Authority matLevel = new Authority( + matName, + Collections.emptyList(), + List.of(prit), + List.of(u5, u8) + ); + + AuthorityName glassName = new AuthorityName("glass"); + Authority glassLevel = new Authority( + glassName, + List.of(new Authority.SuperGroupPost(styrit, chair)), + Collections.emptyList(), + Collections.emptyList() + ); + + this.authorityLevelRepositoryAdapter.create(adminName); + this.authorityLevelRepositoryAdapter.save(adminLevel); + + setAuthenticatedUser(null, null, u1, true); + + this.authorityLevelRepositoryAdapter.create(matName); + this.authorityLevelRepositoryAdapter.save(matLevel); + + this.authorityLevelRepositoryAdapter.create(glassName); + this.authorityLevelRepositoryAdapter.save(glassLevel); + + adminLevel = asSaved(adminLevel); + matLevel = asSaved(matLevel); + glassLevel = asSaved(glassLevel); + + assertThat(this.authorityLevelRepositoryAdapter.getAll()) + .containsExactlyInAnyOrder(adminLevel, matLevel, glassLevel); + + assertThat(this.authorityLevelRepositoryAdapter.get(adminName)) + .isNotEmpty() + .get().isEqualTo(adminLevel); + assertThat(this.authorityLevelRepositoryAdapter.get(matName)) + .isNotEmpty() + .get().isEqualTo(matLevel); + assertThat(this.authorityLevelRepositoryAdapter.get(glassName)) + .isNotEmpty() + .get().isEqualTo(glassLevel); + + AuthorityName sprit = new AuthorityName("sprit"); + AuthorityName prit = new AuthorityName("prit"); + AuthorityName prit18 = new AuthorityName("prit18"); + AuthorityName prit19 = new AuthorityName("prit19"); + AuthorityName didit = new AuthorityName("didit"); + AuthorityName digit = new AuthorityName("digit"); + AuthorityName digit18 = new AuthorityName("digit18"); + AuthorityName digit19 = new AuthorityName("digit19"); + AuthorityName emeritus = new AuthorityName("emeritus"); + AuthorityName styrit = new AuthorityName("styrit"); + AuthorityName styrit18 = new AuthorityName("styrit18"); + AuthorityName styrit19 = new AuthorityName("styrit19"); + + assertThat(this.authorityLevelRepositoryAdapter.getByUser(u0.id())) + .isEmpty(); + assertThat(this.authorityLevelRepositoryAdapter.getByUser(u1.id())) + .containsExactlyInAnyOrder( + ua(adminName, AUTHORITY), + ua(sprit, SUPERGROUP), + ua(didit, SUPERGROUP), + ua(prit18, GROUP), + ua(digit18, GROUP) + ); + assertThat(this.authorityLevelRepositoryAdapter.getByUser(u2.id())) + .containsExactlyInAnyOrder( + ua(sprit, SUPERGROUP), + ua(didit, SUPERGROUP), + ua(digit, SUPERGROUP), + ua(digit19, GROUP), + ua(digit18, GROUP), + ua(prit18, GROUP) + ); + assertThat(this.authorityLevelRepositoryAdapter.getByUser(u3.id())) + .containsExactlyInAnyOrder( + ua(adminName, AUTHORITY), + ua(digit, SUPERGROUP), + ua(digit19, GROUP) + ); + assertThat(this.authorityLevelRepositoryAdapter.getByUser(u4.id())) + .containsExactlyInAnyOrder( + ua(digit, SUPERGROUP), + ua(digit19, GROUP) + ); + assertThat(this.authorityLevelRepositoryAdapter.getByUser(u5.id())) + .containsExactlyInAnyOrder( + ua(matName, AUTHORITY), + ua(prit, SUPERGROUP), + ua(prit19, GROUP) + ); + assertThat(this.authorityLevelRepositoryAdapter.getByUser(u6.id())) + .containsExactlyInAnyOrder( + ua(matName, AUTHORITY), + ua(prit, SUPERGROUP), + ua(prit19, GROUP) + ); + assertThat(this.authorityLevelRepositoryAdapter.getByUser(u7.id())) + .containsExactlyInAnyOrder( + ua(emeritus, SUPERGROUP), + ua(styrit18, GROUP) + ); + assertThat(this.authorityLevelRepositoryAdapter.getByUser(u8.id())) + .containsExactlyInAnyOrder( + ua(emeritus, SUPERGROUP), + ua(styrit18, GROUP), + ua(matName, AUTHORITY) + ); + assertThat(this.authorityLevelRepositoryAdapter.getByUser(u9.id())) + .containsExactlyInAnyOrder( + ua(emeritus, SUPERGROUP), + ua(styrit18, GROUP) + ); + assertThat(this.authorityLevelRepositoryAdapter.getByUser(u10.id())) + .containsExactlyInAnyOrder( + ua(glassName, AUTHORITY), + ua(adminName, AUTHORITY), + ua(styrit, SUPERGROUP), + ua(styrit19, GROUP) + ); + assertThat(this.authorityLevelRepositoryAdapter.getByUser(u11.id())) + .containsExactlyInAnyOrder( + ua(adminName, AUTHORITY), + ua(styrit, SUPERGROUP), + ua(styrit19, GROUP) + ); + } + + @Test + public void Given_TwoAuthorityLevels_Expect_create_To_Work() throws ClientAuthorityRepository.AuthorityLevelAlreadyExistsException { + AuthorityName admin = new AuthorityName("admin"); + AuthorityName mat = new AuthorityName("mat"); + + authorityLevelRepositoryAdapter.create(admin); + authorityLevelRepositoryAdapter.create(mat); + + assertThat(authorityLevelRepositoryAdapter.getAll().stream()) + .extracting(Authority::name) + .containsExactlyInAnyOrder(admin, mat); + } + + @Test + public void Given_NewAuthorityLevel_Expect_create_To_CreateEmptyAuthorityLevel() throws ClientAuthorityRepository.AuthorityLevelAlreadyExistsException { + AuthorityName admin = new AuthorityName("admin"); + + authorityLevelRepositoryAdapter.create(admin); + + assertThat(authorityLevelRepositoryAdapter.get(admin)) + .isNotEmpty() + .get().isEqualTo(new Authority( + admin, + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + )); + } + + @Test + public void Given_TwoAuthorityLevelWithSameName_Expect_create_To_Throw() throws ClientAuthorityRepository.AuthorityLevelAlreadyExistsException { + AuthorityName admin = new AuthorityName("admin"); + + authorityLevelRepositoryAdapter.create(admin); + assertThatExceptionOfType(ClientAuthorityRepository.AuthorityLevelAlreadyExistsException.class) + .isThrownBy(() -> authorityLevelRepositoryAdapter.create(admin)); + + assertThat(authorityLevelRepositoryAdapter.get(admin)) + .isNotEmpty(); + } + + @Test + public void Given_AValidAuthorityLevel_Expect_save_With_AInvalidSuperGroup_To_Throw() + throws ClientAuthorityRepository.AuthorityLevelAlreadyExistsException { + AuthorityName admin = new AuthorityName("admin"); + + authorityLevelRepositoryAdapter.create(admin); + assertThatExceptionOfType(ClientAuthorityRepository.NotCompleteAuthorityLevelException.class) + .isThrownBy(() -> authorityLevelRepositoryAdapter.save( + new Authority( + admin, + Collections.emptyList(), + List.of(digit), + Collections.emptyList() + ) + )); + } + + @Test + public void Given_AValidAuthorityLevel_Expect_save_With_AInvalidSuperGroupPost_Specifically_NoPost_To_Throw() + throws ClientAuthorityRepository.AuthorityLevelAlreadyExistsException, SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + AuthorityName admin = new AuthorityName("admin"); + + superGroupTypeRepository.add(digit.type()); + superGroupRepository.save(digit); + + authorityLevelRepositoryAdapter.create(admin); + assertThatExceptionOfType(ClientAuthorityRepository.NotCompleteAuthorityLevelException.class) + .isThrownBy(() -> authorityLevelRepositoryAdapter.save( + new Authority( + admin, + List.of(new Authority.SuperGroupPost(digit, chair)), + Collections.emptyList(), + Collections.emptyList() + ) + )); + } + + @Test + public void Given_AValidAuthorityLevel_Expect_save_With_AInvalidSuperGroupPost_Specifically_NoSuperGroup_To_Throw() + throws ClientAuthorityRepository.AuthorityLevelAlreadyExistsException { + AuthorityName admin = new AuthorityName("admin"); + + postRepository.save(chair); + + authorityLevelRepositoryAdapter.create(admin); + assertThatExceptionOfType(ClientAuthorityRepository.NotCompleteAuthorityLevelException.class) + .isThrownBy(() -> authorityLevelRepositoryAdapter.save( + new Authority( + admin, + List.of(new Authority.SuperGroupPost(digit, chair)), + Collections.emptyList(), + Collections.emptyList() + ) + )); + } + + @Test + public void Given_AValidAuthorityLevel_Expect_save_With_AInvalidUser_To_Throw() + throws ClientAuthorityRepository.AuthorityLevelAlreadyExistsException { + AuthorityName admin = new AuthorityName("admin"); + + authorityLevelRepositoryAdapter.create(admin); + assertThatExceptionOfType(ClientAuthorityRepository.NotCompleteAuthorityLevelException.class) + .isThrownBy(() -> authorityLevelRepositoryAdapter.save( + new Authority( + admin, + Collections.emptyList(), + Collections.emptyList(), + List.of(u1) + ) + )); + } + + @Test + public void Given_AValidAuthorityLevelThatHasNotBeenSaved_Expect_save_To_Throw() { + AuthorityName admin = new AuthorityName("admin"); + + assertThatExceptionOfType(ClientAuthorityRepository.AuthorityLevelNotFoundRuntimeException.class) + .isThrownBy(() -> authorityLevelRepositoryAdapter.save( + new Authority( + admin, + List.of(new Authority.SuperGroupPost(digit, chair)), + List.of(digit), + List.of(u1) + ))); + } + + @Test + public void Given_AValidAuthorityLevel_Expect_delete_To_Work() throws ClientAuthorityRepository.AuthorityLevelAlreadyExistsException { + AuthorityName admin = new AuthorityName("admin"); + + authorityLevelRepositoryAdapter.create(admin); + assertThatNoException() + .isThrownBy(() -> authorityLevelRepositoryAdapter.delete(admin)); + } + + @Test + public void Given_NoAuthorityLevel_Expect_delete_To_Throw() { + AuthorityName admin = new AuthorityName("admin"); + + assertThatExceptionOfType(ClientAuthorityRepository.AuthorityLevelNotFoundException.class) + .isThrownBy(() -> authorityLevelRepositoryAdapter.delete(admin)); + } + + */ + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/ClientEntityIntegrationTests.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/ClientEntityIntegrationTests.java new file mode 100644 index 000000000..8b313b882 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/ClientEntityIntegrationTests.java @@ -0,0 +1,503 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +import it.chalmers.gamma.adapter.secondary.jpa.apikey.ApiKeyEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.apikey.ApiKeyRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.settings.SettingsRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserRepositoryAdapter; +import it.chalmers.gamma.app.authentication.UserAccessGuard; +import it.chalmers.gamma.security.user.PasswordConfiguration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +import static it.chalmers.gamma.utils.GammaSecurityContextHolderTestUtils.setAuthenticatedAsAdminUser; +import static it.chalmers.gamma.utils.GammaSecurityContextHolderTestUtils.setAuthenticatedAsClientWithApi; +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("test") +@Import({ClientRepositoryAdapter.class, + ClientEntityConverter.class, + UserRepositoryAdapter.class, + PasswordConfiguration.class, + UserEntityConverter.class, + UserAccessGuard.class, + ApiKeyRepositoryAdapter.class, + ApiKeyEntityConverter.class, + ClientAuthorityRepositoryAdapter.class, + ClientAuthorityEntityConverter.class, + SuperGroupEntityConverter.class, + PostEntityConverter.class, + UserEntityConverter.class, + SettingsRepositoryAdapter.class}) +public class ClientEntityIntegrationTests extends AbstractEntityIntegrationTests { + + /* + @Autowired + private ClientRepositoryAdapter clientRepositoryAdapter; + @Autowired + private ApiKeyRepository apiKeyRepository; + @Autowired + private UserRepository userRepository; + @Autowired + private ClientAuthorityRepository clientAuthorityRepository; + @Autowired + private SettingsRepository settingsRepository; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @BeforeEach + public void setSettings() { + this.settingsRepository.setSettings(defaultSettings); + } + + @Test + public void Given_AValidClient_Expect_save_To_Work() { + ClientUid uid = ClientUid.generate(); + Client newClient = new Client( + uid, + ClientId.generate(), + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat"), + new Text( + "Klient för mat", + "Client for mat" + ), + Collections.emptyList(), + null, + new ClientOwnerOfficial() + ); + + this.clientRepositoryAdapter.save(newClient); + + Client savedClient = this.clientRepositoryAdapter.get(uid).orElseThrow(); + + assertThat(savedClient) + .isEqualTo(newClient); + } + + @Test + public void Given_AValidClientWithScopes_Expect_save_To_Work() { + ClientUid uid = ClientUid.generate(); + Client newClient = new Client( + uid, + ClientId.generate(), + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat"), + new Text( + "Klient för mat", + "Client for mat" + ), + List.of(Scope.PROFILE, Scope.EMAIL), + null, + new ClientOwnerOfficial() + ); + + this.clientRepositoryAdapter.save(newClient); + + Client savedClient = this.clientRepositoryAdapter.get(uid).orElseThrow(); + + assertThat(savedClient) + .isEqualTo(newClient); + } + + @Test + public void Given_AValidClientWithApiKey_Expect_save_To_Work() { + ApiKey apiKey = new ApiKey( + ApiKeyId.generate(), + new PrettyName("Mat"), + new Text( + "Api nyckel för mat", + "Api key for mat" + ), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ); + + ClientUid uid = ClientUid.generate(); + Client newClient = new Client( + uid, + ClientId.generate(), + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat"), + new Text( + "Klient för mat", + "Client for mat" + ), + Collections.emptyList(), + apiKey, + new ClientOwnerOfficial() + ); + + this.clientRepositoryAdapter.save(newClient); + + Client savedClient = this.clientRepositoryAdapter.get(uid).orElseThrow(); + + assertThat(savedClient) + .isEqualTo(newClient); + } + + @Test + public void Given_AValidClientWithRestrictions_Expect_save_To_Work() throws ClientAuthorityRepository.AuthorityLevelAlreadyExistsException { + clientAuthorityRepository.create(new AuthorityName("admin")); + setAuthenticatedAsAdminUser(userRepository, null); + + addAll(userRepository, u1); + clientAuthorityRepository.save(new Authority( + new AuthorityName("admin"), + Collections.emptyList(), + Collections.emptyList(), + List.of(removeUserExtended(u1)) + )); + + ClientUid uid = ClientUid.generate(); + Client newClient = new Client( + uid, + ClientId.generate(), + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat"), + new Text( + "Klient för mat", + "Client for mat" + ), +// List.of(new AuthorityName("admin")), + Collections.emptyList(), + null, + new ClientOwnerOfficial() + ); + + this.clientRepositoryAdapter.save(newClient); + + Client savedClient = this.clientRepositoryAdapter.get(uid).orElseThrow(); + + assertThat(savedClient) + .isEqualTo(newClient); + } + + @Test + public void Given_AValidClientWithApprovedUsers_Expect_save_To_Work() { + GammaUser user = setAuthenticatedAsAdminUser(userRepository, clientAuthorityRepository); + + ClientUid uid = ClientUid.generate(); + Client newClient = new Client( + uid, + ClientId.generate(), + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat"), + new Text( + "Klient för mat", + "Client for mat" + ), + Collections.emptyList(), + new ApiKey( + ApiKeyId.generate(), + new PrettyName("Api Key"), + new Text(), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ), + new ClientOwnerOfficial() + ); + + this.clientRepositoryAdapter.save(newClient); + + setAuthenticatedAsClientWithApi(newClient); + + Client savedClient = this.clientRepositoryAdapter.get(uid).orElseThrow(); + + assertThat(savedClient) + .isEqualTo(newClient); + + setAuthenticatedAsAdminUser(user); + + addAll(userRepository, u1, u2, u4); + + throw new UnsupportedOperationException(); +// Client newClient2 = newClient.withApprovedUsers( +// List.of(u1, u2, u4) +// ); +// +// this.clientRepositoryAdapter.save(newClient2); +// +// setAuthenticatedAsClientWithApi(newClient); +// +// Client savedClient2 = this.clientRepositoryAdapter.get(uid).orElseThrow(); +// +// //No u2 since they are locked +// assertThat(savedClient2) +// .isEqualTo(removeUserExtended(newClient2.withApprovedUsers(List.of(u1, u4)))); + } + + @Test + public void Given_AValidClient_Expect_save_And_delete_To_Work() throws ClientRepository.ClientNotFoundException { + ClientUid uid = ClientUid.generate(); + Client newClient = new Client( + uid, + ClientId.generate(), + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat"), + new Text( + "Klient för mat", + "Client for mat" + ), + Collections.emptyList(), + null, + new ClientOwnerOfficial() + ); + + this.clientRepositoryAdapter.save(newClient); + + assertThat(this.clientRepositoryAdapter.getAll()) + .containsExactlyInAnyOrder(newClient); + + this.clientRepositoryAdapter.delete(uid); + + assertThat(this.clientRepositoryAdapter.getAll()) + .isEmpty(); + } + + @Test + public void Given_ClientWithApiKey_Expect_delete_To_DeleteBoth() throws ClientRepository.ClientNotFoundException { + ApiKey apiKey = new ApiKey( + ApiKeyId.generate(), + new PrettyName("Mat"), + new Text( + "Api nyckel för mat", + "Api key for mat" + ), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ); + + ClientUid uid = ClientUid.generate(); + Client newClient = new Client( + uid, + ClientId.generate(), + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat"), + new Text( + "Klient för mat", + "Client for mat" + ), + Collections.emptyList(), + apiKey, + new ClientOwnerOfficial() + ); + + this.clientRepositoryAdapter.save(newClient); + + assertThat(this.clientRepositoryAdapter.get(uid)) + .get().isEqualTo(newClient); + + this.clientRepositoryAdapter.delete(uid); + assertThat(this.clientRepositoryAdapter.get(uid)) + .isEmpty(); + assertThat(this.apiKeyRepository.getById(apiKey.id())) + .isEmpty(); + } + + @Test + public void Given_InvalidClient_Expect_delete_To_Throw() { + assertThatExceptionOfType(ClientRepository.ClientNotFoundException.class) + .isThrownBy(() -> this.clientRepositoryAdapter.delete(ClientUid.generate())); + } + + @Test + public void Given_AValidClient_Expect_get_WithClientId_To_Work() { + ClientId clientId = ClientId.generate(); + Client newClient = new Client( + ClientUid.generate(), + clientId, + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat"), + new Text( + "Klient för mat", + "Client for mat" + ), + Collections.emptyList(), + null, + new ClientOwnerOfficial() + ); + + this.clientRepositoryAdapter.save(newClient); + + Client savedClient = this.clientRepositoryAdapter.get(clientId).orElseThrow(); + + assertThat(savedClient) + .isEqualTo(newClient); + } + + @Test + public void Given_ClientWithApiKey_Expect_DeletingApiKey_To_Work() throws ApiKeyRepository.ApiKeyNotFoundException { + ApiKey apiKey = new ApiKey( + ApiKeyId.generate(), + new PrettyName("Mat"), + new Text( + "Api nyckel för mat", + "Api key for mat" + ), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ); + + ClientUid uid = ClientUid.generate(); + Client newClient = new Client( + uid, + ClientId.generate(), + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat"), + new Text( + "Klient för mat", + "Client for mat" + ), + Collections.emptyList(), + apiKey, + new ClientOwnerOfficial() + ); + + this.clientRepositoryAdapter.save(newClient); + + this.apiKeyRepository.delete(apiKey.id()); + + Client retrievedClient = this.clientRepositoryAdapter.get(uid).orElseThrow(); + + assertThat(retrievedClient.clientApiKey()) + .isEmpty(); + assertThat(apiKeyRepository.getById(apiKey.id())) + .isEmpty(); + } + + @Test + public void Given_ClientWithInvalidRestriction_Expect_save_To_Throw() { + Client newClient = new Client( + ClientUid.generate(), + ClientId.generate(), + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat"), + new Text( + "Klient för mat", + "Client for mat" + ), +// List.of(new AuthorityName("test")), + Collections.emptyList(), + null, + new ClientOwnerOfficial() + ); + + assertThatExceptionOfType(ClientRepository.AuthorityLevelNotFoundRuntimeException.class) + .isThrownBy(() -> this.clientRepositoryAdapter.save(newClient)); + } + + @Test + public void Given_SameClientId_Expect_save_To_Work() { + Client client = new Client( + ClientUid.generate(), + ClientId.generate(), + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat"), + new Text( + "Klient för mat", + "Client for mat" + ), + Collections.emptyList(), + null, + new ClientOwnerOfficial() + ); + + this.clientRepositoryAdapter.save(client); + + Client clientWithSameId = new Client( + ClientUid.generate(), + client.clientId(), + client.clientSecret(), + client.clientRedirectUrl(), + client.prettyName(), + client.description(), + client.scopes(), + client.clientApiKey().orElse(null), + client.access() + ); + + assertThatExceptionOfType(ClientRepository.ClientIdAlreadyExistsRuntimeException.class) + .isThrownBy(() -> this.clientRepositoryAdapter.save(clientWithSameId)); + } + + @Test + public void Given_UserThatHasApprovedClients_Expect_getClientsByUserApproved_To_Work() { + setAuthenticatedAsAdminUser(userRepository, clientAuthorityRepository); + + addAll(userRepository, u1); + + Client client = new Client( + ClientUid.generate(), + ClientId.generate(), + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat"), + new Text( + "Klient för mat", + "Client for mat" + ), + Collections.emptyList(), +// List.of(asSaved(u1)), + null, + new ClientOwnerOfficial() + ); + + //TODO: Fix + +// Client client2 = client.with() +// .clientId(ClientId.generate()) +// .clientUid(ClientUid.generate()) +// .build(); +// Client client3 = client.with() +// .clientId(ClientId.generate()) +// .clientUid(ClientUid.generate()) +//// .approvedUsers(Collections.emptyList()) +// .build(); +// Client client4 = client.with() +// .clientId(ClientId.generate()) +// .clientUid(ClientUid.generate()) +// .build(); +// Client client5 = client.with() +// .clientId(ClientId.generate()) +// .clientUid(ClientUid.generate()) +// .build(); +// +// this.clientRepositoryAdapter.save(client); +// this.clientRepositoryAdapter.save(client2); +// this.clientRepositoryAdapter.save(client3); +// this.clientRepositoryAdapter.save(client4); +// this.clientRepositoryAdapter.save(client5); +// +// assertThat(this.clientRepositoryAdapter.getClientsByUserApproved(u1.id())) +// .containsExactlyInAnyOrder( +// client, +// client2, +// client4, +// client5 +// ); + } + + */ +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/EntityIntegrationTest.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/EntityIntegrationTest.java new file mode 100644 index 000000000..9eb0db10d --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/EntityIntegrationTest.java @@ -0,0 +1,4 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +public @interface EntityIntegrationTest { +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/GroupEntityIntegrationTests.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/GroupEntityIntegrationTests.java new file mode 100644 index 000000000..b90a59a61 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/GroupEntityIntegrationTests.java @@ -0,0 +1,287 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.group.GroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.group.GroupRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.settings.SettingsRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupTypeRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserRepositoryAdapter; +import it.chalmers.gamma.app.authentication.UserAccessGuard; +import it.chalmers.gamma.security.user.PasswordConfiguration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +import static it.chalmers.gamma.utils.GammaSecurityContextHolderTestUtils.setAuthenticatedAsAdminUser; + +@ActiveProfiles("test") +@Import({GroupRepositoryAdapter.class, + GroupEntityConverter.class, + SuperGroupEntityConverter.class, + UserEntityConverter.class, + PostEntityConverter.class, + UserRepositoryAdapter.class, + PasswordConfiguration.class, + UserAccessGuard.class, + PostRepositoryAdapter.class, + SuperGroupRepositoryAdapter.class, + SuperGroupTypeRepositoryAdapter.class, + SettingsRepositoryAdapter.class, + ClientAuthorityRepositoryAdapter.class, + ClientAuthorityEntityConverter.class}) +public class GroupEntityIntegrationTests extends AbstractEntityIntegrationTests { + + /* + @Autowired + private GroupRepositoryAdapter groupRepositoryAdapter; + @Autowired + private SuperGroupRepository superGroupRepository; + @Autowired + private SuperGroupTypeRepository superGroupTypeRepository; + @Autowired + private UserRepository userRepository; + @Autowired + private PostRepository postRepository; + @Autowired + private SettingsRepository settingsRepository; + @Autowired + private ClientAuthorityRepository clientAuthorityRepository; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @BeforeEach + public void setSettings() { + this.settingsRepository.setSettings(defaultSettings); + } + + @Test + public void Given_ValidGroup_Expect_save_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + setAuthenticatedAsAdminUser(userRepository, clientAuthorityRepository); + Group groupToSave = digit18; + addGroup(groupToSave); + + Group savedGroup = groupRepositoryAdapter.get(groupToSave.id()) + .orElseThrow(); + + assertThat(savedGroup) + .isEqualTo(asSaved(groupToSave)); + } + + @Test + public void Given_SameGroupIdTwice_Expect_save_To_Throw() { + setAuthenticatedAsAdminUser(userRepository, clientAuthorityRepository); + + assertThatExceptionOfType(MutableEntity.StaleDomainObjectException.class) + .isThrownBy(() -> addGroup(digit18, digit18)); + + assertThat(groupRepositoryAdapter.getAll()) + .containsExactlyInAnyOrder( + asSaved(digit18) + ); + } + + @Test + public void Given_SameGroupNameTwice_Expect_save_To_Throw() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + addGroup(digit18); + assertThatExceptionOfType(GroupRepository.GroupNameAlreadyExistsException.class) + .isThrownBy(() -> this.groupRepositoryAdapter.save(digit18.withId(GroupId.generate()))); + } + + @Test + public void Given_GroupWithInvalidSuperGroup_Expect_save_To_Throw() { + Group group = digit18; + addAll(userRepository, group.groupMembers().stream().map(GroupMember::user).toList()); + addAll(postRepository, group.groupMembers().stream().map(GroupMember::post).distinct().toList()); + + assertThatExceptionOfType(GroupRepository.SuperGroupNotFoundRuntimeException.class) + .isThrownBy(() -> groupRepositoryAdapter.save(group)); + } + + @Test + public void Given_GroupWithInvalidUser_Expect_save_To_Throw() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + Group group = digit18; + superGroupTypeRepository.add(group.superGroup().type()); + superGroupRepository.save(group.superGroup()); + addAll(postRepository, group.groupMembers().stream().map(GroupMember::post).distinct().toList()); + + assertThatExceptionOfType(GroupRepository.UserNotFoundRuntimeException.class) + .isThrownBy(() -> groupRepositoryAdapter.save(group)); + } + + @Test + public void Given_GroupWithInvalidPost_Expect_save_To_Throw() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + Group group = digit18; + superGroupTypeRepository.add(group.superGroup().type()); + superGroupRepository.save(group.superGroup()); + addAll(userRepository, group.groupMembers().stream().map(GroupMember::user).toList()); + + assertThatExceptionOfType(GroupRepository.PostNotFoundRuntimeException.class) + .isThrownBy(() -> groupRepositoryAdapter.save(group)); + } + + @Test + public void Given_ValidGroup_Expect_delete_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + Group group = digit18; + + addGroup(group); + assertThatNoException() + .isThrownBy(() -> groupRepositoryAdapter.delete(group.id())); + + assertThat(groupRepositoryAdapter.get(group.id())) + .isEmpty(); + } + + @Test + public void Given_GroupWithInvalidVersion_Expect_save_To_Throw() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + setAuthenticatedAsAdminUser(userRepository, clientAuthorityRepository); + + superGroupTypeRepository.add(digit.type()); + superGroupRepository.save(digit); + + Group group = digit18; + + //Default version is 0 + addGroup(group); + group = groupRepositoryAdapter.get(group.id()).orElseThrow(); + + //Version 1 + groupRepositoryAdapter.save(group.withPrettyName(new PrettyName("what"))); + group = groupRepositoryAdapter.get(group.id()).orElseThrow(); + + //Version 2 + groupRepositoryAdapter.save(group.withGroupMembers(List.of(gm(u1, chair)))); + group = groupRepositoryAdapter.get(group.id()).orElseThrow(); + + //Version 3 + groupRepositoryAdapter.save(group.withAvatarUri(Optional.of(new ImageUri("myimage.png")))); + group = groupRepositoryAdapter.get(group.id()).orElseThrow(); + + //Version 4 + groupRepositoryAdapter.save(group.withSuperGroup(digit)); + group = groupRepositoryAdapter.get(group.id()).orElseThrow(); + + final Group group1 = group; + + assertThatExceptionOfType(MutableEntity.StaleDomainObjectException.class) + .isThrownBy(() -> groupRepositoryAdapter.save( + group1 + .withPrettyName(new PrettyName("new digIT'18")) + .withVersion(3) + )); + + assertThatExceptionOfType(MutableEntity.StaleDomainObjectException.class) + .isThrownBy(() -> groupRepositoryAdapter.save( + group1 + .withGroupMembers(List.of(gm(u1, chair))) + .withVersion(-500) + )); + + assertThatExceptionOfType(MutableEntity.StaleDomainObjectException.class) + .isThrownBy(() -> groupRepositoryAdapter.save( + group1 + .withAvatarUri(Optional.of(new ImageUri("myimage.png"))) + .withVersion(500) + )); + + assertThatExceptionOfType(MutableEntity.StaleDomainObjectException.class) + .isThrownBy(() -> groupRepositoryAdapter.save( + group1 + .withSuperGroup(digit) + .withVersion(0) + )); + + assertThatNoException() + .isThrownBy(() -> groupRepositoryAdapter.save(group1 + .withVersion(5) //Expect version after 4 saves. + .withPrettyName(new PrettyName("new digIT'18"))) + ); + } + + @Test + public void Given_InvalidGroup_Expect_delete_To_Throw() { + assertThatExceptionOfType(GroupRepository.GroupNotFoundException.class) + .isThrownBy(() -> groupRepositoryAdapter.delete(GroupId.generate())); + } + + @Test + public void Given_MultipleValidGroups_Expect_getAll_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + setAuthenticatedAsAdminUser(userRepository, clientAuthorityRepository); + + addGroup(digit18, digit19, prit19, styrit19); + + assertThat(groupRepositoryAdapter.getAll()) + .containsExactlyInAnyOrder( + asSaved(digit18), + asSaved(digit19), + asSaved(prit19), + asSaved(styrit19) + ); + } + + @Test + public void Given_MultipleValidGroups_Expect_getAllBySuperGroup_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + setAuthenticatedAsAdminUser(userRepository, clientAuthorityRepository); + addGroup(digit17, digit18, digit19, prit18, prit19, styrit18, styrit19, drawit18); + + assertThat(groupRepositoryAdapter.getAllBySuperGroup(digit.id())) + .containsExactlyInAnyOrder(asSaved(digit19)); + assertThat(groupRepositoryAdapter.getAllBySuperGroup(didit.id())) + .containsExactlyInAnyOrder(asSaved(digit17), asSaved(digit18)); + assertThat(groupRepositoryAdapter.getAllBySuperGroup(drawit.id())) + .isEmpty(); + } + + @Test + public void Given_MultipleValidGroups_Expect_getAllByPost_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + setAuthenticatedAsAdminUser(userRepository, clientAuthorityRepository); + addGroup(digit17, digit18, digit19, prit18); + + assertThat(groupRepositoryAdapter.getAllByPost(chair.id())) + .containsExactlyInAnyOrder(asSaved(digit17), asSaved(digit18), asSaved(digit19), asSaved(prit18)); + assertThat(groupRepositoryAdapter.getAllByPost(member.id())) + .containsExactlyInAnyOrder(asSaved(digit17), asSaved(digit19), asSaved(prit18)); + } + + @Test + public void Given_MultipleValidGroups_Expect_getAllByUser_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + setAuthenticatedAsAdminUser(userRepository, clientAuthorityRepository); + addGroup(digit17, digit18, digit19, prit18, prit19, styrit18, styrit19, drawit18, drawit19); + + assertThat(groupRepositoryAdapter.getAllByUser(u0.id())) + .isEmpty(); + assertThat(groupRepositoryAdapter.getAllByUser(u1.id())) + .containsExactlyInAnyOrder( + new UserMembership(chair.withVersion(1), asSaved(digit18), new UnofficialPostName("root")), + new UserMembership(chair.withVersion(1), asSaved(prit18), new UnofficialPostName("ChefChef")), + new UserMembership(treasurer.withVersion(1), asSaved(prit18), UnofficialPostName.none()), + new UserMembership(chair.withVersion(1), asSaved(drawit19), UnofficialPostName.none()) + ); + assertThat(groupRepositoryAdapter.getAllByUser(u3.id())) + .containsExactlyInAnyOrder( + new UserMembership(chair.withVersion(1), asSaved(digit19), UnofficialPostName.none()) + ); + } + + private void addGroup(Group... groups) throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + DomainUtils.addGroup( + superGroupTypeRepository, + superGroupRepository, + userRepository, + postRepository, + groupRepositoryAdapter, + groups + ); + } + + */ + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/PostEntityIntegrationTests.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/PostEntityIntegrationTests.java new file mode 100644 index 000000000..82bb3ccb6 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/PostEntityIntegrationTests.java @@ -0,0 +1,73 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.util.MutableEntity; +import it.chalmers.gamma.app.post.domain.PostId; +import it.chalmers.gamma.app.post.domain.PostRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; + +import static it.chalmers.gamma.utils.DomainUtils.chair; +import static it.chalmers.gamma.utils.DomainUtils.treasurer; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@ActiveProfiles("test") +@Import({PostRepositoryAdapter.class, + PostEntityConverter.class}) +public class PostEntityIntegrationTests extends AbstractEntityIntegrationTests { + + @Autowired + private PostRepositoryAdapter postRepositoryAdapter; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @Test + public void Given_ValidPost_Expect_save_To_Work() { + this.postRepositoryAdapter.save(chair); + + assertThat(this.postRepositoryAdapter.get(chair.id())) + .get().isEqualTo(chair.withVersion(1)); + } + + @Test + public void Given_SamePost_Expect_save_To_Throw() { + this.postRepositoryAdapter.save(chair); + + assertThatExceptionOfType(MutableEntity.StaleDomainObjectException.class) + .isThrownBy(() -> this.postRepositoryAdapter.save(chair)); + } + + @Test + public void Given_Post_Expect_delete_To_Work() throws PostRepository.PostNotFoundException { + this.postRepositoryAdapter.save(chair); + this.postRepositoryAdapter.delete(chair.id()); + assertThat(this.postRepositoryAdapter.get(chair.id())) + .isEmpty(); + } + + @Test + public void Given_NoPost_Expect_delete_To_Throw() { + assertThatExceptionOfType(PostRepository.PostNotFoundException.class) + .isThrownBy(() -> this.postRepositoryAdapter.delete(PostId.generate())); + } + + @Test + public void Given_TwoPosts_Expect_getAll_To_Work() { + this.postRepositoryAdapter.save(chair); + this.postRepositoryAdapter.save(treasurer); + + assertThat(this.postRepositoryAdapter.getAll()) + .containsExactlyInAnyOrder(chair.withVersion(1), treasurer.withVersion(1)); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/SettingsEntityIntegrationTests.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/SettingsEntityIntegrationTests.java new file mode 100644 index 000000000..43dc35206 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/SettingsEntityIntegrationTests.java @@ -0,0 +1,127 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +import it.chalmers.gamma.adapter.secondary.jpa.settings.SettingsJpaRepository; +import it.chalmers.gamma.adapter.secondary.jpa.settings.SettingsRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupTypeRepositoryAdapter; +import it.chalmers.gamma.app.settings.domain.Settings; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupTypeRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; + +import static it.chalmers.gamma.utils.DomainUtils.board; +import static it.chalmers.gamma.utils.DomainUtils.committee; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@ActiveProfiles("test") +@Import({SettingsRepositoryAdapter.class, + SuperGroupTypeRepositoryAdapter.class}) +public class SettingsEntityIntegrationTests extends AbstractEntityIntegrationTests { + + @Autowired + private SettingsRepositoryAdapter settingsRepositoryAdapter; + + @Autowired + private SettingsJpaRepository settingsJpaRepository; + + @Autowired + private SuperGroupTypeRepository superGroupTypeRepository; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + @Test + public void Given_Settings_Expect_setSettings_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + superGroupTypeRepository.add(committee); + superGroupTypeRepository.add(board); + + Settings settings = new Settings( + Instant.now(), + List.of(committee, board) + ); + + settingsRepositoryAdapter.setSettings(settings); + + assertThat(settingsRepositoryAdapter.getSettings()) + .isEqualTo(settings); + + settingsRepositoryAdapter.setSettings(settings.withLastUpdatedUserAgreement(Instant.now())); + + assertThat(settingsJpaRepository.findAll()) + .hasSize(1); + } + + @Test + public void Given_NoSettings_Expect_getSettings_To_Throw() { + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> settingsRepositoryAdapter.getSettings()); + } + + @Test + public void Given_Settings_Expect_hasSettings_To_Work() { + Settings settings = new Settings( + Instant.now(), + Collections.emptyList() + ); + + settingsRepositoryAdapter.setSettings(settings); + + assertThat(settingsRepositoryAdapter.hasSettings()) + .isTrue(); + } + + @Test + public void Given_InvalidSuperGroupTypes_Expect_setSettings_To_Throw() { + Settings validSettings = new Settings( + Instant.now(), + Collections.emptyList() + ); + + this.settingsRepositoryAdapter.setSettings(validSettings); + + Settings settings = new Settings( + Instant.now(), + List.of(committee, board) + ); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> settingsRepositoryAdapter.setSettings(settings)); + } + + @Test + public void Given_Settings_Expect_setSettingsWithOnlySuperGroupTypes_To_NotUpdateLastUpdatedUserAgreement() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + superGroupTypeRepository.add(committee); + superGroupTypeRepository.add(board); + + Settings settings = new Settings( + Instant.ofEpochSecond(1674809530), + List.of(committee) + ); + + settingsRepositoryAdapter.setSettings(settings); + + assertThat(settingsRepositoryAdapter.getSettings()) + .isEqualTo(settings); + + settingsRepositoryAdapter.setSettings(oldSettings -> oldSettings.withInfoSuperGroupTypes(List.of(board, committee))); + + assertThat(settingsJpaRepository.findAll()) + .hasSize(1); + + assertThat(settingsRepositoryAdapter.getSettings()).isEqualTo(new Settings( + Instant.ofEpochSecond(1674809530), + List.of(board, committee) + )); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/SuperGroupEntityIntegrationTests.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/SuperGroupEntityIntegrationTests.java new file mode 100644 index 000000000..3ff4337c9 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/SuperGroupEntityIntegrationTests.java @@ -0,0 +1,168 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +import it.chalmers.gamma.adapter.secondary.jpa.group.GroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.group.GroupRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.settings.SettingsRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupTypeRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserRepositoryAdapter; +import it.chalmers.gamma.app.authentication.UserAccessGuard; +import it.chalmers.gamma.app.group.domain.GroupRepository; +import it.chalmers.gamma.app.post.domain.PostRepository; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupRepository; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupTypeRepository; +import it.chalmers.gamma.app.user.domain.UserRepository; +import it.chalmers.gamma.security.user.PasswordConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; + +import static it.chalmers.gamma.utils.DomainUtils.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@ActiveProfiles("test") +@Import({SuperGroupRepositoryAdapter.class, + SuperGroupEntityConverter.class, + SuperGroupTypeRepositoryAdapter.class, + GroupRepositoryAdapter.class, + GroupEntityConverter.class, + UserRepositoryAdapter.class, + PasswordConfiguration.class, + UserEntityConverter.class, + UserAccessGuard.class, + PostRepositoryAdapter.class, + PostEntityConverter.class, + SettingsRepositoryAdapter.class +}) +public class SuperGroupEntityIntegrationTests extends AbstractEntityIntegrationTests { + + @Autowired + private SuperGroupRepositoryAdapter superGroupRepositoryAdapter; + @Autowired + private SuperGroupTypeRepository superGroupTypeRepository; + @Autowired + private GroupRepository groupRepository; + @Autowired + private UserRepository userRepository; + @Autowired + private PostRepository postRepository; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @Test + public void Given_ValidSuperGroup_Expect_save_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + superGroupTypeRepository.add(committee); + superGroupRepositoryAdapter.save(digit); + + assertThat(superGroupRepositoryAdapter.get(digit.id())) + .get().isEqualTo(digit.withVersion(1)); + } + + @Test + public void Given_InvalidType_Expect_save_To_Throw() { + assertThatExceptionOfType(SuperGroupRepository.TypeNotFoundRuntimeException.class) + .isThrownBy(() -> superGroupRepositoryAdapter.save(digit)); + } + + @Test + public void Given_SameSuperGroup_Expect_save_To_Throw() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + superGroupTypeRepository.add(committee); + superGroupRepositoryAdapter.save(digit); + + assertThatExceptionOfType(SuperGroupRepository.NameAlreadyExistsRuntimeException.class) + .isThrownBy(() -> superGroupRepositoryAdapter.save(digit.withId(SuperGroupId.generate()))); + } + + @Test + public void Given_ValidSuperGroup_Expect_delete_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, SuperGroupRepository.SuperGroupNotFoundException, SuperGroupRepository.SuperGroupIsUsedException { + superGroupTypeRepository.add(committee); + superGroupRepositoryAdapter.save(digit); + + assertThat(superGroupRepositoryAdapter.get(digit.id())) + .isPresent(); + + superGroupRepositoryAdapter.delete(digit.id()); + + assertThat(superGroupRepositoryAdapter.get(digit.id())) + .isEmpty(); + } + + @Test + public void Given_InvalidSuperGroup_Expect_delete_To_Throw() { + assertThatExceptionOfType(SuperGroupRepository.SuperGroupNotFoundException.class) + .isThrownBy(() -> superGroupRepositoryAdapter.delete(SuperGroupId.generate())); + } + + @Test + public void Given_MultipleSuperGroup_Expect_getAll_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + superGroupTypeRepository.add(committee); + superGroupTypeRepository.add(society); + superGroupTypeRepository.add(alumni); + + superGroupRepositoryAdapter.save(drawit); + superGroupRepositoryAdapter.save(digit); + superGroupRepositoryAdapter.save(didit); + superGroupRepositoryAdapter.save(sprit); + + assertThat(superGroupRepositoryAdapter.getAll()) + .containsExactlyInAnyOrder( + drawit.withVersion(1), + digit.withVersion(1), + didit.withVersion(1), + sprit.withVersion(1) + ); + } + + @Test + public void Given_MultipleSuperGroup_Expect_getAllByType_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + superGroupTypeRepository.add(committee); + superGroupTypeRepository.add(society); + superGroupTypeRepository.add(alumni); + superGroupTypeRepository.add(board); + + superGroupRepositoryAdapter.save(drawit); + superGroupRepositoryAdapter.save(dragit); + superGroupRepositoryAdapter.save(digit); + superGroupRepositoryAdapter.save(didit); + superGroupRepositoryAdapter.save(sprit); + superGroupRepositoryAdapter.save(styrit); + superGroupRepositoryAdapter.save(emeritus); + + assertThat(superGroupRepositoryAdapter.getAllByType(alumni)) + .containsExactlyInAnyOrder( + dragit.withVersion(1), + didit.withVersion(1), + sprit.withVersion(1), + emeritus.withVersion(1) + ); + } + + @Test + public void Given_SuperGroupThatIsUsed_Expect_delete_To_Throw() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + addGroup( + superGroupTypeRepository, + superGroupRepositoryAdapter, + userRepository, + postRepository, + groupRepository, + digit19 + ); + + assertThatExceptionOfType(SuperGroupRepository.SuperGroupIsUsedException.class) + .isThrownBy(() -> superGroupRepositoryAdapter.delete(digit19.superGroup().id())); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/SuperGroupTypeEntityIntegrationTests.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/SuperGroupTypeEntityIntegrationTests.java new file mode 100644 index 000000000..08ab73576 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/SuperGroupTypeEntityIntegrationTests.java @@ -0,0 +1,97 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupTypeRepositoryAdapter; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupRepository; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupType; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupTypeRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; + +import static it.chalmers.gamma.utils.DomainUtils.committee; +import static it.chalmers.gamma.utils.DomainUtils.digit; +import static org.assertj.core.api.Assertions.*; + +@ActiveProfiles("test") +@Import({SuperGroupTypeRepositoryAdapter.class, + SuperGroupRepositoryAdapter.class, + SuperGroupEntityConverter.class}) +public class SuperGroupTypeEntityIntegrationTests extends AbstractEntityIntegrationTests { + + @Autowired + private SuperGroupTypeRepositoryAdapter superGroupTypeRepositoryAdapter; + @Autowired + private SuperGroupRepository superGroupRepository; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @Test + public void Given_ValidType_Expect_save_To_Work() { + SuperGroupType superGroupType = new SuperGroupType("committee"); + + assertThatNoException() + .isThrownBy(() -> superGroupTypeRepositoryAdapter.add(superGroupType)); + } + + @Test + public void Given_ValidType_Expect_delete_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + SuperGroupType superGroupType = new SuperGroupType("committee"); + + superGroupTypeRepositoryAdapter.add(superGroupType); + + assertThatNoException() + .isThrownBy(() -> superGroupTypeRepositoryAdapter.delete(superGroupType)); + + assertThat(superGroupTypeRepositoryAdapter.getAll()) + .isEmpty(); + } + + @Test + public void Given_InvalidType_Expect_delete_To_Throw() { + assertThatExceptionOfType(SuperGroupTypeRepository.SuperGroupTypeNotFoundException.class) + .isThrownBy(() -> superGroupTypeRepositoryAdapter.delete(new SuperGroupType("committee"))); + } + + @Test + public void Given_MultipleTypes_Expect_getAll_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + superGroupTypeRepositoryAdapter.add(new SuperGroupType("committee")); + superGroupTypeRepositoryAdapter.add(new SuperGroupType("alumni")); + superGroupTypeRepositoryAdapter.add(new SuperGroupType("board")); + superGroupTypeRepositoryAdapter.add(new SuperGroupType("emailchain")); + + assertThat(superGroupTypeRepositoryAdapter.getAll()) + .containsExactlyInAnyOrder( + new SuperGroupType("committee"), + new SuperGroupType("alumni"), + new SuperGroupType("board"), + new SuperGroupType("emailchain") + ); + } + + @Test + public void Given_SameType_Expect_add_To_Throw() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + superGroupTypeRepositoryAdapter.add(new SuperGroupType("committee")); + + assertThatExceptionOfType(SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException.class) + .isThrownBy(() -> superGroupTypeRepositoryAdapter.add(new SuperGroupType("committee"))); + } + + @Test + public void Given_TypeThatIsUsed_Expect_delete_To_Throw() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, SuperGroupTypeRepository.SuperGroupTypeNotFoundException, SuperGroupTypeRepository.SuperGroupTypeHasUsagesException { + superGroupTypeRepositoryAdapter.add(committee); + superGroupRepository.save(digit); + + assertThatExceptionOfType(SuperGroupTypeRepository.SuperGroupTypeHasUsagesException.class) + .isThrownBy(() -> superGroupTypeRepositoryAdapter.delete(committee)); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/UserActivationEntityIntegrationTests.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/UserActivationEntityIntegrationTests.java new file mode 100644 index 000000000..49a599040 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/UserActivationEntityIntegrationTests.java @@ -0,0 +1,180 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +import it.chalmers.gamma.adapter.secondary.jpa.user.UserActivationRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.allowlist.AllowListRepositoryAdapter; +import it.chalmers.gamma.app.user.activation.domain.UserActivation; +import it.chalmers.gamma.app.user.activation.domain.UserActivationRepository; +import it.chalmers.gamma.app.user.activation.domain.UserActivationToken; +import it.chalmers.gamma.app.user.domain.Cid; +import it.chalmers.gamma.app.user.allowlist.AllowListRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +@ActiveProfiles("test") +@Import({UserActivationRepositoryAdapter.class, + AllowListRepositoryAdapter.class}) +public class UserActivationEntityIntegrationTests extends AbstractEntityIntegrationTests { + + @Autowired + private UserActivationRepositoryAdapter userActivationRepositoryAdapter; + + @Autowired + private AllowListRepository allowListRepository; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @Test + void Given_ValidCid_Expect_createActivationToken_To_CreateAValidUserActivationToken() throws AllowListRepository.AlreadyAllowedException { + Cid asdf = new Cid("asdf"); + + allowListRepository.allow(asdf); + + assertThatNoException() + .isThrownBy(() -> { + UserActivationToken userActivationToken = userActivationRepositoryAdapter.createActivationToken(asdf); + assertThat(userActivationToken) + .isNotNull() + .extracting(UserActivationToken::value) + .isNotNull() + .isNotEqualTo(""); + }); + } + + @Test + public void Given_ValidCid_Expect_removeActivation_To_Work() throws AllowListRepository.AlreadyAllowedException, UserActivationRepository.CidNotAllowedException { + Cid asdf = new Cid("asdf"); + + allowListRepository.allow(asdf); + + userActivationRepositoryAdapter.createActivationToken(asdf); + + assertThatNoException() + .isThrownBy(() -> userActivationRepositoryAdapter.removeActivation(asdf)); + + assertThat(userActivationRepositoryAdapter.get(asdf)) + .isEmpty(); + } + + @Test + public void Given_MultipleActivations_Expect_getAll_To_ReturnThem() throws AllowListRepository.AlreadyAllowedException, UserActivationRepository.CidNotAllowedException { + Cid cid1 = new Cid("asdf"), + cid2 = new Cid("fdas"), + cid3 = new Cid("lmao"); + + allowListRepository.allow(cid1); + allowListRepository.allow(cid2); + allowListRepository.allow(cid3); + + userActivationRepositoryAdapter.createActivationToken(cid1); + userActivationRepositoryAdapter.createActivationToken(cid2); + userActivationRepositoryAdapter.createActivationToken(cid3); + + List activations = userActivationRepositoryAdapter.getAll(); + + //Make sure that the activations have the cids + assertThat(activations) + .hasSize(3) + .extracting(UserActivation::cid) + .containsExactlyInAnyOrder(cid1, cid2, cid3); + + //Make sure that the activations are unique + assertThat(activations) + .extracting(UserActivation::token) + .doesNotHaveDuplicates(); + } + + @Test + public void Given_ValidCid_Expect_get_To_ReturnCorrespondingActivation() throws AllowListRepository.AlreadyAllowedException, UserActivationRepository.CidNotAllowedException { + Cid cid1 = new Cid("asdf"); + + allowListRepository.allow(cid1); + + userActivationRepositoryAdapter.createActivationToken(cid1); + + assertThat(userActivationRepositoryAdapter.get(cid1)) + .isNotEmpty() + .get() + .extracting(UserActivation::cid) + .isEqualTo(cid1); + } + + @Test + public void Given_ValidCid_Expect_getByToken_To_ReturnCorrespondingActivation() throws AllowListRepository.AlreadyAllowedException, UserActivationRepository.CidNotAllowedException { + Cid cid1 = new Cid("asdf"); + + allowListRepository.allow(cid1); + + UserActivationToken userActivationToken = userActivationRepositoryAdapter.createActivationToken(cid1); + + assertThat(userActivationRepositoryAdapter.getByToken(userActivationToken)) + .isEqualTo(cid1); + } + + @Test + public void Given_CidThatIsNotAllowed_Expect_createActivationToken_To_Throw() { + Cid cid1 = new Cid("asdf"); + + assertThatExceptionOfType(UserActivationRepository.CidNotAllowedException.class) + .isThrownBy(() -> userActivationRepositoryAdapter.createActivationToken(cid1)); + } + + @Test + public void Given_InvalidCid_Expect_get_To_ReturnEmpty() throws AllowListRepository.AlreadyAllowedException, UserActivationRepository.CidNotAllowedException { + Cid cid1 = new Cid("asdf"), + cid2 = new Cid("fdsa"); + + allowListRepository.allow(cid1); + allowListRepository.allow(cid2); + + userActivationRepositoryAdapter.createActivationToken(cid1); + + assertThat(userActivationRepositoryAdapter.get(cid2)) + .isEmpty(); + } + + @Test + public void Given_NoActivatedUser_Expect_removeActivation_To_Throw() { + Cid cid1 = new Cid("asdf"); + + } + + @Test + public void Given_AnAlreadyActivatedCid_Expect_createActivationToken_To_GenerateANewToken() throws AllowListRepository.AlreadyAllowedException, UserActivationRepository.CidNotAllowedException { + Cid cid1 = new Cid("asdf"); + + allowListRepository.allow(cid1); + + UserActivationToken token1 = userActivationRepositoryAdapter.createActivationToken(cid1); + UserActivationToken token2 = userActivationRepositoryAdapter.createActivationToken(cid1); + + assertThat(token2) + .isNotNull(); + + assertThat(token1) + .isNotNull() + .isNotEqualTo(token2); + } + + @Test + public void Given_InvalidCid_Expect_getByToken_To_Throw() throws AllowListRepository.AlreadyAllowedException, UserActivationRepository.CidNotAllowedException { + Cid cid1 = new Cid("asdf"); + allowListRepository.allow(cid1); + this.userActivationRepositoryAdapter.createActivationToken(cid1); + + assertThatExceptionOfType(UserActivationRepository.TokenNotActivatedException.class) + .isThrownBy(() -> userActivationRepositoryAdapter.getByToken(UserActivationToken.generate())); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/UserEntityIntegrationTests.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/UserEntityIntegrationTests.java new file mode 100644 index 000000000..99f23cbdc --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/UserEntityIntegrationTests.java @@ -0,0 +1,310 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.settings.SettingsRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserRepositoryAdapter; +import it.chalmers.gamma.app.authentication.UserAccessGuard; +import it.chalmers.gamma.utils.PasswordEncoderTestConfiguration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@Import({UserRepositoryAdapter.class, + UserEntityConverter.class, + UserAccessGuard.class, + ClientAuthorityRepositoryAdapter.class, + ClientAuthorityEntityConverter.class, + SuperGroupEntityConverter.class, + PostEntityConverter.class, + SettingsRepositoryAdapter.class, + PasswordEncoderTestConfiguration.class}) +public class UserEntityIntegrationTests extends AbstractEntityIntegrationTests { + + /* + @Autowired + private UserRepositoryAdapter userRepositoryAdapter; + @Autowired + private ClientAuthorityRepositoryAdapter authorityLevelRepositoryAdapter; + + @Autowired + private SettingsRepository settingsRepository; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @BeforeEach + public void setupSettings() { + this.settingsRepository.setSettings(new Settings( + Instant.ofEpochSecond(0), + Collections.emptyList() + )); + } + + @Test + public void Given_ValidNewUser_Expect_create_To_Work() { + GammaUser user = setAuthenticatedAsNormalUser(userRepositoryAdapter); + + assertThat(this.userRepositoryAdapter.get(user.id())) + .get() + .isEqualTo(user.withExtended(user.extended().withVersion(1))); + } + + @Test + public void Given_UserWithoutExtended_Expect_create_To_Throw() { + GammaUser user = DEFAULT_USER.withExtended(null); + + assertThatIllegalStateException() + .isThrownBy(() -> this.userRepositoryAdapter.create( + user, + new UnencryptedPassword("password") + )); + } + + @Test + public void Given_ValidUser_Expect_save_To_Work() { + //Will save the user + GammaUser user = setAuthenticatedAsNormalUser(userRepositoryAdapter); + + GammaUser newUser = user.with() + .nick(new Nick("Smurf_RandoM")) + .acceptanceYear(new AcceptanceYear(2020)) + .build(); + + this.userRepositoryAdapter.save(newUser); + + assertThat(this.userRepositoryAdapter.get(DEFAULT_USER.id())) + .get() + .isEqualTo(newUser.withExtended(newUser.extended().withVersion(2))); + } + + @Test + public void Given_UserWithNewUserId_Expect_save_To_Throw() throws UserRepository.CidAlreadyInUseException, UserRepository.EmailAlreadyInUseException { + GammaUser user = setAuthenticatedAsNormalUser(userRepositoryAdapter); + this.userRepositoryAdapter.create(user, new UnencryptedPassword("password")); + GammaUser loadedUser = this.userRepositoryAdapter.get(user.id()).orElseThrow(); + assertThatExceptionOfType(MutableEntity.IllegalEntityStateException.class) + .isThrownBy(() -> this.userRepositoryAdapter.save(loadedUser.withId(UserId.generate()))); + } + + @Test + public void Given_ValidUser_Expect_get_To_HaveAUpdatedVersion() { + UserId userId = setAuthenticatedAsNormalUser(userRepositoryAdapter).id(); + GammaUser loadedUser = this.userRepositoryAdapter.get(userId).orElseThrow(); + assertThat(loadedUser.extended().version()) + .isEqualTo(1); + } + + @Test + public void Given_UserWithoutExtended_Expect_save_To_Throw() { + GammaUser user = DEFAULT_USER.withExtended(null); + + assertThatIllegalStateException() + .isThrownBy(() -> this.userRepositoryAdapter.create( + user, + new UnencryptedPassword("password") + )); + } + + @Test + public void Given_User_Expect_setPassword_and_checkPassword_To_Work() throws UserRepository.CidAlreadyInUseException, UserRepository.EmailAlreadyInUseException { + GammaUser user = DEFAULT_USER; + this.userRepositoryAdapter.create(user, new UnencryptedPassword("password")); + assertThat(this.userRepositoryAdapter.checkPassword(user.id(), new UnencryptedPassword("password"))) + .isTrue(); + + this.userRepositoryAdapter.setPassword(user.id(), new UnencryptedPassword("new_password")); + assertThat(this.userRepositoryAdapter.checkPassword(user.id(), new UnencryptedPassword("new_password"))) + .isTrue(); + } + + @Test + public void Given_User_Expect_setPassword_and_wrong_checkPassword_To_Return_False() throws UserRepository.CidAlreadyInUseException, UserRepository.EmailAlreadyInUseException { + GammaUser user = DEFAULT_USER; + this.userRepositoryAdapter.create(user, new UnencryptedPassword("password")); + assertThat(this.userRepositoryAdapter.checkPassword(user.id(), new UnencryptedPassword("_password"))) + .isFalse(); + + this.userRepositoryAdapter.setPassword(user.id(), new UnencryptedPassword("new_password")); + assertThat(this.userRepositoryAdapter.checkPassword(user.id(), new UnencryptedPassword("new_password_"))) + .isFalse(); + } + + @Test + public void Given_ValidUser_Expect_acceptUserAgreement_To_Work() { + GammaUser user = DEFAULT_USER.withExtended(DEFAULT_USER.extended().withAcceptedUserAgreement(false)); + + setAuthenticatedUser( + userRepositoryAdapter, + authorityLevelRepositoryAdapter, + user, + false + ); + + GammaUser updatedUser = this.userRepositoryAdapter.get(user.id()).orElseThrow(); + assertThat(updatedUser.extended().acceptedUserAgreement()) + .isFalse(); + + this.userRepositoryAdapter.acceptUserAgreement(user.id()); + updatedUser = this.userRepositoryAdapter.get(user.id()).orElseThrow(); + assertThat(updatedUser.extended().acceptedUserAgreement()) + .isTrue(); + } + + @Test + public void Given_OneUserTryingToAccessLockedUser_Expect_get_To_Return_Null() throws UserRepository.CidAlreadyInUseException, UserRepository.EmailAlreadyInUseException { + setAuthenticatedAsNormalUser(userRepositoryAdapter); + + GammaUser lockedUser = new GammaUser( + UserId.generate(), + new Cid("hmmm"), + new Nick("SomethinG"), + new FirstName("Smurf"), + new LastName("Smurfsson"), + new AcceptanceYear(2018), + Language.EN, + new UserExtended( + new Email("somthing@chalmers.it"), + 0, + true, + false, + true, + ImageUri.defaultUserAvatar() + ) + ); + + this.userRepositoryAdapter.create(lockedUser, new UnencryptedPassword("password")); + + assertThat(this.userRepositoryAdapter.get(lockedUser.id())) + .isEmpty(); + } + + @Test + public void Given_OneUserTryingToAccessUserThatHaveNotAcceptedUserAgreement_Expect_get_To_Return_Null() throws UserRepository.CidAlreadyInUseException, UserRepository.EmailAlreadyInUseException { + setAuthenticatedAsNormalUser(userRepositoryAdapter); + + GammaUser userThatHasNotAcceptedUserAgreement = new GammaUser( + UserId.generate(), + new Cid("hmmm"), + new Nick("SomethinG"), + new FirstName("Smurf"), + new LastName("Smurfsson"), + new AcceptanceYear(2018), + Language.EN, + new UserExtended( + new Email("somthing@chalmers.it"), + 0, + false, + false, + false, + ImageUri.defaultUserAvatar() + ) + ); + + this.userRepositoryAdapter.create(userThatHasNotAcceptedUserAgreement, new UnencryptedPassword("password")); + + assertThat(this.userRepositoryAdapter.get(userThatHasNotAcceptedUserAgreement.id())) + .isEmpty(); + } + + @Test + public void Given_AdminTryingToAccessUserThatHaveNotAcceptedUserAgreement_Expect_get_To_Work() throws UserRepository.CidAlreadyInUseException, UserRepository.EmailAlreadyInUseException { + setAuthenticatedAsAdminUser(userRepositoryAdapter, authorityLevelRepositoryAdapter); + + GammaUser userThatHasNotAcceptedUserAgreement = new GammaUser( + UserId.generate(), + new Cid("hmmm"), + new Nick("SomethinG"), + new FirstName("Smurf"), + new LastName("Smurfsson"), + new AcceptanceYear(2018), + Language.EN, + new UserExtended( + new Email("somthing@chalmers.it"), + 0, + false, + false, + false, + ImageUri.defaultUserAvatar() + ) + ); + + this.userRepositoryAdapter.create(userThatHasNotAcceptedUserAgreement, new UnencryptedPassword("password")); + + assertThat(this.userRepositoryAdapter.get(userThatHasNotAcceptedUserAgreement.id())) + .get().isEqualTo( + userThatHasNotAcceptedUserAgreement + .withExtended(userThatHasNotAcceptedUserAgreement.extended().withVersion(1)) + ); + } + + @Test + public void Given_AdminTryingToAccessLockedUser_Expect_get_To_Work() throws UserRepository.CidAlreadyInUseException, UserRepository.EmailAlreadyInUseException { + setAuthenticatedAsAdminUser(userRepositoryAdapter, authorityLevelRepositoryAdapter); + + GammaUser lockedUser = new GammaUser( + UserId.generate(), + new Cid("hmmm"), + new Nick("SomethinG"), + new FirstName("Smurf"), + new LastName("Smurfsson"), + new AcceptanceYear(2018), + Language.EN, + new UserExtended( + new Email("somthing@chalmers.it"), + 0, + true, + false, + true, + ImageUri.defaultUserAvatar() + ) + ); + + this.userRepositoryAdapter.create(lockedUser, new UnencryptedPassword("password")); + + assertThat(this.userRepositoryAdapter.get(lockedUser.id())) + .get().isEqualTo( + lockedUser + .withExtended(lockedUser.extended().withVersion(1)) + ); + } + + @Test + public void Given_TwoUsersWithSameCid_Expect_create_To_Throw() throws UserRepository.CidAlreadyInUseException, UserRepository.EmailAlreadyInUseException { + this.userRepositoryAdapter.create(DEFAULT_USER, new UnencryptedPassword("password")); + + assertThatExceptionOfType(UserRepository.CidAlreadyInUseException.class) + .isThrownBy(() -> this.userRepositoryAdapter.create( + DEFAULT_USER.with() + .id(UserId.generate()) + .extended(DEFAULT_USER.extended().with() + .email(new Email("lmao@chalmers.it")) + .build() + ) + .build(), + new UnencryptedPassword("password") + )); + } + + @Test + public void Given_TwoUsersWithSameEmail_Expect_create_To_Throw() throws UserRepository.CidAlreadyInUseException, UserRepository.EmailAlreadyInUseException { + this.userRepositoryAdapter.create(DEFAULT_USER, new UnencryptedPassword("password")); + + assertThatExceptionOfType(UserRepository.EmailAlreadyInUseException.class) + .isThrownBy(() -> this.userRepositoryAdapter.create( + DEFAULT_USER.with() + .id(UserId.generate()) + .cid(new Cid("somethi")) + .build(), + new UnencryptedPassword("password") + )); + } + + */ +} diff --git a/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/UserPasswordRetrieverEntityIntegrationTests.java b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/UserPasswordRetrieverEntityIntegrationTests.java new file mode 100644 index 000000000..180342b2d --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/adapter/secondary/jpa/UserPasswordRetrieverEntityIntegrationTests.java @@ -0,0 +1,100 @@ +package it.chalmers.gamma.adapter.secondary.jpa; + +import it.chalmers.gamma.adapter.secondary.jpa.settings.SettingsRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserPasswordRetrieverAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserRepositoryAdapter; +import it.chalmers.gamma.app.authentication.UserAccessGuard; +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.image.domain.ImageUri; +import it.chalmers.gamma.app.settings.domain.Settings; +import it.chalmers.gamma.app.settings.domain.SettingsRepository; +import it.chalmers.gamma.app.user.domain.*; +import it.chalmers.gamma.security.user.UserPasswordRetriever; +import it.chalmers.gamma.utils.PasswordEncoderTestConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.ActiveProfiles; + +import java.time.Instant; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@ActiveProfiles("test") +@Import({UserPasswordRetrieverAdapter.class, + UserRepositoryAdapter.class, + UserEntityConverter.class, + UserAccessGuard.class, + SettingsRepositoryAdapter.class, + PasswordEncoderTestConfiguration.class}) +public class UserPasswordRetrieverEntityIntegrationTests extends AbstractEntityIntegrationTests { + + @Autowired + private UserPasswordRetrieverAdapter userPasswordRetrieverAdapter; + @Autowired + private UserRepository userRepository; + @Autowired + private SettingsRepository settingsRepository; + @Autowired + private PasswordEncoder passwordEncoder; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @Test + public void Given_ValidUser_Expect_getPassword_To_Work() throws UserRepository.CidAlreadyInUseException, UserRepository.EmailAlreadyInUseException { + settingsRepository.setSettings(new Settings( + Instant.now(), + Collections.emptyList() + )); + + UserId userId = UserId.generate(); + this.userRepository.create( + new GammaUser( + userId, + new Cid("asdf"), + new Nick("RandoM"), + new FirstName("Smurf"), + new LastName("Smurfsson"), + new AcceptanceYear(2018), + Language.EN, + new UserExtended( + new Email("smurf@chalmers.it"), + 0, + true, + false, + ImageUri.defaultUserAvatar() + ) + ), + new UnencryptedPassword("password") + ); + + Password password = this.userPasswordRetrieverAdapter.getPassword(userId); + + assertThat(passwordEncoder.matches("password", password.value())) + .isTrue(); + } + + @Test + public void Given_InvalidUser_Expect_getPassword_To_Throw() { + settingsRepository.setSettings(new Settings( + Instant.now(), + Collections.emptyList() + )); + + UserId userId = UserId.generate(); + + assertThatExceptionOfType(UserPasswordRetriever.UserNotFoundException.class) + .isThrownBy(() -> this.userPasswordRetrieverAdapter.getPassword(userId)); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/app/apikey/ApiKeyFacadeIntegrationTest.java b/backend/src/test/java/it/chalmers/gamma/app/apikey/ApiKeyFacadeIntegrationTest.java new file mode 100644 index 000000000..fce939bfa --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/app/apikey/ApiKeyFacadeIntegrationTest.java @@ -0,0 +1,147 @@ +package it.chalmers.gamma.app.apikey; + +import it.chalmers.gamma.adapter.secondary.jpa.apikey.ApiKeyEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.apikey.ApiKeyRepositoryAdapter; +import it.chalmers.gamma.app.authentication.AccessGuard; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@ActiveProfiles("test") +@DataJpaTest +@Import({ApiKeyFacade.class, + ApiKeyRepositoryAdapter.class, + ApiKeyEntityConverter.class}) +@Testcontainers +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public class ApiKeyFacadeIntegrationTest { + + @MockBean + private AccessGuard accessGuard; + + @Autowired + private ApiKeyFacade apiKeyFacade; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + @Test + public void Given_ValidNewApiKey_Expect_create_To_Work() { + ApiKeyFacade.NewApiKey newApiKey = new ApiKeyFacade.NewApiKey( + "My api key", + "Svenska", + "English", + "INFO" + ); + + apiKeyFacade.create(newApiKey); + + List apiKeys = apiKeyFacade.getAll(); + + assertThat(apiKeys) + .hasSize(1) + .allSatisfy(apiKeyDTO -> { + assertThat(apiKeyDTO) + .isEqualTo(new ApiKeyFacade.ApiKeyDTO( + apiKeyDTO.id(), + newApiKey.prettyName(), + newApiKey.svDescription(), + newApiKey.enDescription(), + "INFO" + )); + }); + + assertThat(apiKeyFacade.getById(apiKeys.get(0).id())) + .isPresent(); + } + + @Test + public void Given_TwoApiKeys_Expect_create_To_Work() { + ApiKeyFacade.NewApiKey newApiKey = new ApiKeyFacade.NewApiKey( + "My api key", + "Svenska", + "English", + "INFO" + ); + + apiKeyFacade.create(newApiKey); + apiKeyFacade.create(newApiKey); + + List apiKeys = apiKeyFacade.getAll(); + + assertThat(apiKeys) + .hasSize(2); + + //Different id + assertThat(apiKeys.get(0)) + .isNotEqualTo(apiKeys.get(1)); + } + + @Test + public void Given_ValidApiKey_Expect_delete_To_Work() throws ApiKeyFacade.ApiKeyNotFoundException { + ApiKeyFacade.NewApiKey newApiKey = new ApiKeyFacade.NewApiKey( + "My api key", + "Svenska", + "English", + "INFO" + ); + + apiKeyFacade.create(newApiKey); + List apiKeys = apiKeyFacade.getAll(); + assertThat(apiKeys) + .hasSize(1); + + UUID id = apiKeys.get(0).id(); + apiKeyFacade.delete(id); + assertThat(apiKeyFacade.getAll()) + .isEmpty(); + } + + @Test + public void Given_NoApiKeys_Expect_delete_To_Throw() { + assertThatExceptionOfType(ApiKeyFacade.ApiKeyNotFoundException.class) + .isThrownBy(() -> apiKeyFacade.delete(UUID.randomUUID())); + } + + @Test + public void Given_ValidApiKey_Expect_resetApiKeyToken_To_Work() throws ApiKeyFacade.ApiKeyNotFoundException { + ApiKeyFacade.NewApiKey newApiKey = new ApiKeyFacade.NewApiKey( + "My api key", + "Svenska", + "English", + "INFO" + ); + + String token = apiKeyFacade.create(newApiKey); + List apiKeys = apiKeyFacade.getAll(); + assertThat(apiKeys) + .hasSize(1); + + UUID id = apiKeys.get(0).id(); + String newToken = apiKeyFacade.resetApiKeyToken(id); + + assertThat(token) + .isNotNull(); + assertThat(newToken) + .isNotNull(); + + assertThat(token) + .isNotEqualTo(newToken); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/app/apikey/ApiKeyFacadeUnitTest.java b/backend/src/test/java/it/chalmers/gamma/app/apikey/ApiKeyFacadeUnitTest.java new file mode 100644 index 000000000..ee170bc66 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/app/apikey/ApiKeyFacadeUnitTest.java @@ -0,0 +1,366 @@ +package it.chalmers.gamma.app.apikey; + +import it.chalmers.gamma.app.apikey.domain.*; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +@ExtendWith(SpringExtension.class) +class ApiKeyFacadeUnitTest { + + @Mock + private AccessGuard accessGuard; + + @Mock + private ApiKeyRepository apiKeyRepository; + + @InjectMocks + private ApiKeyFacade apiKeyFacade; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + /** + * Creating a new type of api key should not be an easy decision. + * Make sure that you really need to expose more data. + * Note that adding a new api key type means that you probably need + * to update your user agreement. + */ + @Test + public void EnsureThereIsOnlyFourApiKeyTypes() { + String[] expected = new String[]{ + "CLIENT", + "GOLDAPPS", + "INFO", + "ALLOW_LIST" + }; + + assertThat(apiKeyFacade.getApiKeyTypes()) + .isEqualTo(expected); + } + + /** + * Main test for create() that checks that isAdmin is called and that the generated token is also returned properly. + */ + @Test + public void Given_AValidApiKey_Expect_create_To_CreateValidApiKey() throws ApiKeyRepository.ApiKeyAlreadyExistRuntimeException { + ApiKeyFacade.NewApiKey newApiKey = new ApiKeyFacade.NewApiKey( + "My Api Key", + "Det här är en test api nyckel", + "This is my test api key", + "CLIENT" + ); + + String apiKeyTokenRaw = apiKeyFacade.create(newApiKey); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ApiKey.class); + verify(apiKeyRepository).create(captor.capture()); + ApiKey capturedApiKey = captor.getValue(); + + //If not null, then they are valid. + assertThat(capturedApiKey.id()) + .isNotNull(); + assertThat(capturedApiKey.apiKeyToken()) + .isNotNull(); + + //Makes sure that the token returned is the same that is sent to the repository + assertThat(apiKeyTokenRaw) + .isEqualTo(capturedApiKey.apiKeyToken().value()); + + ApiKey expectedApiKey = new ApiKey( + capturedApiKey.id(), + new PrettyName(newApiKey.prettyName()), + new Text( + newApiKey.svDescription(), + newApiKey.enDescription() + ), + ApiKeyType.CLIENT, + capturedApiKey.apiKeyToken() + ); + + + assertThat(capturedApiKey) + .isEqualTo(expectedApiKey); + + InOrder inOrder = inOrder(accessGuard, apiKeyRepository); + + //Makes sure that isAdmin is called first. + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(apiKeyRepository).create(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_NullPrettyName_Expect_create_To_Throw() { + ApiKeyFacade.NewApiKey newApiKey = new ApiKeyFacade.NewApiKey( + null, + "Det här är en test api nyckel", + "This is my test api key", + "CLIENT" + ); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> apiKeyFacade.create(newApiKey)); + } + + @Test + public void Given_InvalidPrettyName_Expect_create_To_Throw() { + ApiKeyFacade.NewApiKey newApiKey = new ApiKeyFacade.NewApiKey( + "T", + "Det här är en test api nyckel", + "This is my test api key", + "CLIENT" + ); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> apiKeyFacade.create(newApiKey)); + } + + @Test + public void Given_NullText_Expect_create_To_Throw() { + ApiKeyFacade.NewApiKey newApiKey = new ApiKeyFacade.NewApiKey( + "My Api Key", + null, + null, + "CLIENT" + ); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> apiKeyFacade.create(newApiKey)); + } + + @Test + public void Given_InvalidApiKeyType_Expect_create_ToThrow() { + ApiKeyFacade.NewApiKey newApiKey = new ApiKeyFacade.NewApiKey( + "My Api Key", + "Det här är en test api nyckel", + "This is my test api key", + "LMAO" + ); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> apiKeyFacade.create(newApiKey)); + + ApiKeyFacade.NewApiKey newApiKey2 = new ApiKeyFacade.NewApiKey( + "My Api Key", + "Det här är en test api nyckel", + "This is my test api key", + "client" + ); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> apiKeyFacade.create(newApiKey)); + } + + @Test + public void Given_AValidApiKeyId_Expect_delete_To_DeleteApiKey() + throws ApiKeyRepository.ApiKeyNotFoundException, ApiKeyFacade.ApiKeyNotFoundException { + UUID id = UUID.randomUUID(); + + apiKeyFacade.delete(id); + + InOrder inOrder = inOrder(accessGuard, apiKeyRepository); + + //Makes sure that isAdmin is called first. + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(apiKeyRepository).delete(new ApiKeyId(id)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_InvalidApiKey_Expect_delete_To_Throw() throws ApiKeyRepository.ApiKeyNotFoundException { + doThrow(ApiKeyRepository.ApiKeyNotFoundException.class) + .when(apiKeyRepository).delete(any()); + + assertThatExceptionOfType(ApiKeyFacade.ApiKeyNotFoundException.class) + .isThrownBy(() -> apiKeyFacade.delete(UUID.randomUUID())); + } + + @Test + public void Given_AValidApiKeyId_Expect_getById_To_ReturnApiKey() { + UUID id = UUID.randomUUID(); + + ApiKey apiKey = new ApiKey( + new ApiKeyId(id), + new PrettyName("My Api Key"), + new Text( + "Det här är en test api nyckel", + "This is my test api key" + ), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ); + + given(this.apiKeyRepository.getById(apiKey.id())) + .willReturn(Optional.of(apiKey)); + + Optional maybeApiKey = apiKeyFacade.getById(id); + + ApiKeyDTOAssert.assertThat(maybeApiKey) + .isEqualTo(apiKey); + + InOrder inOrder = inOrder(accessGuard, apiKeyRepository); + + //Makes sure that isAdmin is called first. + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(apiKeyRepository).getById(apiKey.id()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_InvalidApiKeyId_Expect_getById_To_ReturnEmptyOptional() { + UUID id = UUID.randomUUID(); + ApiKeyId apiKeyId = new ApiKeyId(id); + + given(this.apiKeyRepository.getById(apiKeyId)) + .willReturn(Optional.empty()); + + assertThat(this.apiKeyFacade.getById(id)) + .isEmpty(); + } + + @Test + public void Given_ExistingApiKeys_Expect_getAll_To_ThoseApiKeys() { + ApiKey apiKey1 = new ApiKey( + ApiKeyId.generate(), + new PrettyName("My Api Key"), + new Text( + "Det här är en test api nyckel", + "This is my test api key" + ), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ); + + ApiKey apiKey2 = new ApiKey( + ApiKeyId.generate(), + new PrettyName("My Api Key 2"), + new Text( + "Det här är en test api nyckel 2", + "This is my test api key 2" + ), + ApiKeyType.INFO, + ApiKeyToken.generate() + ); + + List apiKeys = List.of(apiKey1, apiKey2); + + given(this.apiKeyRepository.getAll()) + .willReturn(apiKeys); + + List apiKeyDTOs = apiKeyFacade.getAll(); + + assertThat(apiKeyDTOs.size()) + .isEqualTo(2); + + for (int i = 0; i < apiKeys.size(); i++) { + ApiKeyFacade.ApiKeyDTO apiKeyDTO = apiKeyDTOs.get(i); + ApiKey apiKey = apiKeys.get(i); + + ApiKeyDTOAssert.assertThat(apiKeyDTO) + .isEqualTo(apiKey); + } + + InOrder inOrder = inOrder(accessGuard, apiKeyRepository); + + //Makes sure that isAdmin is called first. + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(apiKeyRepository).getAll(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_AValidApiKeyId_Expect_resetApiKeyToken_To_ResetSuccessfully() throws ApiKeyFacade.ApiKeyNotFoundException, ApiKeyRepository.ApiKeyAlreadyExistRuntimeException, ApiKeyRepository.ApiKeyNotFoundException { + ApiKeyId apiKeyId = ApiKeyId.generate(); + ApiKeyToken previousToken = ApiKeyToken.generate(); + ApiKey apiKey = new ApiKey( + apiKeyId, + new PrettyName("My Api Key"), + new Text( + "Det här är en test api nyckel", + "This is my test api key" + ), + ApiKeyType.CLIENT, + previousToken + ); + + given(this.apiKeyRepository.getById(apiKeyId)) + .willReturn(Optional.of(apiKey)); + ApiKeyToken generatedToken = ApiKeyToken.generate(); + given(this.apiKeyRepository.resetApiKeyToken(apiKeyId)) + .willReturn(generatedToken); + + + String newToken = this.apiKeyFacade.resetApiKeyToken(apiKeyId.value()); + ApiKeyToken newApiKeyToken = new ApiKeyToken(newToken); + + assertThat(newApiKeyToken) + .isEqualTo(generatedToken); + + InOrder inOrder = inOrder(accessGuard, apiKeyRepository); + + //Makes sure that isAdmin is called first. + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(apiKeyRepository).resetApiKeyToken(apiKeyId); + inOrder.verifyNoMoreInteractions(); + } + + public static class ApiKeyDTOAssert extends AbstractAssert { + protected ApiKeyDTOAssert(ApiKeyFacade.ApiKeyDTO actual) { + super(actual, ApiKeyDTOAssert.class); + } + + public static ApiKeyDTOAssert assertThat(ApiKeyFacade.ApiKeyDTO actual) { + return new ApiKeyDTOAssert(actual); + } + + public static ApiKeyDTOAssert assertThat(Optional actual) { + if (actual.isEmpty()) { + fail("Optional is empty"); + return null; + } + return new ApiKeyDTOAssert(actual.get()); + } + + public ApiKeyDTOAssert isEqualTo(ApiKey apiKey) { + isNotNull(); + + Assertions.assertThat(actual) + .hasOnlyFields("id", "prettyName", "svDescription", "enDescription", "keyType") + .isEqualTo( + new ApiKeyFacade.ApiKeyDTO( + apiKey.id().value(), + apiKey.prettyName().value(), + apiKey.description().sv().value(), + apiKey.description().en().value(), + apiKey.keyType().name() + ) + ); + + return this; + } + + } + +} \ No newline at end of file diff --git a/backend/src/test/java/it/chalmers/gamma/app/authentication/AccessGuardTest.java b/backend/src/test/java/it/chalmers/gamma/app/authentication/AccessGuardTest.java new file mode 100644 index 000000000..3160022ed --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/app/authentication/AccessGuardTest.java @@ -0,0 +1,675 @@ +package it.chalmers.gamma.app.authentication; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.mockito.Mockito.mockStatic; + + +@ExtendWith(SpringExtension.class) +class AccessGuardTest { + + /* + private static final GammaUser adminUser = new GammaUser( + UserId.generate(), + new Cid("edcba"), + new Nick("TheAdmin"), + new FirstName("Something1"), + new LastName("Somethingsson1"), + new AcceptanceYear(2021), + Language.EN, + new UserExtended( + new Email("edcba@chalmers.it"), + 0, + true, + false, + false, + null + ) + ); + private static final GammaUser normalUser = new GammaUser( + UserId.generate(), + new Cid("edcba"), + new Nick("TheUser"), + new FirstName("Something1"), + new LastName("Somethingsson1"), + new AcceptanceYear(2021), + Language.EN, + new UserExtended( + new Email("edcba@chalmers.it"), + 0, + true, + false, + false, + null + ) + ); + private static final Post member = new Post( + PostId.generate(), + 0, + new Text( + "Ledamot", + "Member" + ), + new EmailPrefix("ledamot") + ); + private static final SuperGroupType committee = new SuperGroupType("committee"); + private static final SuperGroup digIT = new SuperGroup( + SuperGroupId.generate(), + 0, + new Name("digit"), + new PrettyName("digIT"), + committee, + new Text( + "Hanterar sektionens digitala system", + "Manages the student divisions digital systems" + ) + ); + private static final Group digIT18 = new Group( + GroupId.generate(), + 0, + new Name("digit18"), + new PrettyName("digIT' 18"), + digIT, + Collections.singletonList( + new GroupMember( + member, + new UnofficialPostName("ServerChef"), + normalUser + ) + ), + Optional.empty(), + Optional.empty() + ); + private static final AuthorityName digIt18Authority = new AuthorityName(digIT18.name().value()); + private static final Group digIT19 = new Group( + GroupId.generate(), + 0, + new Name("digit19"), + new PrettyName("digIT' 19"), + digIT, + Collections.emptyList(), + Optional.empty(), + Optional.empty() + ); + private static final AuthorityName digitAuthority = new AuthorityName(digIT.name().value()); + private static final AuthorityName adminAuthority = new AuthorityName("admin"); + private static final AuthorityName matAuthority = new AuthorityName("mat"); + private static final Map> userAuthoritiesMap = new HashMap<>() {{ + put(normalUser.id(), List.of( + new UserAuthority(matAuthority, AuthorityType.AUTHORITY), + new UserAuthority(digitAuthority, AuthorityType.SUPERGROUP), + new UserAuthority(digIt18Authority, AuthorityType.GROUP) + )); + put(adminUser.id(), List.of( + new UserAuthority(adminAuthority, AuthorityType.AUTHORITY) + )); + }}; + private static final ApiKey clientApiKey = new ApiKey( + ApiKeyId.generate(), + new PrettyName("My api key"), + new Text( + "Det här är min api nyckel", + "This is my api key" + ), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ); + private static final Client client = new Client( + ClientUid.generate(), + ClientId.generate(), + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat client"), + new Text( + "Det här är mat klienten", + "This is the mat client" + ), + Collections.singletonList(Scope.PROFILE), + clientApiKey, + new ClientOwnerOfficial() + ); + private static final ApiAuthentication CLIENT_API_DETAILS = new ApiAuthentication() { + @Override + public ApiKey get() { + return clientApiKey; + } + + @Override + public Optional getClient() { + return Optional.of(client); + } + }; + private static final ApiKey infoApi = new ApiKey( + ApiKeyId.generate(), + new PrettyName("chalmers.it api key"), + new Text( + "chalmers.it api nyckel", + "chalmers.it api key" + ), + ApiKeyType.INFO, + ApiKeyToken.generate() + ); + private static final ApiKey goldappsitApi = new ApiKey( + ApiKeyId.generate(), + new PrettyName("goldapps api key"), + new Text( + "goldapps api nyckel", + "goldapps api key" + ), + ApiKeyType.GOLDAPPS, + ApiKeyToken.generate() + ); + private static final ApiKey allowListApi = new ApiKey( + ApiKeyId.generate(), + new PrettyName("allow list api key"), + new Text( + "allow list api nyckel", + "allow list api key" + ), + ApiKeyType.ALLOW_LIST, + ApiKeyToken.generate() + ); + private static final ApiAuthentication INFO_API_DETAILS = new ApiAuthentication() { + @Override + public ApiKey get() { + return infoApi; + } + + @Override + public Optional getClient() { + return Optional.empty(); + } + }; + private static final ApiAuthentication GOLDAPPS_API_DETAILS = new ApiAuthentication() { + @Override + public ApiKey get() { + return goldappsitApi; + } + + @Override + public Optional getClient() { + return Optional.empty(); + } + }; + private static final ApiAuthentication ALLOW_LIST_API_DETAILS = new ApiAuthentication() { + @Override + public ApiKey get() { + return allowListApi; + } + + @Override + public Optional getClient() { + return Optional.empty(); + } + }; + private static final UserAuthentication adminAuthenticated = new UserAuthentication() { + @Override + public GammaUser get() { + return adminUser; + } + + @Override + public List getAuthorities() { + return Collections.singletonList(new UserAuthority(new AuthorityName("admin"), AuthorityType.AUTHORITY)); + } + + }; + private static final UserAuthentication normalUserAuthenticated = new UserAuthentication() { + @Override + public GammaUser get() { + return normalUser; + } + + @Override + public List getAuthorities() { + return Collections.emptyList(); + } + }; + @Mock + private UserRepository userRepository; + @Mock + private ClientAuthorityRepository clientAuthorityRepository; + @InjectMocks + private AccessGuard accessGuard; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + @Test + public void Given_Admin_Expect_isAdmin_To_NotThrow() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(adminAuthenticated)); + given(clientAuthorityRepository.getByUser(adminUser.id())) + .willReturn(userAuthoritiesMap.get(adminUser.id())); + + assertThatNoException() + .isThrownBy(() -> this.accessGuard.require(isAdmin())); + } + } + + @Test + public void Given_AdminThatIsLocked_Expect_isAdmin_To_Throw() { + GammaUser lockedAdminUser = adminUser.with() + .id(UserId.generate()) + .extended(adminUser.extended().withLocked(true)) + .build(); + + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(lockedAdminUser)); + given(clientAuthorityRepository.getByUser(lockedAdminUser.id())) + .willReturn(userAuthoritiesMap.get(lockedAdminUser.id())); + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isAdmin())); + } + } + + @Test + public void Given_NonAdmin_Expect_isAdmin_To_Throw() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(normalUserAuthenticated)); + given(clientAuthorityRepository.getByUser(normalUser.id())) + .willReturn(userAuthoritiesMap.get(normalUser.id())); + + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isAdmin())); + } + } + + @Test + public void Given_Unauthenticated_Expect_isAdmin_To_Throw() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(new SecurityContextImpl()); + + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isAdmin())); + } + } + + @Test + public void Given_Api_Expect_isAdmin_To_Throw() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(CLIENT_API_DETAILS)); + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isAdmin())); + } + } + + @Test + public void Given_AdminAndCorrectPassword_Expect_isAdmin_To_NotThrow() { + String password = "password"; + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(adminAuthenticated)); + given(clientAuthorityRepository.getByUser(adminUser.id())) + .willReturn(userAuthoritiesMap.get(adminUser.id())); + given(userRepository.checkPassword(adminUser.id(), new UnencryptedPassword(password))) + .willReturn(true); + + assertThatNoException() + .isThrownBy(() -> this.accessGuard.requireAll( + isAdmin(), + passwordCheck(password) + )); + } + + + } + + @Test + public void Given_AdminAndIncorrectPassword_Expect_isAdmin_To_Throw() { + String password = "wrongpassword"; + + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(adminAuthenticated)); + given(clientAuthorityRepository.getByUser(adminUser.id())) + .willReturn(userAuthoritiesMap.get(adminUser.id())); + given(userRepository.checkPassword(adminUser.id(), new UnencryptedPassword(password))) + .willReturn(false); + + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.requireAll( + isAdmin(), + passwordCheck(password) + )); + } + + + } + + @Test + public void Given_ClientApi_Expect_isClientApi_To_NotThrow() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(CLIENT_API_DETAILS)); + assertThatNoException() + .isThrownBy(() -> this.accessGuard.require(isClientApi())); + } + } + + @Test + public void Given_ClientApiWithNormalUserApproved_Expect_userHasAcceptedClient_To_NotThrow() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(CLIENT_API_DETAILS)); + assertThatNoException() + .isThrownBy(() -> this.accessGuard.requireAll( + isClientApi(), + userHasAcceptedClient(normalUser.id()) + )); + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.requireAll( + isClientApi(), + userHasAcceptedClient(adminUser.id()) + )); + } + + } + + @Test + public void Given_ClientApiWithNormalUserApproved_Expect_userHasAcceptedClient_With_AdminUser_To_Throw() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(CLIENT_API_DETAILS)); + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.requireAll( + isClientApi(), + userHasAcceptedClient(adminUser.id()) + )); + } + + } + + @Test + public void Given_UserIsSignedIn_Expect_isNotSignedIn_To_Throw() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(normalUserAuthenticated)); + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isNotSignedIn())); + } + + } + + @Test + public void Given_UserNotSignedIn_Expect_isNotSignedIn_To_NotThrow() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(new SecurityContextImpl()); + assertThatNoException() + .isThrownBy(() -> this.accessGuard.require(isNotSignedIn())); + } + + } + + @Test + public void Given_UserIsSignedIn_Expect_isSignedIn_To_NotThrow() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(normalUserAuthenticated)); + assertThatNoException() + .isThrownBy(() -> this.accessGuard.require(isSignedIn())); + } + + } + + @Test + public void Given_UserIsSignedInAndLocked_Expect_isSignedIn_To_NotThrow() { + GammaUser lockedUser = normalUser.with() + .id(UserId.generate()) + .extended(normalUser.extended().withLocked(true)) + .build(); + + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(lockedUser)); + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isSignedIn())); + } + + } + + @Test + public void Given_UserNotSignedIn_Expect_isSignedIn_To_Throw() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(new SecurityContextImpl()); + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isSignedIn())); + } + + } + + @Test + public void Given_UserIsInGroup_Expect_isSignedInUserMemberOfGroup_To_NotThrow() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(normalUserAuthenticated)); + assertThatNoException() + .isThrownBy(() -> this.accessGuard.require(isSignedInUserMemberOfGroup(digIT18))); + } + + } + + @Test + public void Given_UserIsNotInGroup_Expect_isSignedInUserMemberOfGroup_To_Throw() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(adminAuthenticated)); + } + + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isSignedInUserMemberOfGroup(digIT18))); + } + + @Test + public void Given_CorrectApiType_Expect_isApi_To_NotThrow() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(CLIENT_API_DETAILS)); + assertThatNoException() + .isThrownBy(() -> this.accessGuard.require(isApi(ApiKeyType.CLIENT))); + } + + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(INFO_API_DETAILS)); + assertThatNoException() + .isThrownBy(() -> this.accessGuard.require(isApi(ApiKeyType.INFO))); + } + + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(GOLDAPPS_API_DETAILS)); + assertThatNoException() + .isThrownBy(() -> this.accessGuard.require(isApi(ApiKeyType.GOLDAPPS))); + } + + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(ALLOW_LIST_API_DETAILS)); + assertThatNoException() + .isThrownBy(() -> this.accessGuard.require(isApi(ApiKeyType.ALLOW_LIST))); + } + } + + @Test + public void Given_IncorrectApiType_Expect_isApi_To_Throw() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(CLIENT_API_DETAILS)); + } + + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isApi(ApiKeyType.ALLOW_LIST))); + + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(INFO_API_DETAILS)); + } + + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isApi(ApiKeyType.CLIENT))); + + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(GOLDAPPS_API_DETAILS)); + } + + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isApi(ApiKeyType.INFO))); + + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(ALLOW_LIST_API_DETAILS)); + } + + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isApi(ApiKeyType.GOLDAPPS))); + } + + @Test + public void Given_IsAdmin_Expect_isApi_To_Throw() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(adminAuthenticated)); + } + + given(clientAuthorityRepository.getByUser(adminUser.id())) + .willReturn(userAuthoritiesMap.get(adminUser.id())); + + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isApi(ApiKeyType.CLIENT))); + } + + @Test + public void Given_NeitherChecksIsValid_Expect_requireEither_To_Throw() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(normalUserAuthenticated)); + } + + given(clientAuthorityRepository.getByUser(normalUser.id())) + .willReturn(userAuthoritiesMap.get(normalUser.id())); + + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.requireEither( + isAdmin(), + isSignedInUserMemberOfGroup(digIT19) + )); + } + + @Test + public void Given_OneOfTwoChecksIsValid_Expect_requireEither_To_NotThrow() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(normalUserAuthenticated)); + given(clientAuthorityRepository.getByUser(normalUser.id())) + .willReturn(userAuthoritiesMap.get(normalUser.id())); + assertThatNoException() + .isThrownBy(() -> this.accessGuard.requireEither( + isAdmin(), + isSignedInUserMemberOfGroup(digIT18) + )); + } + } + + @Test + public void Given_IsApi_Expect_passwordCheck_To_Throw() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(CLIENT_API_DETAILS)); + } + + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(passwordCheck("password"))); + } + + @Test + public void Given_IsAdmin_Expect_isClientApi_To_Throw() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(adminAuthenticated)); + } + + given(clientAuthorityRepository.getByUser(adminUser.id())) + .willReturn(userAuthoritiesMap.get(adminUser.id())); + + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(isClientApi())); + } + + @Test + public void Given_IsInfoApi_Expect_userHasAcceptedClient_To_Throw() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(INFO_API_DETAILS)); + + assertThatExceptionOfType(AccessGuard.AccessDeniedException.class) + .isThrownBy(() -> this.accessGuard.require(userHasAcceptedClient(adminUser.id()))); + } + + } + + @Test + public void Given_BootstrapAuthenticatedAdmin_Expect_isLocalRunner_To_NotThrow() { + try (MockedStatic mocked = mockStatic(SecurityContextHolder.class)) { + mocked.when(SecurityContextHolder::getContext) + .thenReturn(wrapDetails(new LocalRunnerAuthentication() { + })); + + assertThatNoException() + .isThrownBy(() -> this.accessGuard.require(isLocalRunner())); + } + + } + + private SecurityContext wrapDetails(Object details) { + return new SecurityContextImpl(new Authentication() { + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getDetails() { + return details; + } + + @Override + public Object getPrincipal() { + return null; + } + + @Override + public boolean isAuthenticated() { + return false; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + + } + + @Override + public String getName() { + return null; + } + }); + } + + */ +} \ No newline at end of file diff --git a/backend/src/test/java/it/chalmers/gamma/app/authority/ClientAuthorityFacadeIntegrationTest.java b/backend/src/test/java/it/chalmers/gamma/app/authority/ClientAuthorityFacadeIntegrationTest.java new file mode 100644 index 000000000..5c7261231 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/app/authority/ClientAuthorityFacadeIntegrationTest.java @@ -0,0 +1,69 @@ +package it.chalmers.gamma.app.authority; + +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.settings.SettingsRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserRepositoryAdapter; +import it.chalmers.gamma.app.authentication.UserAccessGuard; +import it.chalmers.gamma.app.client.domain.ClientAuthorityFacade; +import it.chalmers.gamma.security.user.PasswordConfiguration; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.junit.jupiter.Testcontainers; + +@ActiveProfiles("test") +@DataJpaTest +@Import({ClientAuthorityFacade.class, + ClientAuthorityRepositoryAdapter.class, + SuperGroupRepositoryAdapter.class, + SuperGroupEntityConverter.class, + ClientAuthorityEntityConverter.class, + SuperGroupEntityConverter.class, + UserEntityConverter.class, + UserAccessGuard.class, + PostEntityConverter.class, + UserRepositoryAdapter.class, + PasswordConfiguration.class, + PostRepositoryAdapter.class, + SuperGroupRepositoryAdapter.class, + SettingsRepositoryAdapter.class +}) +@Testcontainers +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public class ClientAuthorityFacadeIntegrationTest { +/* + @MockBean + private AccessGuard accessGuard; + @Autowired + private ClientAuthorityFacade clientAuthorityFacade; + @Autowired + private SettingsRepository settingsRepository; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @BeforeEach + public void setSettings() { + this.settingsRepository.setSettings(defaultSettings); + } + + @Test + public void Given_ValidAuthorityLevel_Expect_addUserToAuthorityLevel_With_InvalidUser_To_Throw() + throws ClientAuthorityRepository.AuthorityLevelAlreadyExistsException { + clientAuthorityFacade.create("hello"); + + assertThatExceptionOfType(ClientAuthorityFacade.UserNotFoundException.class) + .isThrownBy(() -> clientAuthorityFacade.addUserToAuthorityLevel("hello", UUID.randomUUID())); + } +*/ +} diff --git a/backend/src/test/java/it/chalmers/gamma/app/authority/ClientAuthorityFacadeUnitTest.java b/backend/src/test/java/it/chalmers/gamma/app/authority/ClientAuthorityFacadeUnitTest.java new file mode 100644 index 000000000..b49f03191 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/app/authority/ClientAuthorityFacadeUnitTest.java @@ -0,0 +1,614 @@ +package it.chalmers.gamma.app.authority; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +class ClientAuthorityFacadeUnitTest { +/* + @Mock + private AccessGuard accessGuard; + + @Mock + private ClientAuthorityRepository clientAuthorityRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private PostRepository postRepository; + + @Mock + private SuperGroupRepository superGroupRepository; + + @InjectMocks + private ClientAuthorityFacade clientAuthorityFacade; + + private static Authority.SuperGroupPost sgp(SuperGroup sg, Post p) { + return new Authority.SuperGroupPost(sg, p); + } + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + @Test + public void Given_AValidName_Expect_create_To_CreateValidAuthorityLevel() throws ClientAuthorityRepository.AuthorityLevelAlreadyExistsException { + UUID clientUid = UUID.randomUUID(); + String clientAuthority = "mat"; + + clientAuthorityFacade.create(clientUid, clientAuthority); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuthorityName.class); + verify(clientAuthorityRepository).create(captor.capture()); + AuthorityName authorityName = captor.getValue(); + + assertThat(authorityName) + .isEqualTo(new AuthorityName(adminAuthorityLevelName)); + + InOrder inOrder = inOrder(accessGuard, clientAuthorityRepository); + + //Makes sure that isAdmin is called first + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(clientAuthorityRepository).create(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_VariousInvalidNames_Expect_create_To_Throw() { + assertThatNullPointerException() + .isThrownBy(() -> clientAuthorityFacade.create(null)); + + //Too short + assertThatIllegalArgumentException() + .isThrownBy(() -> clientAuthorityFacade.create("a")); + + //Uppercase + assertThatIllegalArgumentException() + .isThrownBy(() -> clientAuthorityFacade.create("A")); + + //Too long + assertThatIllegalArgumentException() + .isThrownBy(() -> clientAuthorityFacade.create("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + } + + @Test + public void Given_AValidName_Expect_delete_To_DeleteAuthorityLevel() + throws ClientAuthorityFacade.AuthorityLevelNotFoundException, + ClientAuthorityRepository.AuthorityLevelNotFoundException { + String adminAuthorityLevelName = "admin"; + + clientAuthorityFacade.delete(adminAuthorityLevelName); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuthorityName.class); + verify(clientAuthorityRepository).delete(captor.capture()); + AuthorityName authorityName = captor.getValue(); + + assertThat(authorityName) + .isEqualTo(new AuthorityName(adminAuthorityLevelName)); + + InOrder inOrder = inOrder(accessGuard, clientAuthorityRepository); + + //Makes sure that isAdmin is called first + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(clientAuthorityRepository).delete(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_VariousInvalidNames_Expect_delete_To_Throw() throws ClientAuthorityRepository.AuthorityLevelNotFoundException { + assertThatNullPointerException() + .isThrownBy(() -> clientAuthorityFacade.delete(null)); + + //Too short + assertThatIllegalArgumentException() + .isThrownBy(() -> clientAuthorityFacade.delete("a")); + + //Uppercase + assertThatIllegalArgumentException() + .isThrownBy(() -> clientAuthorityFacade.delete("A")); + + //Too long + assertThatIllegalArgumentException() + .isThrownBy(() -> clientAuthorityFacade.delete("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + + //Illegal characters + assertThatIllegalArgumentException() + .isThrownBy(() -> clientAuthorityFacade.delete("ö$a")); + + //Legal name, but doesn't exist + + doThrow(ClientAuthorityRepository.AuthorityLevelNotFoundException.class) + .when(clientAuthorityRepository).delete(any()); + + assertThatExceptionOfType(ClientAuthorityFacade.AuthorityLevelNotFoundException.class) + .isThrownBy(() -> clientAuthorityFacade.delete("hello")); + } + + @Test + public void Given_AValidName_Expect_get_To_ReturnAuthorityLevel() { + Authority adminAuthority = new Authority( + new AuthorityName("admin"), + List.of( + sgp( + styrit, + chair + ) + ), + List.of( + digit + ), + List.of(u1, u2, u3) + ); + + given(clientAuthorityRepository.get(new AuthorityName("admin"))) + .willReturn(Optional.of(adminAuthority)); + + Optional authorityLevelDTO = clientAuthorityFacade.get("admin"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuthorityName.class); + verify(clientAuthorityRepository).get(captor.capture()); + AuthorityName authorityName = captor.getValue(); + + assertThat(authorityName) + .isEqualTo(new AuthorityName("admin")); + + AuthorityLevelDTOAssert.assertThat(authorityLevelDTO) + .isEqualTo(adminAuthority); + + InOrder inOrder = inOrder(accessGuard, clientAuthorityRepository); + + //Makes sure that isAdmin is called first + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(clientAuthorityRepository).get(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_VariousInvalidNames_Expect_get_To_Throw() { + assertThatNullPointerException() + .isThrownBy(() -> clientAuthorityFacade.get(null)); + + //Too short + assertThatIllegalArgumentException() + .isThrownBy(() -> clientAuthorityFacade.get("a")); + + //Uppercase + assertThatIllegalArgumentException() + .isThrownBy(() -> clientAuthorityFacade.get("A")); + + //Too long + assertThatIllegalArgumentException() + .isThrownBy(() -> clientAuthorityFacade.get("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + } + + @Test + public void MakeSureGetAllWorksProperly() { + Authority adminAuthority = new Authority( + new AuthorityName("admin"), + List.of(sgp(styrit, chair)), + List.of(digit), + List.of(u1, u2, u3) + ); + + Authority matAuthority = new Authority( + new AuthorityName("mat"), + List.of(sgp(digit, chair)), + Collections.emptyList(), + List.of(u5, u6) + ); + + Authority bookitAuthority = new Authority( + new AuthorityName("bookit"), + Collections.emptyList(), + List.of(prit), + List.of(u8, u9) + ); + + List authorities = List.of(adminAuthority, matAuthority, bookitAuthority); + + given(clientAuthorityRepository.getAll()) + .willReturn(authorities); + + List clientAuthorityDTOS = this.clientAuthorityFacade.getAll(); + + Assertions.assertThat(clientAuthorityDTOS) + .zipSatisfy(authorities, (actual, expected) -> + AuthorityLevelDTOAssert.assertThat(actual) + .isEqualTo(expected)); + + InOrder inOrder = inOrder(accessGuard, clientAuthorityRepository); + + //Makes sure that isAdmin is called first + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(clientAuthorityRepository).getAll(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_EmptyAuthorityLevel_Expect_addSuperGroupToAuthorityLevel_To_Work() throws ClientAuthorityFacade.AuthorityLevelNotFoundException, ClientAuthorityFacade.SuperGroupNotFoundException { + Authority adminAuthority = new Authority( + new AuthorityName("admin"), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + ); + + given(clientAuthorityRepository.get(new AuthorityName("admin"))) + .willReturn(Optional.of(adminAuthority)); + + given(superGroupRepository.get(digit.id())) + .willReturn(Optional.of(digit)); + + this.clientAuthorityFacade.addSuperGroupToAuthorityLevel("admin", digit.id().value()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Authority.class); + verify(clientAuthorityRepository).save(captor.capture()); + Authority newAuthority = captor.getValue(); + + Assertions.assertThat(newAuthority) + .isEqualTo(new Authority( + new AuthorityName("admin"), + Collections.emptyList(), + List.of(digit), + Collections.emptyList() + )); + + InOrder inOrder = inOrder(accessGuard, clientAuthorityRepository, superGroupRepository); + + //Makes sure that isAdmin is called first + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(clientAuthorityRepository).get(new AuthorityName("admin")); + inOrder.verify(superGroupRepository).get(digit.id()); + inOrder.verify(clientAuthorityRepository).save(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_FilledAuthorityLevel_Expect_addSuperGroupToAuthorityLevel_To_Work() throws ClientAuthorityFacade.AuthorityLevelNotFoundException, ClientAuthorityFacade.SuperGroupNotFoundException { + Authority adminAuthority = new Authority( + new AuthorityName("admin"), + List.of(sgp(styrit, chair), sgp(emeritus, chair)), + List.of(drawit, dragit), + List.of(u2, u3, u4) + ); + + given(clientAuthorityRepository.get(new AuthorityName("admin"))) + .willReturn(Optional.of(adminAuthority)); + + given(superGroupRepository.get(digit.id())) + .willReturn(Optional.of(digit)); + + this.clientAuthorityFacade.addSuperGroupToAuthorityLevel("admin", digit.id().value()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Authority.class); + verify(clientAuthorityRepository).save(captor.capture()); + Authority newAuthority = captor.getValue(); + + Assertions.assertThat(newAuthority) + .isEqualTo(new Authority( + new AuthorityName("admin"), + List.of(sgp(styrit, chair), sgp(emeritus, chair)), + List.of(drawit, dragit, digit), + List.of(u2, u3, u4) + )); + } + + @Test + public void Given_EmptyAuthorityLevel_Expect_addSuperGroupPostToAuthorityLevel_To_Work() throws ClientAuthorityFacade.AuthorityLevelNotFoundException, ClientAuthorityFacade.PostNotFoundException, ClientAuthorityFacade.SuperGroupNotFoundException { + Authority adminAuthority = new Authority( + new AuthorityName("admin"), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + ); + + given(clientAuthorityRepository.get(new AuthorityName("admin"))) + .willReturn(Optional.of(adminAuthority)); + + given(superGroupRepository.get(digit.id())) + .willReturn(Optional.of(digit)); + + given(postRepository.get(chair.id())) + .willReturn(Optional.of(chair)); + + this.clientAuthorityFacade.addSuperGroupPostToAuthorityLevel( + "admin", + digit.id().value(), + chair.id().value() + ); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Authority.class); + verify(clientAuthorityRepository).save(captor.capture()); + Authority newAuthority = captor.getValue(); + + Assertions.assertThat(newAuthority) + .isEqualTo(new Authority( + new AuthorityName("admin"), + List.of(sgp(digit, chair)), + Collections.emptyList(), + Collections.emptyList() + )); + + InOrder inOrder = inOrder( + accessGuard, + clientAuthorityRepository, + superGroupRepository, + postRepository + ); + + //Makes sure that isAdmin is called first + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(clientAuthorityRepository).get(new AuthorityName("admin")); + inOrder.verify(superGroupRepository).get(digit.id()); + inOrder.verify(postRepository).get(chair.id()); + inOrder.verify(clientAuthorityRepository).save(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_FilledAuthorityLevel_Expect_addSuperGroupPostToAuthorityLevel_To_Work() throws ClientAuthorityFacade.AuthorityLevelNotFoundException, ClientAuthorityFacade.PostNotFoundException, ClientAuthorityFacade.SuperGroupNotFoundException { + Authority adminAuthority = new Authority( + new AuthorityName("admin"), + List.of(sgp(styrit, chair), sgp(emeritus, chair)), + List.of(drawit, dragit), + List.of(u2, u3, u4) + ); + + given(clientAuthorityRepository.get(new AuthorityName("admin"))) + .willReturn(Optional.of(adminAuthority)); + + given(superGroupRepository.get(digit.id())) + .willReturn(Optional.of(digit)); + + given(postRepository.get(chair.id())) + .willReturn(Optional.of(chair)); + + this.clientAuthorityFacade.addSuperGroupPostToAuthorityLevel( + "admin", + digit.id().value(), + chair.id().value() + ); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Authority.class); + verify(clientAuthorityRepository).save(captor.capture()); + Authority newAuthority = captor.getValue(); + + Assertions.assertThat(newAuthority) + .isEqualTo(new Authority( + new AuthorityName("admin"), + List.of(sgp(styrit, chair), sgp(emeritus, chair), sgp(digit, chair)), + List.of(drawit, dragit), + List.of(u2, u3, u4) + )); + } + + @Test + public void Given_EmptyAuthorityLevel_Expect_addUserToAuthorityLevel_To_Work() throws ClientAuthorityFacade.UserNotFoundException, ClientAuthorityFacade.AuthorityLevelNotFoundException { + Authority adminAuthority = new Authority( + new AuthorityName("admin"), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + ); + + given(clientAuthorityRepository.get(new AuthorityName("admin"))) + .willReturn(Optional.of(adminAuthority)); + + given(userRepository.get(u1.id())) + .willReturn(Optional.of(u1)); + + this.clientAuthorityFacade.addUserToAuthorityLevel("admin", u1.id().value()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Authority.class); + verify(clientAuthorityRepository).save(captor.capture()); + Authority newAuthority = captor.getValue(); + + Assertions.assertThat(newAuthority) + .isEqualTo(new Authority( + new AuthorityName("admin"), + Collections.emptyList(), + Collections.emptyList(), + List.of(u1) + )); + + InOrder inOrder = inOrder( + accessGuard, + clientAuthorityRepository, + userRepository + ); + + //Makes sure that isAdmin is called first + inOrder.verify(accessGuard).requireEither(isAdmin(), isLocalRunner()); + inOrder.verify(clientAuthorityRepository).get(new AuthorityName("admin")); + inOrder.verify(userRepository).get(u1.id()); + inOrder.verify(clientAuthorityRepository).save(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_FilledAuthorityLevel_Expect_addUserToAuthorityLevel_To_Work() throws ClientAuthorityFacade.UserNotFoundException, ClientAuthorityFacade.AuthorityLevelNotFoundException { + Authority adminAuthority = new Authority( + new AuthorityName("admin"), + List.of(sgp(styrit, chair), sgp(emeritus, chair)), + List.of(drawit, dragit), + List.of(u2, u3, u4) + ); + + given(clientAuthorityRepository.get(new AuthorityName("admin"))) + .willReturn(Optional.of(adminAuthority)); + + given(userRepository.get(u1.id())) + .willReturn(Optional.of(u1)); + + this.clientAuthorityFacade.addUserToAuthorityLevel("admin", u1.id().value()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Authority.class); + verify(clientAuthorityRepository).save(captor.capture()); + Authority newAuthority = captor.getValue(); + + Assertions.assertThat(newAuthority) + .isEqualTo(new Authority( + new AuthorityName("admin"), + List.of(sgp(styrit, chair), sgp(emeritus, chair)), + List.of(drawit, dragit), + List.of(u2, u3, u4, u1) + )); + } + + @Test + public void Given_AuthorityLevelWithSuperGroup_Expect_removeSuperGroupFromAuthorityLevel_To_Work() throws ClientAuthorityFacade.AuthorityLevelNotFoundException { + Authority adminAuthority = new Authority( + new AuthorityName("admin"), + Collections.emptyList(), + List.of(digit), + Collections.emptyList() + ); + + given(clientAuthorityRepository.get(new AuthorityName("admin"))) + .willReturn(Optional.of(adminAuthority)); + + this.clientAuthorityFacade.removeSuperGroupFromAuthorityLevel("admin", digit.id().value()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Authority.class); + verify(clientAuthorityRepository).save(captor.capture()); + Authority newAuthority = captor.getValue(); + + Assertions.assertThat(newAuthority) + .isEqualTo(new Authority( + new AuthorityName("admin"), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + )); + + InOrder inOrder = inOrder(accessGuard, clientAuthorityRepository, superGroupRepository); + + //Makes sure that isAdmin is called first + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(clientAuthorityRepository).get(new AuthorityName("admin")); + inOrder.verify(clientAuthorityRepository).save(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_AuthorityLevelWithSuperGroupPost_Expect_removeSuperGroupPostFromAuthorityLevel_To_Work() throws ClientAuthorityFacade.AuthorityLevelNotFoundException { + Authority adminAuthority = new Authority( + new AuthorityName("admin"), + List.of(sgp(digit, chair)), + Collections.emptyList(), + Collections.emptyList() + ); + + given(clientAuthorityRepository.get(new AuthorityName("admin"))) + .willReturn(Optional.of(adminAuthority)); + + this.clientAuthorityFacade.removeSuperGroupPostFromAuthorityLevel("admin", digit.id().value(), chair.id().value()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Authority.class); + verify(clientAuthorityRepository).save(captor.capture()); + Authority newAuthority = captor.getValue(); + + Assertions.assertThat(newAuthority) + .isEqualTo(new Authority( + new AuthorityName("admin"), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + )); + + InOrder inOrder = inOrder(accessGuard, clientAuthorityRepository, superGroupRepository); + + //Makes sure that isAdmin is called first + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(clientAuthorityRepository).get(new AuthorityName("admin")); + inOrder.verify(clientAuthorityRepository).save(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_AuthorityLevelWithUser_Expect_removeUserFromAuthorityLevel_To_Work() throws ClientAuthorityFacade.AuthorityLevelNotFoundException { + Authority adminAuthority = new Authority( + new AuthorityName("admin"), + Collections.emptyList(), + Collections.emptyList(), + List.of(u1) + ); + + given(clientAuthorityRepository.get(new AuthorityName("admin"))) + .willReturn(Optional.of(adminAuthority)); + + this.clientAuthorityFacade.removeUserFromAuthorityLevel("admin", u1.id().value()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Authority.class); + verify(clientAuthorityRepository).save(captor.capture()); + Authority newAuthority = captor.getValue(); + + Assertions.assertThat(newAuthority) + .isEqualTo(new Authority( + new AuthorityName("admin"), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + )); + + InOrder inOrder = inOrder(accessGuard, clientAuthorityRepository, superGroupRepository); + + //Makes sure that isAdmin is called first + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(clientAuthorityRepository).get(new AuthorityName("admin")); + inOrder.verify(clientAuthorityRepository).save(any()); + inOrder.verifyNoMoreInteractions(); + } + + public static class AuthorityLevelDTOAssert extends AbstractAssert { + + protected AuthorityLevelDTOAssert(ClientAuthorityFacade.ClientAuthorityDTO actual) { + super(actual, AuthorityLevelDTOAssert.class); + } + + public static AuthorityLevelDTOAssert assertThat(ClientAuthorityFacade.ClientAuthorityDTO clientAuthorityDTO) { + return new AuthorityLevelDTOAssert(clientAuthorityDTO); + } + + public static AuthorityLevelDTOAssert assertThat(Optional actual) { + if (actual.isEmpty()) { + fail("Optional is empty"); + return null; + } + return new AuthorityLevelDTOAssert(actual.get()); + } + + + public AuthorityLevelDTOAssert isEqualTo(Authority authority) { + isNotNull(); + + Assertions.assertThat(actual) + .hasOnlyFields("clientAuthority", "superGroups", "users", "posts"); + + Assertions.assertThat(actual.clientAuthority()) + .isEqualTo(authority.name().value()); + + Assertions.assertThat(actual.superGroups()).zipSatisfy(authority.superGroups(), (actual, expected) -> + Assertions.assertThat(actual) + .isEqualTo(new SuperGroupFacade.SuperGroupDTO(expected)) + ); + + Assertions.assertThat(actual.users()).zipSatisfy(authority.users(), (actual, expected) -> + Assertions.assertThat(actual) + .isEqualTo(new UserFacade.UserDTO(expected)) + ); + + Assertions.assertThat(actual.posts()).zipSatisfy(authority.posts(), (actual, expected) -> { + Assertions.assertThat(actual.superGroup()) + .isEqualTo(new SuperGroupFacade.SuperGroupDTO(expected.superGroup())); + Assertions.assertThat(actual.post()) + .isEqualTo(new PostFacade.PostDTO(expected.post())); + }); + + return this; + } + } +*/ +} \ No newline at end of file diff --git a/backend/src/test/java/it/chalmers/gamma/app/client/ClientFacadeIntegrationTest.java b/backend/src/test/java/it/chalmers/gamma/app/client/ClientFacadeIntegrationTest.java new file mode 100644 index 000000000..865a919b9 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/app/client/ClientFacadeIntegrationTest.java @@ -0,0 +1,109 @@ +package it.chalmers.gamma.app.client; + +import it.chalmers.gamma.adapter.secondary.jpa.apikey.ApiKeyEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.apikey.ApiKeyRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.client.ClientRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.settings.SettingsRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserRepositoryAdapter; +import it.chalmers.gamma.app.authentication.UserAccessGuard; +import it.chalmers.gamma.security.user.PasswordConfiguration; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("test") +@DataJpaTest +@Testcontainers +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import({ClientFacade.class, + ClientRepositoryAdapter.class, + ClientEntityConverter.class, + ApiKeyRepositoryAdapter.class, + ApiKeyEntityConverter.class, + UserRepositoryAdapter.class, + PasswordConfiguration.class, + UserEntityConverter.class, + UserAccessGuard.class, + ClientAuthorityRepositoryAdapter.class, + SuperGroupRepositoryAdapter.class, + SuperGroupEntityConverter.class, + ClientAuthorityEntityConverter.class, + SuperGroupEntityConverter.class, + UserEntityConverter.class, + UserAccessGuard.class, + PostEntityConverter.class, + UserRepositoryAdapter.class, + PasswordConfiguration.class, + PostRepositoryAdapter.class, + SuperGroupRepositoryAdapter.class, + SettingsRepositoryAdapter.class}) +public class ClientFacadeIntegrationTest { +/* + @MockBean + private AccessGuard accessGuard; + @Autowired + private ClientFacade clientFacade; + @Autowired + private ClientAuthorityRepository clientAuthorityRepository; + @Autowired + private SettingsRepository settingsRepository; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @BeforeEach + public void setSettings() { + this.settingsRepository.setSettings(defaultSettings); + } + + @Test + public void Given_ANewValidClient_Expect_create_To_Work() throws ClientAuthorityRepository.AuthorityLevelAlreadyExistsException { + clientAuthorityRepository.create(new AuthorityName("mat")); + + ClientFacade.NewClient newClient = new ClientFacade.NewClient( + "https://mat.chalmers.it", + "Mat", + "Klient för Mat", + "Client for mat", + true, +// List.of("mat"), + true + ); + + ClientFacade.ClientAndApiKeySecrets secrets = clientFacade.create(newClient); + + assertThat(secrets) + .hasNoNullFieldsOrProperties(); + + ClientFacade.ClientDTO clientDTO = clientFacade.get(secrets.clientId()).orElseThrow(); + assertThat(clientDTO) + .isEqualTo(new ClientFacade.ClientDTO( + secrets.clientUid(), + secrets.clientId(), + newClient.redirectUrl(), + newClient.prettyName(), + newClient.svDescription(), + newClient.enDescription(), +// List.of("mat"), + true + )); + } + + */ + +} diff --git a/backend/src/test/java/it/chalmers/gamma/app/client/ClientFacadeUnitTest.java b/backend/src/test/java/it/chalmers/gamma/app/client/ClientFacadeUnitTest.java new file mode 100644 index 000000000..85f043fb3 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/app/client/ClientFacadeUnitTest.java @@ -0,0 +1,285 @@ +package it.chalmers.gamma.app.client; + +import it.chalmers.gamma.app.apikey.domain.ApiKey; +import it.chalmers.gamma.app.apikey.domain.ApiKeyToken; +import it.chalmers.gamma.app.apikey.domain.ApiKeyType; +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.client.domain.*; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.*; + +@ExtendWith(SpringExtension.class) +class ClientFacadeUnitTest { + + @Mock + private AccessGuard accessGuard; + + @Mock + private ClientRepository clientRepository; + + @InjectMocks + private ClientFacade clientFacade; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @Test + public void Given_AValidClientWithoutApiKey_Expect_create_To_ReturnValidSecrets() { + ClientFacade.NewClient newClient = new ClientFacade.NewClient( + "https://mat.chalmers.it", + "Mat", + "Klient för Mat", + "Client for mat", + false, + false, + null + ); + + ClientFacade.ClientAndApiKeySecrets secrets = this.clientFacade.create(newClient); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Client.class); + verify(clientRepository).save(captor.capture()); + Client capturedNewClient = captor.getValue(); + + //If not null, then they are valid. + Assertions.assertThat(capturedNewClient.clientUid()) + .isNotNull(); + Assertions.assertThat(capturedNewClient.clientId()) + .isNotNull(); + + Assertions.assertThat(secrets.apiKeyToken()) + .isNull(); + + Client expectedClient = new Client( + capturedNewClient.clientUid(), + capturedNewClient.clientId(), + new ClientSecret(secrets.clientSecret()), + new ClientRedirectUrl(newClient.redirectUrl()), + new PrettyName(newClient.prettyName()), + new Text( + newClient.svDescription(), + newClient.enDescription() + ), + List.of(Scope.PROFILE), + null, + new ClientOwnerOfficial(), + null + ); + + Assertions.assertThat(capturedNewClient) + .isEqualTo(expectedClient); + + InOrder inOrder = inOrder(accessGuard, clientRepository); + + //Makes sure that isAdmin is called first. + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(clientRepository).save(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_AValidClientWithoutApiKeyAndEmailScope_Expect_create_To_ReturnValidSecrets() { + ClientFacade.NewClient newClient = new ClientFacade.NewClient( + "https://mat.chalmers.it", + "Mat", + "Klient för Mat", + "Client for mat", + false, + true, + null + ); + + ClientFacade.ClientAndApiKeySecrets secrets = this.clientFacade.create(newClient); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Client.class); + verify(clientRepository).save(captor.capture()); + Client capturedNewClient = captor.getValue(); + + //If not null, then they are valid. + Assertions.assertThat(capturedNewClient.clientUid()) + .isNotNull(); + Assertions.assertThat(capturedNewClient.clientId()) + .isNotNull(); + + Assertions.assertThat(secrets.apiKeyToken()) + .isNull(); + + Client expectedClient = new Client( + capturedNewClient.clientUid(), + capturedNewClient.clientId(), + new ClientSecret(secrets.clientSecret()), + new ClientRedirectUrl(newClient.redirectUrl()), + new PrettyName(newClient.prettyName()), + new Text( + newClient.svDescription(), + newClient.enDescription() + ), + List.of(Scope.PROFILE, Scope.EMAIL), + null, + new ClientOwnerOfficial(), + null + ); + + Assertions.assertThat(capturedNewClient) + .isEqualTo(expectedClient); + } + + @Test + public void Given_AValidClientWithApiKey_Expect_create_To_ReturnValidSecrets() { + ClientFacade.NewClient newClient = new ClientFacade.NewClient( + "https://mat.chalmers.it", + "Mat", + "Klient för Mat", + "Client for mat", + true, + true, + null + ); + + ClientFacade.ClientAndApiKeySecrets secrets = this.clientFacade.create(newClient); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Client.class); + verify(clientRepository).save(captor.capture()); + Client capturedNewClient = captor.getValue(); + + //If not null, then they are valid. + Assertions.assertThat(capturedNewClient.clientUid()) + .isNotNull(); + Assertions.assertThat(capturedNewClient.clientId()) + .isNotNull(); + Assertions.assertThat(capturedNewClient.clientApiKey()) + .isNotEmpty(); + + Assertions.assertThat(secrets.apiKeyToken()) + .isNotNull(); + + ApiKey actualApiKey = capturedNewClient.clientApiKey().get(); + + ApiKey expectedApiKey = new ApiKey( + actualApiKey.id(), + new PrettyName(newClient.prettyName()), + new Text( + "Api nyckel för klienten: " + newClient.prettyName(), + "Api key for client: " + newClient.prettyName() + ), + ApiKeyType.CLIENT, + new ApiKeyToken(secrets.apiKeyToken()) + ); + + Client expectedClient = new Client( + capturedNewClient.clientUid(), + capturedNewClient.clientId(), + new ClientSecret(secrets.clientSecret()), + new ClientRedirectUrl(newClient.redirectUrl()), + new PrettyName(newClient.prettyName()), + new Text( + newClient.svDescription(), + newClient.enDescription() + ), + List.of(Scope.PROFILE, Scope.EMAIL), + expectedApiKey, + new ClientOwnerOfficial(), + null + ); + + Assertions.assertThat(capturedNewClient) + .isEqualTo(expectedClient); + } + + @Test + public void Given_AValidClientWithoutApiKeyAndRestrictions_Expect_create_To_ReturnValidSecrets() { + ClientFacade.NewClient newClient = new ClientFacade.NewClient( + "https://mat.chalmers.it", + "Mat", + "Klient för Mat", + "Client for mat", + false, + false, + null + ); + + ClientFacade.ClientAndApiKeySecrets secrets = this.clientFacade.create(newClient); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Client.class); + verify(clientRepository).save(captor.capture()); + Client capturedNewClient = captor.getValue(); + + //If not null, then they are valid. + Assertions.assertThat(capturedNewClient.clientUid()) + .isNotNull(); + Assertions.assertThat(capturedNewClient.clientId()) + .isNotNull(); + + Assertions.assertThat(secrets.apiKeyToken()) + .isNull(); + + Client expectedClient = new Client( + capturedNewClient.clientUid(), + capturedNewClient.clientId(), + new ClientSecret(secrets.clientSecret()), + new ClientRedirectUrl(newClient.redirectUrl()), + new PrettyName(newClient.prettyName()), + new Text( + newClient.svDescription(), + newClient.enDescription() + ), + List.of(Scope.PROFILE), + null, + new ClientOwnerOfficial(), + null + ); + + Assertions.assertThat(capturedNewClient) + .isEqualTo(expectedClient); + } + + @Test + public void Given_AValidClientUID_Expect_delete_To_NotThrow() throws ClientFacade.ClientNotFoundException, ClientRepository.ClientNotFoundException { + ClientUid clientUid = ClientUid.generate(); + + this.clientFacade.delete(clientUid.value().toString()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ClientUid.class); + verify(clientRepository).delete(captor.capture()); + ClientUid capturedClientUid = captor.getValue(); + + Assertions.assertThat(capturedClientUid) + .isEqualTo(clientUid); + + InOrder inOrder = inOrder(accessGuard, clientRepository); + + //Makes sure that isAdmin is called first. + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(clientRepository).delete(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_InvalidClient_Expect_delete_To_Throw() throws ClientRepository.ClientNotFoundException { + doThrow(ClientRepository.ClientNotFoundException.class) + .when(clientRepository).delete(any()); + + assertThatExceptionOfType(ClientFacade.ClientNotFoundException.class) + .isThrownBy(() -> this.clientFacade.delete(ClientUid.generate().getValue())); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/it/chalmers/gamma/app/goldapps/GoldappsFacadeUnitTest.java b/backend/src/test/java/it/chalmers/gamma/app/goldapps/GoldappsFacadeUnitTest.java new file mode 100644 index 000000000..a9ff45592 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/app/goldapps/GoldappsFacadeUnitTest.java @@ -0,0 +1,159 @@ +package it.chalmers.gamma.app.goldapps; + +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.group.domain.Group; +import it.chalmers.gamma.app.group.domain.GroupRepository; +import it.chalmers.gamma.app.user.domain.GammaUser; +import it.chalmers.gamma.utils.DomainUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.ArrayList; +import java.util.List; + +import static it.chalmers.gamma.app.authentication.AccessGuard.isApi; +import static it.chalmers.gamma.utils.DomainUtils.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.*; + +@ExtendWith(SpringExtension.class) +class GoldappsFacadeUnitTest { + + @Mock + private AccessGuard accessGuard; + + @Mock + private GroupRepository groupRepository; + + @InjectMocks + private GoldappsFacade goldappsFacade; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @Test + public void getActiveSuperGroupsTest() { + ActiveSuperGroupsTestCase testCase = getActiveSuperGroupTestCase(); + given(groupRepository.getAll()) + .willReturn(testCase.groups); + + List activeSuperGroups = goldappsFacade.getActiveSuperGroups( + List.of("committee", "board") + ); + + assertThat(false).isTrue(); + } + + @Test + public void getActiveUsersTest() { + ActiveUsersTestCase testCase = getActiveUsersTestCase(); + List expectedActiveUsers = testCase.expectedActive + .stream() + .map(GoldappsFacade.GoldappsUserDTO::new) + .toList(); + + given(groupRepository.getAll()) + .willReturn(testCase.groups.stream().map(DomainUtils::asSaved).toList()); + + List activeUsers = goldappsFacade.getActiveUsers( + List.of("committee", "board") + ); + + assertThat(activeUsers) + .hasSameElementsAs(expectedActiveUsers); + + InOrder inOrder = inOrder(accessGuard, groupRepository); + + //Makes sure that isApi is called first. + //TODO: check that goldapps api is called + inOrder.verify(accessGuard).require(isApi(any())); + inOrder.verify(groupRepository).getAll(); + inOrder.verifyNoMoreInteractions(); + } + + private ActiveSuperGroupsTestCase getActiveSuperGroupTestCase() { + return new ActiveSuperGroupsTestCase( + List.of( + digit18, + digit19, + prit18, + prit19, + drawit18, + drawit19, + styrit18, + styrit19 + ), + new ArrayList<>() + ); + } + + private ActiveUsersTestCase getActiveUsersTestCase() { + var digit = sg("digit", committee); + var didit = sg("didit", alumni); + var prit = sg("prit", committee); + var sprit = sg("sprit", alumni); + var drawit = sg("drawit", society); + var dragit = sg("dragit", alumni); + var styrit = sg("board", board); + var emeritus = sg("emeritus", alumni); + + var u1 = u("abca"); + var u2 = u("abcb", true, true); + var u3 = u("abcc", false, false); + var u4 = u("abcd"); + var u5 = u("abce", false, false); + var u6 = u("abcf"); + var u7 = u("abcg", true, true); + var u8 = u("abch"); + var u9 = u("abci"); + var u10 = u("abcj"); + var u11 = u("abck"); + + var digit18 = g("digit18", didit, List.of(gm(u1, chair), gm(u2, treasurer))); + var digit19 = g("digit19", digit, List.of(gm(u3, chair), gm(u4, member), gm(u2, member))); + var prit18 = g("prit18", sprit, List.of(gm(u1, chair), gm(u1, treasurer), gm(u2, member))); + var prit19 = g("prit19", prit, List.of(gm(u5, chair), gm(u6, treasurer), gm(u6, member))); + var drawit18 = g("drawit18", dragit, List.of(gm(u6, chair))); + var drawit19 = g("drawit19", drawit, List.of(gm(u1, chair), gm(u11, member))); + var styrit18 = g("styrit18", emeritus, List.of(gm(u7, chair), gm(u8, member), gm(u9, member))); + var styrit19 = g("styrit19", styrit, List.of(gm(u10, chair), gm(u11, treasurer))); + + return new ActiveUsersTestCase( + List.of( + digit18, + digit19, + prit18, + prit19, + drawit18, + drawit19, + styrit18, + styrit19 + ), + List.of( + u4, + u6, + u10, + u11 + ) + ); + } + + private record ActiveSuperGroupsTestCase(List groups, + List expectedSuperGroups) { + } + + private record ActiveUsersTestCase(List groups, + List expectedActive) { + } + + +} \ No newline at end of file diff --git a/backend/src/test/java/it/chalmers/gamma/app/group/GroupFacadeIntegrationTest.java b/backend/src/test/java/it/chalmers/gamma/app/group/GroupFacadeIntegrationTest.java new file mode 100644 index 000000000..b3016b7ae --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/app/group/GroupFacadeIntegrationTest.java @@ -0,0 +1,225 @@ +package it.chalmers.gamma.app.group; + +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.client.authority.ClientAuthorityRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.group.GroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.group.GroupRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.group.PostRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.settings.SettingsRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.supergroup.SuperGroupTypeRepositoryAdapter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntityConverter; +import it.chalmers.gamma.adapter.secondary.jpa.user.UserRepositoryAdapter; +import it.chalmers.gamma.app.admin.domain.AdminRepository; +import it.chalmers.gamma.app.authentication.UserAccessGuard; +import it.chalmers.gamma.app.client.domain.authority.ClientAuthorityRepository; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.group.domain.Group; +import it.chalmers.gamma.app.group.domain.GroupMember; +import it.chalmers.gamma.app.group.domain.GroupRepository; +import it.chalmers.gamma.app.group.domain.UnofficialPostName; +import it.chalmers.gamma.app.post.domain.PostRepository; +import it.chalmers.gamma.app.settings.domain.SettingsRepository; +import it.chalmers.gamma.app.supergroup.SuperGroupFacade; +import it.chalmers.gamma.app.supergroup.domain.SuperGroup; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupRepository; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupTypeRepository; +import it.chalmers.gamma.app.user.domain.Name; +import it.chalmers.gamma.app.user.domain.UserRepository; +import it.chalmers.gamma.security.user.PasswordConfiguration; +import it.chalmers.gamma.utils.DomainUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.List; + +import static it.chalmers.gamma.utils.DomainUtils.*; +import static it.chalmers.gamma.utils.GammaSecurityContextHolderTestUtils.setAuthenticatedAsAdminUser; +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("test") +@DataJpaTest +@Testcontainers +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import({GroupFacade.class, + GroupRepositoryAdapter.class, + GroupEntityConverter.class, + UserRepositoryAdapter.class, + PasswordConfiguration.class, + UserAccessGuard.class, + UserEntityConverter.class, + UserAccessGuard.class, + PostRepositoryAdapter.class, + PostEntityConverter.class, + SuperGroupRepositoryAdapter.class, + SuperGroupEntityConverter.class, + SuperGroupTypeRepositoryAdapter.class, + SuperGroupEntityConverter.class, + SettingsRepositoryAdapter.class, + ClientAuthorityRepositoryAdapter.class, + ClientAuthorityEntityConverter.class +}) +public class GroupFacadeIntegrationTest { + + @Autowired + private GroupFacade groupFacade; + @Autowired + private GroupRepository groupRepository; + @Autowired + private SuperGroupRepository superGroupRepository; + @Autowired + private SuperGroupTypeRepository superGroupTypeRepository; + @Autowired + private UserRepository userRepository; + @Autowired + private PostRepository postRepository; + @Autowired + private SettingsRepository settingsRepository; + @Autowired + private ClientAuthorityRepository clientAuthorityRepository; + @Autowired + private AdminRepository adminRepository; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @BeforeEach + public void setSettings() { + this.settingsRepository.setSettings(defaultSettings); + } + + @Test + public void Given_Group_Expect_create_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupFacade.GroupAlreadyExistsException, GroupFacade.SuperGroupNotFoundRuntimeException { + superGroupTypeRepository.add(committee); + superGroupRepository.save(digit); + + GroupFacade.NewGroup newGroup = new GroupFacade.NewGroup( + "mygroup", + "My Group", + digit.id().value() + ); + + groupFacade.create(newGroup); + + List groups = groupFacade.getAll(); + + assertThat(groups) + .hasSize(1); + + GroupFacade.GroupDTO groupDTO = groups.get(0); + SuperGroup superGroup = this.superGroupRepository.get(new SuperGroupId(newGroup.superGroup())).orElseThrow(); + + assertThat(groupDTO) + .hasNoNullFieldsOrProperties() + .isEqualTo(new GroupFacade.GroupDTO( + groupDTO.id(), + newGroup.name(), + newGroup.prettyName(), + new SuperGroupFacade.SuperGroupDTO(superGroup) + )); + } + + @Test + public void Given_Group_Expect_update_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException, GroupFacade.GroupAlreadyExistsException { + setAuthenticatedAsAdminUser(userRepository, adminRepository); + + Group group = digit18; + SuperGroup newSuperGroup = digit; + + superGroupTypeRepository.add(newSuperGroup.type()); + superGroupRepository.save(newSuperGroup); + + addGroup(group); + + GroupFacade.UpdateGroup updateGroup = new GroupFacade.UpdateGroup( + group.id().value(), + group.version() + 1, + "newdigit18", + "new digIT'18", + newSuperGroup.id().value() + ); + + this.groupFacade.update(updateGroup); + + GroupFacade.GroupWithMembersDTO groupDTO = this.groupFacade.getWithMembers(group.id().value()).orElseThrow(); + assertThat(groupDTO) + .isEqualTo(new GroupFacade.GroupWithMembersDTO( + group.with() + .name(new Name("newdigit18")) + .prettyName(new PrettyName("new digIT'18")) + .superGroup(newSuperGroup.withVersion(1)) + .version(2) + .groupMembers(group.groupMembers() + .stream() + //If the user is locked, remove +// .filter(groupMember -> !(groupMember.user().extended().locked() || !groupMember.user().extended().acceptedUserAgreement()) ) + .map(groupMember -> new GroupMember( + groupMember.post().withVersion(1), + groupMember.unofficialPostName(), + asSaved(groupMember.user())) + ).toList() + ) + .build() + )); + + } + + @Test + public void Given_GroupWithNewMembers_Expect_setMembers_To_Work() throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + setAuthenticatedAsAdminUser(userRepository, adminRepository); + postRepository.save(member); + addAll(userRepository, u3, u4); + addGroup(digit18); + + List newMembers = List.of( + new GroupFacade.ShallowMember(u1.id().value(), chair.id().value(), null), + new GroupFacade.ShallowMember(u3.id().value(), member.id().value(), "ServerChef"), + new GroupFacade.ShallowMember(u4.id().value(), treasurer.id().value(), "DubbelAnsvarig"), + new GroupFacade.ShallowMember(u4.id().value(), member.id().value(), null) + ); + + this.groupFacade.setMembers(digit18.id().value(), newMembers); + + Group expectedGroup = digit18.with() + .version(1) + .groupMembers(List.of( + gm(u1, chair), + gm(u3, member, new UnofficialPostName("ServerChef")), + gm(u4, treasurer, new UnofficialPostName("DubbelAnsvarig")), + gm(u4, member) + )).build(); + + //setMembers increases by one. + expectedGroup = asSaved(expectedGroup); + GroupFacade.GroupWithMembersDTO expectedGroupDTO = new GroupFacade.GroupWithMembersDTO(expectedGroup); + + assertThat(this.groupFacade.getWithMembers(digit18.id().value())) + .get() + .isEqualTo(expectedGroupDTO); + } + + private void addGroup(Group... groups) throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + DomainUtils.addGroup( + superGroupTypeRepository, + superGroupRepository, + userRepository, + postRepository, + groupRepository, + groups + ); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/app/group/GroupFacadeUnitTest.java b/backend/src/test/java/it/chalmers/gamma/app/group/GroupFacadeUnitTest.java new file mode 100644 index 000000000..bee58c916 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/app/group/GroupFacadeUnitTest.java @@ -0,0 +1,480 @@ +package it.chalmers.gamma.app.group; + +import it.chalmers.gamma.app.authentication.AccessGuard; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; +import it.chalmers.gamma.app.group.domain.Group; +import it.chalmers.gamma.app.group.domain.GroupId; +import it.chalmers.gamma.app.group.domain.GroupRepository; +import it.chalmers.gamma.app.group.domain.UnofficialPostName; +import it.chalmers.gamma.app.post.domain.PostRepository; +import it.chalmers.gamma.app.settings.domain.Settings; +import it.chalmers.gamma.app.settings.domain.SettingsRepository; +import it.chalmers.gamma.app.supergroup.SuperGroupFacade; +import it.chalmers.gamma.app.supergroup.domain.SuperGroup; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupId; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupRepository; +import it.chalmers.gamma.app.supergroup.domain.SuperGroupType; +import it.chalmers.gamma.app.user.domain.Name; +import it.chalmers.gamma.app.user.domain.UserId; +import it.chalmers.gamma.app.user.domain.UserRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static it.chalmers.gamma.app.authentication.AccessGuard.*; +import static it.chalmers.gamma.utils.DomainUtils.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(SpringExtension.class) +public class GroupFacadeUnitTest { + + @Mock + private AccessGuard accessGuard; + @Mock + private GroupRepository groupRepository; + @Mock + private UserRepository userRepository; + @Mock + private PostRepository postRepository; + @Mock + private SuperGroupRepository superGroupRepository; + @Mock + private SettingsRepository settingsRepository; + + @InjectMocks + private GroupFacade groupFacade; + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + + @Test + public void Given_ValidNewGroup_Expect_create_To_Work() throws GroupFacade.SuperGroupNotFoundRuntimeException, GroupFacade.GroupAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + UUID superGroupId = UUID.randomUUID(); + SuperGroup superGroup = new SuperGroup( + new SuperGroupId(superGroupId), + 0, + new Name("mysupergroup"), + new PrettyName("My Super Group"), + new SuperGroupType("cool"), + new Text( + "Det här är coolt", + "This is a cool" + ) + ); + + given(superGroupRepository.get(new SuperGroupId(superGroupId))) + .willReturn(Optional.of(superGroup)); + + GroupFacade.NewGroup newGroup = new GroupFacade.NewGroup( + "mygroup", + "My Group", + superGroupId + ); + + groupFacade.create(newGroup); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Group.class); + verify(groupRepository).save(captor.capture()); + Group capturedNewGroup = captor.getValue(); + + Assertions.assertThat(capturedNewGroup.id()) + .isNotNull(); + + Group expectedGroup = new Group( + capturedNewGroup.id(), + 0, + new Name(newGroup.name()), + new PrettyName(newGroup.prettyName()), + superGroup, + Collections.emptyList(), + Optional.empty(), + Optional.empty() + ); + + Assertions.assertThat(capturedNewGroup) + .isEqualTo(expectedGroup); + + InOrder inOrder = inOrder(accessGuard, groupRepository, superGroupRepository); + + //Makes sure that isAdmin is called first. + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(superGroupRepository).get(new SuperGroupId(superGroupId)); + inOrder.verify(groupRepository).save(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_GroupWithNameThatAlreadyExists_Expect_create_To_Throw() throws GroupRepository.GroupNameAlreadyExistsException { + doThrow(GroupRepository.GroupNameAlreadyExistsException.class) + .when(this.groupRepository).save(any()); + + UUID superGroupId = UUID.randomUUID(); + SuperGroup superGroup = new SuperGroup( + new SuperGroupId(superGroupId), + 0, + new Name("mysupergroup"), + new PrettyName("My Super Group"), + new SuperGroupType("cool"), + new Text( + "Det här är coolt", + "This is a cool" + ) + ); + + given(superGroupRepository.get(new SuperGroupId(superGroupId))) + .willReturn(Optional.of(superGroup)); + + GroupFacade.NewGroup newGroup = new GroupFacade.NewGroup( + "mygroup", + "My Group", + superGroupId + ); + + assertThatExceptionOfType(GroupFacade.GroupAlreadyExistsException.class) + .isThrownBy(() -> groupFacade.create(newGroup)); + + } + + @Test + public void Given_GroupWithInvalidSuperGroup_Expect_create_To_Throw() { + given(superGroupRepository.get(any())) + .willReturn(Optional.empty()); + + GroupFacade.NewGroup newGroup = new GroupFacade.NewGroup( + "mygroup", + "My Group", + UUID.randomUUID() + ); + + assertThatExceptionOfType(GroupFacade.SuperGroupNotFoundRuntimeException.class) + .isThrownBy(() -> groupFacade.create(newGroup)); + } + + @Test + public void Given_Group_Expect_update_To_Work() throws GroupFacade.SuperGroupNotFoundRuntimeException, GroupFacade.GroupNotFoundRuntimeException, GroupFacade.GroupAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + given(groupRepository.get(digit19.id())) + .willReturn(Optional.of(digit19)); + + given(superGroupRepository.get(didit.id())) + .willReturn(Optional.of(didit)); + + GroupFacade.UpdateGroup updateGroup = new GroupFacade.UpdateGroup( + digit19.id().value(), + 0, + "digit182", + "digIT 18", + didit.id().value() + ); + + groupFacade.update(updateGroup); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Group.class); + verify(groupRepository).save(captor.capture()); + Group capturedNewGroup = captor.getValue(); + + assertThat(capturedNewGroup) + .isEqualTo(new Group( + digit19.id(), + 0, + new Name("digit182"), + new PrettyName("digIT 18"), + didit, + digit19.groupMembers(), + digit19.avatarUri(), + digit19.bannerUri() + )); + + InOrder inOrder = inOrder(accessGuard, groupRepository, superGroupRepository); + + //Makes sure that isAdmin is called first. + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(superGroupRepository).get(didit.id()); + inOrder.verify(groupRepository).save(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_InvalidUpdateGroup_Expect_update_To_Throw_GroupNotFound() { + given(groupRepository.get(any())) + .willReturn(Optional.empty()); + + GroupFacade.UpdateGroup updateGroup = new GroupFacade.UpdateGroup( + digit19.id().value(), + 0, + "digit182", + "digIT 18", + didit.id().value() + ); + + assertThatExceptionOfType(GroupFacade.GroupNotFoundRuntimeException.class) + .isThrownBy(() -> groupFacade.update(updateGroup)); + } + + @Test + public void Given_InvalidUpdateGroup_Expect_update_To_Throw_SuperGroupNotFound() { + given(groupRepository.get(digit19.id())) + .willReturn(Optional.of(digit19)); + + given(superGroupRepository.get(didit.id())) + .willReturn(Optional.empty()); + + GroupFacade.UpdateGroup updateGroup = new GroupFacade.UpdateGroup( + digit19.id().value(), + 0, + "digit182", + "digIT 18", + didit.id().value() + ); + + assertThatExceptionOfType(GroupFacade.SuperGroupNotFoundRuntimeException.class) + .isThrownBy(() -> groupFacade.update(updateGroup)); + } + + @Test + public void Given_ValidGroup_Expect_setMembers_To_Work() throws GroupFacade.GroupNotFoundRuntimeException, GroupFacade.UserNotFoundRuntimeException, GroupFacade.PostNotFoundRuntimeException, GroupRepository.GroupNameAlreadyExistsException { + given(userRepository.get(u8.id())) + .willReturn(Optional.of(u8)); + given(userRepository.get(u9.id())) + .willReturn(Optional.of(u9)); + given(userRepository.get(u10.id())) + .willReturn(Optional.of(u10)); + given(userRepository.get(u11.id())) + .willReturn(Optional.of(u11)); + + given(postRepository.get(chair.id())) + .willReturn(Optional.of(chair)); + given(postRepository.get(treasurer.id())) + .willReturn(Optional.of(treasurer)); + given(postRepository.get(member.id())) + .willReturn(Optional.of(member)); + + given(groupRepository.get(digit19.id())) + .willReturn(Optional.of(digit19)); + + given(superGroupRepository.get(didit.id())) + .willReturn(Optional.of(didit)); + + List newMembers = List.of( + new GroupFacade.ShallowMember(u8.id().value(), chair.id().value(), "root"), + new GroupFacade.ShallowMember(u9.id().value(), member.id().value(), "ServerChef"), + new GroupFacade.ShallowMember(u10.id().value(), member.id().value(), null), + new GroupFacade.ShallowMember(u11.id().value(), treasurer.id().value(), "") + ); + + groupFacade.setMembers(digit19.id().value(), newMembers); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Group.class); + verify(groupRepository).save(captor.capture()); + Group capturedNewGroup = captor.getValue(); + + assertThat(capturedNewGroup) + .isEqualTo(digit19.withGroupMembers(List.of( + gm(u8, chair, new UnofficialPostName("root")), + gm(u9, member, new UnofficialPostName("ServerChef")), + gm(u10, member), + gm(u11, treasurer) + ))); + + InOrder inOrder = inOrder(accessGuard, groupRepository, superGroupRepository); + + //Makes sure that isAdmin is called first. + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(groupRepository).save(any()); + } + + @Test + public void Given_InvalidGroup_Expect_setMembers_To_Throw() { + given(groupRepository.get(any())) + .willReturn(Optional.empty()); + + List newMembers = List.of( + new GroupFacade.ShallowMember(u8.id().value(), chair.id().value(), "root") + ); + + assertThatExceptionOfType(GroupFacade.GroupNotFoundRuntimeException.class) + .isThrownBy(() -> groupFacade.setMembers(digit19.id().value(), newMembers)); + } + + @Test + public void Given_InvalidUser_Expect_setMembers_To_Throw() { + given(groupRepository.get(digit19.id())) + .willReturn(Optional.of(digit19)); + given(postRepository.get(chair.id())) + .willReturn(Optional.of(chair)); + given(userRepository.get((UserId) any())) + .willReturn(Optional.empty()); + + List newMembers = List.of( + new GroupFacade.ShallowMember(u8.id().value(), chair.id().value(), "root") + ); + + assertThatExceptionOfType(GroupFacade.UserNotFoundRuntimeException.class) + .isThrownBy(() -> groupFacade.setMembers(digit19.id().value(), newMembers)); + } + + @Test + public void Given_InvalidPosts_Expect_setMembers_To_Throw() { + given(groupRepository.get(digit19.id())) + .willReturn(Optional.of(digit19)); + given(postRepository.get(chair.id())) + .willReturn(Optional.empty()); + given(userRepository.get(u8.id())) + .willReturn(Optional.of(u8)); + + List newMembers = List.of( + new GroupFacade.ShallowMember(u8.id().value(), chair.id().value(), "root") + ); + + assertThatExceptionOfType(GroupFacade.PostNotFoundRuntimeException.class) + .isThrownBy(() -> groupFacade.setMembers(digit19.id().value(), newMembers)); + } + + @Test + public void Given_ValidGroup_Expect_delete_To_Work() throws GroupFacade.GroupNotFoundRuntimeException, GroupRepository.GroupNotFoundException { + GroupId groupId = GroupId.generate(); + + groupFacade.delete(groupId.value()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(GroupId.class); + verify(groupRepository).delete(captor.capture()); + GroupId deleteGroupId = captor.getValue(); + + Assertions.assertThat(deleteGroupId) + .isEqualTo(groupId); + + InOrder inOrder = inOrder(accessGuard, groupRepository, superGroupRepository); + + //Makes sure that isAdmin is called first. + inOrder.verify(accessGuard).require(isAdmin()); + inOrder.verify(groupRepository).delete(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_InvalidGroup_Expect_delete_To_Throw() throws GroupRepository.GroupNotFoundException { + doThrow(GroupRepository.GroupNotFoundException.class) + .when(groupRepository).delete(any()); + + assertThatExceptionOfType(GroupFacade.GroupNotFoundRuntimeException.class) + .isThrownBy(() -> groupFacade.delete(UUID.randomUUID())); + } + + @Test + public void Given_ValidGroupId_Expect_getWithMembers_To_Work() { + given(groupRepository.get(digit19.id())) + .willReturn(Optional.of(digit19)); + + GroupFacade.GroupWithMembersDTO group = this.groupFacade.getWithMembers(digit19.id().value()) + .orElseThrow(); + + assertThat(group) + .isEqualTo( + new GroupFacade.GroupWithMembersDTO( + digit19.id().value(), + 0, + digit19.name().value(), + digit19.prettyName().value(), + digit19.groupMembers().stream().map(GroupFacade.GroupMemberDTO::new).toList(), + new SuperGroupFacade.SuperGroupDTO(digit) + ) + ); + + InOrder inOrder = inOrder(accessGuard, groupRepository); + + inOrder.verify(accessGuard).require(isSignedIn()); + inOrder.verify(groupRepository).get(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_Groups_Expect_getAll_To_Work() { + given(groupRepository.getAll()) + .willReturn(List.of(digit18, digit19, prit18, prit19)); + + List groups = groupFacade.getAll(); + + assertThat(groups) + .containsExactlyInAnyOrder( + new GroupFacade.GroupDTO(digit18), + new GroupFacade.GroupDTO(digit19), + new GroupFacade.GroupDTO(prit18), + new GroupFacade.GroupDTO(prit19) + ); + + InOrder inOrder = inOrder(accessGuard, groupRepository); + + inOrder.verify(accessGuard).requireEither(isSignedIn(), isClientApi()); + inOrder.verify(groupRepository).getAll(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_ValidGroups_Expect_getAllForInfoApi() { + given(settingsRepository.getSettings()) + .willReturn(new Settings( + Instant.now(), + List.of(committee, board) + )); + + given(groupRepository.getAll()) + .willReturn(List.of(digit18, digit19, prit18, prit19, styrit18, styrit19, drawit18, drawit19)); + + List groups = groupFacade.getAllForInfoApi(); + + + assertThat(groups) + .containsExactlyInAnyOrder( + new GroupFacade.GroupWithMembersDTO(digit19), + new GroupFacade.GroupWithMembersDTO(prit19), + new GroupFacade.GroupWithMembersDTO(styrit19) + ); + + InOrder inOrder = inOrder(accessGuard, groupRepository); + + //TODO: Make sure only info can call + inOrder.verify(accessGuard).require(isApi(any())); + inOrder.verify(groupRepository).getAll(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void Given_ValidGroups_Expect_getAllBySuperGroup_To_Work() { + given(groupRepository.getAllBySuperGroup(didit.id())) + .willReturn(List.of(digit18)); + + List groups = groupFacade.getAllBySuperGroup(didit.id().value()); + + assertThat(groups) + .containsExactlyInAnyOrder( + new GroupFacade.GroupWithMembersDTO(digit18) + ); + + InOrder inOrder = inOrder(accessGuard, groupRepository); + + inOrder.verify(accessGuard).require(isSignedIn()); + inOrder.verify(groupRepository).getAllBySuperGroup(any()); + inOrder.verifyNoMoreInteractions(); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/app/post/PostFacadeIntegrationTest.java b/backend/src/test/java/it/chalmers/gamma/app/post/PostFacadeIntegrationTest.java new file mode 100644 index 000000000..bacdf47c0 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/app/post/PostFacadeIntegrationTest.java @@ -0,0 +1,15 @@ +package it.chalmers.gamma.app.post; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.security.core.context.SecurityContextHolder; + +public class PostFacadeIntegrationTest { + + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + +} diff --git a/backend/src/test/java/it/chalmers/gamma/app/post/PostFacadeUnitTest.java b/backend/src/test/java/it/chalmers/gamma/app/post/PostFacadeUnitTest.java new file mode 100644 index 000000000..173b1ccbc --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/app/post/PostFacadeUnitTest.java @@ -0,0 +1,13 @@ +package it.chalmers.gamma.app.post; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.security.core.context.SecurityContextHolder; + +public class PostFacadeUnitTest { + + @BeforeEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/utils/DomainUtils.java b/backend/src/test/java/it/chalmers/gamma/utils/DomainUtils.java new file mode 100644 index 000000000..5e175cf36 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/utils/DomainUtils.java @@ -0,0 +1,314 @@ +package it.chalmers.gamma.utils; + +import it.chalmers.gamma.app.client.domain.authority.Authority; +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; +import it.chalmers.gamma.app.group.domain.*; +import it.chalmers.gamma.app.post.domain.Post; +import it.chalmers.gamma.app.post.domain.PostId; +import it.chalmers.gamma.app.post.domain.PostRepository; +import it.chalmers.gamma.app.settings.domain.Settings; +import it.chalmers.gamma.app.supergroup.domain.*; +import it.chalmers.gamma.app.user.domain.*; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; + +/** + * Helper functions + */ +public final class DomainUtils { + + public static final Settings defaultSettings = new Settings( + Instant.now().minus(1, ChronoUnit.DAYS), + Collections.emptyList() + ); + public static final SuperGroupType committee = new SuperGroupType("committee"); + public static final SuperGroupType board = new SuperGroupType("board"); + public static final SuperGroupType alumni = new SuperGroupType("alumni"); + public static final SuperGroupType society = new SuperGroupType("society"); + public static final Post chair = new Post( + PostId.generate(), + 0, + new Text( + "Ordförande", + "Chairman" + ), + new EmailPrefix("ordf") + ); + public static final Post treasurer = new Post( + PostId.generate(), + 0, + new Text( + "Kassör", + "Treasurer" + ), + new EmailPrefix("kassor") + ); + public static final Post member = new Post( + PostId.generate(), + 0, + new Text( + "Ledamot", + "Member" + ), + EmailPrefix.none() + ); + public static SuperGroup digit = sg("digit", committee); + public static SuperGroup didit = sg("didit", alumni); + public static SuperGroup prit = sg("prit", committee); + public static SuperGroup sprit = sg("sprit", alumni); + public static SuperGroup drawit = sg("drawit", society); + public static SuperGroup dragit = sg("dragit", alumni); + public static SuperGroup styrit = sg("styrit", board); + public static SuperGroup emeritus = sg("emeritus", alumni); + public static GammaUser u0 = u("abcaa"); + public static GammaUser u1 = u("abca"); + public static GammaUser u2 = u("abcb", true, true); + public static Group digit18 = g("digit18", didit, List.of(gm(u1, chair, new UnofficialPostName("root")), gm(u2, treasurer))); + public static Group prit18 = g("prit18", sprit, List.of(gm(u1, chair, new UnofficialPostName("ChefChef")), gm(u1, treasurer), gm(u2, member))); + public static GammaUser u3 = u("abcc", false, false); + public static GammaUser u4 = u("abcd"); + public static Group digit19 = g("digit19", digit, List.of(gm(u3, chair), gm(u4, member), gm(u2, member))); + public static GammaUser u5 = u("abce", false, false); + public static GammaUser u6 = u("abcf"); + public static Group prit19 = g("prit19", prit, List.of(gm(u5, chair), gm(u6, treasurer, new UnofficialPostName("Kas$$Chef")), gm(u6, member))); + public static Group drawit18 = g("drawit18", dragit, List.of(gm(u6, chair))); + public static GammaUser u7 = u("abcg", true, true); + public static GammaUser u8 = u("abch"); + public static GammaUser u9 = u("abci"); + public static Group styrit18 = g("styrit18", emeritus, List.of(gm(u7, chair), gm(u8, member), gm(u9, member))); + public static GammaUser u10 = u("abcj"); + public static GammaUser u11 = u("abck"); + public static Group digit17 = g("digit17", didit, List.of(gm(u11, chair), gm(u2, treasurer), gm(u4, member))); + public static Group drawit19 = g("drawit19", drawit, List.of(gm(u1, chair), gm(u11, member))); + public static Group styrit19 = g("styrit19", styrit, List.of(gm(u10, chair), gm(u11, treasurer))); + + public static SuperGroup sg(String name, SuperGroupType type) { + return new SuperGroup( + SuperGroupId.generate(), + 0, + new Name(name), + new PrettyName(name.toUpperCase()), + type, + new Text() + ); + } + + public static Group g(String name, SuperGroup sg, List members) { + return new Group( + GroupId.generate(), + 0, + new Name(name), + new PrettyName(name.toUpperCase()), + sg, + members, + Optional.empty(), + Optional.empty() + ); + } + + public static GroupMember gm(GammaUser u, Post p) { + return gm(u, p, UnofficialPostName.none()); + } + + public static GroupMember gm(GammaUser u, Post p, UnofficialPostName unofficialPostName) { + return new GroupMember( + p, + unofficialPostName, + u + ); + } + + public static GammaUser u(String cid) { + return u(cid, false, true); + } + + public static GammaUser u(String cid, boolean locked, boolean gdprTrained) { + return new GammaUser( + UserId.generate(), + new Cid(cid), + new Nick("N-" + cid), + new FirstName("F-" + cid), + new LastName("L-" + cid), + new AcceptanceYear(2021), + Language.SV, + new UserExtended( + new Email(cid + "@chalmers.it"), + 0, + true, + locked, + null + ) + ); + } + + /** + * Basically adds 1 to each version since they all have been saved. + * Also removes extended. + */ + public static Group asSaved(Group group) { + return new Group( + group.id(), + group.version() + 1, + group.name(), + group.prettyName(), + group.superGroup().withVersion(1), + group.groupMembers() + .stream() + .map(groupMember -> new GroupMember( + groupMember.post().withVersion(1), + groupMember.unofficialPostName(), + groupMember.user().withExtended(groupMember.user().extended().withVersion(1)))) + .toList(), + group.avatarUri(), + group.bannerUri() + ); + } + + public static Group removeLockedUsers(Group group) { + return group.withGroupMembers( + group.groupMembers() + .stream() + .filter(groupMember -> !(groupMember.user().extended().locked() || !groupMember.user().extended().acceptedUserAgreement())) + .toList() + ); + } + + public static Group removeUserExtended(Group group) { + return group.withGroupMembers(group.groupMembers() + .stream() + .map(groupMember -> groupMember.withUser(removeUserExtended(groupMember.user()))) + .toList()); + } + + public static GammaUser removeUserExtended(GammaUser user) { + return user.withExtended(null); + } + + public static GammaUser asSaved(GammaUser user) { + return user.withExtended(user.extended().withVersion(1)); + } + + public static Authority asSaved(Authority authority) { + return new Authority( + authority.client(), + authority.name(), + authority.posts() + .stream() + .map(superGroupPost -> new Authority.SuperGroupPost( + superGroupPost.superGroup().withVersion(1), + superGroupPost.post().withVersion(1) + )) + .toList(), + authority.superGroups() + .stream() + .map(superGroup -> superGroup.withVersion(1)) + .toList(), + authority.users() + .stream() + .map(user -> user.withExtended(user.extended().withVersion(1))) + .toList() + ); + } + + public static void addAll(UserRepository userRepository, GammaUser... users) { + addAll(userRepository, List.of(users)); + } + + public static void addAll(UserRepository userRepository, List users) { + for (GammaUser user : users) { + try { + userRepository.create(user, new UnencryptedPassword("password")); + } catch (UserRepository.CidAlreadyInUseException | UserRepository.EmailAlreadyInUseException e) { + e.printStackTrace(); + } + } + } + + public static void addAll(SuperGroupRepository superGroupRepository, SuperGroup... superGroups) { + for (SuperGroup superGroup : superGroups) { + superGroupRepository.save(superGroup); + } + } + + public static void addAll(GroupRepository groupRepository, Group... groups) throws GroupRepository.GroupNameAlreadyExistsException { + for (Group group : groups) { + groupRepository.save(group); + } + } + + public static void addAll(PostRepository postRepository, Post... posts) { + addAll(postRepository, List.of(posts)); + } + + public static void addAll(PostRepository postRepository, List posts) { + for (Post post : posts) { + postRepository.save(post); + } + } + + public static void addAll(SuperGroupTypeRepository superGroupTypeRepository, SuperGroupType... types) throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException { + for (SuperGroupType type : types) { + superGroupTypeRepository.add(type); + } + } + + public static void addGroup(SuperGroupTypeRepository superGroupTypeRepository, + SuperGroupRepository superGroupRepository, + UserRepository userRepository, + PostRepository postRepository, + GroupRepository groupRepository, + Group... groups) + throws SuperGroupTypeRepository.SuperGroupTypeAlreadyExistsException, GroupRepository.GroupNameAlreadyExistsException { + Set types = new HashSet<>(); + Set superGroups = new HashSet<>(); + Set posts = new HashSet<>(); + Set users = new HashSet<>(); + + for (Group group : groups) { + types.add(group.superGroup().type()); + superGroups.add(group.superGroup()); + group.groupMembers().forEach(groupMember -> { + posts.add(groupMember.post()); + users.add(groupMember.user()); + }); + } + + for (SuperGroupType type : types) { + superGroupTypeRepository.add(type); + } + for (SuperGroup superGroup : superGroups) { + superGroupRepository.save(superGroup); + } + for (Post post : posts) { + postRepository.save(post); + } + for (GammaUser user : users) { + try { + userRepository.create(user, new UnencryptedPassword("password")); + } catch (UserRepository.CidAlreadyInUseException | UserRepository.EmailAlreadyInUseException e) { + e.printStackTrace(); + } + } + for (Group group : groups) { + groupRepository.save(group); + } + } + + public static Post p() { + PostId postId = PostId.generate(); + return new Post( + PostId.generate(), + 0, + new Text( + postId + " - swedish", + postId + " - english" + ), + new EmailPrefix("something") + ); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/utils/FlywayMigrationConfig.java b/backend/src/test/java/it/chalmers/gamma/utils/FlywayMigrationConfig.java new file mode 100644 index 000000000..28f48d994 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/utils/FlywayMigrationConfig.java @@ -0,0 +1,25 @@ +package it.chalmers.gamma.utils; + +import org.flywaydb.core.Flyway; + +import java.sql.Connection; +import java.sql.SQLException; + +public class FlywayMigrationConfig { + + /** + * Called by Testcontainer. Check application-test.yml, there you can see that this function is specified + */ + public static void initFlywayFromTC(Connection connection) throws SQLException { + Flyway flyway = Flyway.configure() + .dataSource( + connection.getMetaData().getURL(), + connection.getMetaData().getUserName(), + "test" + ).baselineOnMigrate(true) + .locations("classpath:/db/migration") + .load(); + flyway.migrate(); + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/utils/GammaSecurityContextHolderTestUtils.java b/backend/src/test/java/it/chalmers/gamma/utils/GammaSecurityContextHolderTestUtils.java new file mode 100644 index 000000000..f0789fb45 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/utils/GammaSecurityContextHolderTestUtils.java @@ -0,0 +1,163 @@ +package it.chalmers.gamma.utils; + +import it.chalmers.gamma.app.admin.domain.AdminRepository; +import it.chalmers.gamma.app.apikey.domain.ApiKey; +import it.chalmers.gamma.app.apikey.domain.ApiKeyId; +import it.chalmers.gamma.app.apikey.domain.ApiKeyToken; +import it.chalmers.gamma.app.apikey.domain.ApiKeyType; +import it.chalmers.gamma.app.client.domain.*; +import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.common.PrettyName; +import it.chalmers.gamma.app.common.Text; +import it.chalmers.gamma.app.image.domain.ImageUri; +import it.chalmers.gamma.app.user.domain.*; +import it.chalmers.gamma.security.api.ApiAuthenticationToken; +import it.chalmers.gamma.security.authentication.ApiAuthentication; +import it.chalmers.gamma.security.authentication.UserAuthentication; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; + +import java.util.Collections; +import java.util.Optional; + +public class GammaSecurityContextHolderTestUtils { + + public static final GammaUser DEFAULT_USER = new GammaUser( + //Same as in the default for WithUser + UserId.valueOf("e3404d7e-bd03-43ec-ba74-67054fa70d94"), + new Cid("asdf"), + new Nick("RandoM"), + new FirstName("Smurf"), + new LastName("Smurfsson"), + new AcceptanceYear(2018), + Language.EN, + new UserExtended( + new Email("smurf@chalmers.it"), + 0, + true, + false, + ImageUri.defaultUserAvatar() + ) + ); + + private static final ApiKey DEFAULT_CLIENT_API_KEY = new ApiKey( + ApiKeyId.generate(), + new PrettyName("Mat"), + new Text( + "Api nyckel för mat", + "Api key for mat" + ), + ApiKeyType.CLIENT, + ApiKeyToken.generate() + ); + + public static final Client DEFAULT_CLIENT = new Client( + ClientUid.generate(), + ClientId.generate(), + ClientSecret.generate(), + new ClientRedirectUrl("https://mat.chalmers.it"), + new PrettyName("Mat"), + new Text( + "Klient för mat", + "Client for mat" + ), + Collections.emptyList(), + DEFAULT_CLIENT_API_KEY, + new ClientOwnerOfficial(), + null + ); + + public static GammaUser setAuthenticatedAsNormalUser(UserRepository userRepository) { + setAuthenticatedUser(userRepository, null, DEFAULT_USER, false); + return DEFAULT_USER.withExtended(DEFAULT_USER.extended().withVersion(1)); + } + + public static GammaUser setAuthenticatedAsAdminUser(UserRepository userRepository, AdminRepository adminRepository) { + setAuthenticatedUser(userRepository, adminRepository, DEFAULT_USER, true); + return DEFAULT_USER.withExtended(DEFAULT_USER.extended().withVersion(1)); + } + + public static void setAuthenticatedAsAdminUser(GammaUser user) { + setAuthenticatedUser(null, null, user, true); + } + + public static void setAuthenticatedUser(UserRepository userRepository, + AdminRepository adminRepository, + GammaUser gammaUser, + boolean isAdmin) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + String password = "password"; + + if (userRepository != null) { + try { + userRepository.create(gammaUser, new UnencryptedPassword(password)); + } catch (UserRepository.CidAlreadyInUseException | UserRepository.EmailAlreadyInUseException e) { + e.printStackTrace(); + } + } + + adminRepository.setAdmin(gammaUser.id(), isAdmin); + + User user = new User(gammaUser.id().value().toString(), "{noop}" + password, Collections.emptyList()); + + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( + user, + null, + Collections.emptyList() + ); + context.setAuthentication(auth); + SecurityContextHolder.setContext(context); + + + auth.setDetails(new UserAuthentication() { + @Override + public GammaUser get() { + return gammaUser; + } + + @Override + public boolean isAdmin() { + return isAdmin; + } + }); + } + + /** + * Will have no approved users + */ + public static ClientWithApiKey setAuthenticatedAsClientWithApi(ClientRepository clientRepository) { + clientRepository.save(DEFAULT_CLIENT); + + setAuthenticatedAsClientWithApi(DEFAULT_CLIENT); + + return new ClientWithApiKey(DEFAULT_CLIENT, DEFAULT_CLIENT_API_KEY); + } + + public static void setAuthenticatedAsClientWithApi(Client client) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + if (client.clientApiKey().isEmpty()) { + throw new IllegalArgumentException(); + } + + context.setAuthentication(ApiAuthenticationToken.fromAuthenticatedApiKey(new ApiAuthentication() { + @Override + public ApiKey get() { + return client.clientApiKey().get(); + } + + @Override + public Optional getClient() { + return Optional.of(client); + } + })); + + SecurityContextHolder.setContext(context); + } + + public record ClientWithApiKey(Client client, ApiKey apiKey) { + } + +} diff --git a/backend/src/test/java/it/chalmers/gamma/utils/PasswordEncoderTestConfiguration.java b/backend/src/test/java/it/chalmers/gamma/utils/PasswordEncoderTestConfiguration.java new file mode 100644 index 000000000..19bc9e1e1 --- /dev/null +++ b/backend/src/test/java/it/chalmers/gamma/utils/PasswordEncoderTestConfiguration.java @@ -0,0 +1,16 @@ +package it.chalmers.gamma.utils; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; + +@TestConfiguration +public class PasswordEncoderTestConfiguration { + + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + +} diff --git a/backend/src/test/resources/application-test-with-mock.yml b/backend/src/test/resources/application-test-with-mock.yml new file mode 100644 index 000000000..077753b52 --- /dev/null +++ b/backend/src/test/resources/application-test-with-mock.yml @@ -0,0 +1,27 @@ +spring: + datasource: + # Flyway migration settings => FlywayMigrationConfig.java + url: jdbc:tc:postgresql:15:///?TC_INITFUNCTION=it.chalmers.gamma.utils.FlywayMigrationConfig::initFlywayFromTC + hikari: + # Set since otherwise the tests will fail because of too many tests that are using the testcontainer + # https://stackoverflow.com/a/63474915/1663265 + minimum-idle: 1 + jpa: + database-platform: org.hibernate.dialect.PostgreSQL9Dialect + properties: + hibernate: + globally_quoted_identifiers: true + temp: + use_jdbc_metadata_defaults: false + hibernate: + ddl-auto: validate + open-in-view: false + session: + store-type: none +logging: + level: + it.chalmers.gamma: DEBUG + +application: + mocking: true + admin-setup: true \ No newline at end of file diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml new file mode 100644 index 000000000..bba3917c7 --- /dev/null +++ b/backend/src/test/resources/application-test.yml @@ -0,0 +1,26 @@ +spring: + datasource: + # Flyway migration settings => FlywayMigrationConfig.java + url: jdbc:tc:postgresql:15:///?TC_INITFUNCTION=it.chalmers.gamma.utils.FlywayMigrationConfig::initFlywayFromTC + hikari: + # Set since otherwise the tests will fail because of too many tests that are using the testcontainer + # https://stackoverflow.com/a/63474915/1663265 + minimum-idle: 1 + jpa: + database-platform: org.hibernate.dialect.PostgreSQL9Dialect + properties: + hibernate: + globally_quoted_identifiers: true + temp: + use_jdbc_metadata_defaults: false + hibernate: + ddl-auto: validate + open-in-view: false + session: + store-type: none +logging: + level: + it.chalmers.gamma: INFO +application: + mocking: false + admin-setup: false diff --git a/demos/java-client/.gitignore b/demos/java-client/.gitignore new file mode 100644 index 000000000..0cdbb81ed --- /dev/null +++ b/demos/java-client/.gitignore @@ -0,0 +1,33 @@ +.gradle/ +/build/ +!gradle/wrapper/gradle-wrapper.jar +/uploads/ + +/src/main/resources/secrets.properties + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +/out/ +.idea/ + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +secrets.properties diff --git a/demos/java-client/HELP.md b/demos/java-client/HELP.md new file mode 100644 index 000000000..d48f84973 --- /dev/null +++ b/demos/java-client/HELP.md @@ -0,0 +1,28 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Gradle documentation](https://docs.gradle.org) +* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.5.5/gradle-plugin/reference/html/) +* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.5.5/gradle-plugin/reference/html/#build-image) +* [Spring Boot DevTools](https://docs.spring.io/spring-boot/docs/2.5.5/reference/htmlsingle/#using-boot-devtools) +* [Spring Web](https://docs.spring.io/spring-boot/docs/2.5.5/reference/htmlsingle/#boot-features-developing-web-applications) +* [Spring Security](https://docs.spring.io/spring-boot/docs/2.5.5/reference/htmlsingle/#boot-features-security) +* [OAuth2 Client](https://docs.spring.io/spring-boot/docs/2.5.5/reference/htmlsingle/#boot-features-security-oauth2-client) + +### Guides +The following guides illustrate how to use some features concretely: + +* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) +* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) +* [Building REST services with Spring](https://spring.io/guides/tutorials/bookmarks/) +* [Securing a Web Application](https://spring.io/guides/gs/securing-web/) +* [Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/) +* [Authenticating a User with LDAP](https://spring.io/guides/gs/authenticating-ldap/) + +### Additional Links +These additional references should also help you: + +* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) + diff --git a/demos/java-client/build.gradle b/demos/java-client/build.gradle new file mode 100644 index 000000000..3b922a05a --- /dev/null +++ b/demos/java-client/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'org.springframework.boot' version '3.0.1' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'java' +} + +group = 'it.chalmers' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '17' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:3.0.1' + implementation 'org.springframework.boot:spring-boot-starter-security:3.0.1' + implementation 'org.springframework.boot:spring-boot-starter-web:3.0.1' + developmentOnly 'org.springframework.boot:spring-boot-devtools:3.0.1' +} + diff --git a/demos/java-client/gradle/wrapper/gradle-wrapper.jar b/demos/java-client/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..7454180f2 Binary files /dev/null and b/demos/java-client/gradle/wrapper/gradle-wrapper.jar differ diff --git a/demos/java-client/gradle/wrapper/gradle-wrapper.properties b/demos/java-client/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..070cb702f --- /dev/null +++ b/demos/java-client/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/demos/java-client/gradlew b/demos/java-client/gradlew new file mode 100755 index 000000000..1b6c78733 --- /dev/null +++ b/demos/java-client/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/demos/java-client/gradlew.bat b/demos/java-client/gradlew.bat new file mode 100644 index 000000000..ac1b06f93 --- /dev/null +++ b/demos/java-client/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/demos/java-client/settings.gradle b/demos/java-client/settings.gradle new file mode 100644 index 000000000..0a383dd84 --- /dev/null +++ b/demos/java-client/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'demo' diff --git a/demos/java-client/src/main/java/it/chalmers/demo/DemoClientApplication.java b/demos/java-client/src/main/java/it/chalmers/demo/DemoClientApplication.java new file mode 100644 index 000000000..7a87aed02 --- /dev/null +++ b/demos/java-client/src/main/java/it/chalmers/demo/DemoClientApplication.java @@ -0,0 +1,40 @@ +package it.chalmers.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.http.*; +import org.springframework.web.client.RestTemplate; + +@SpringBootApplication +public class DemoClientApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoClientApplication.class, args); + +// HttpHeaders headers = new HttpHeaders(); +// headers.add(HttpHeaders.AUTHORIZATION, "pre-shared INFO-super-secret-code"); +// headers.setContentType(MediaType.APPLICATION_JSON); +// +// HttpEntity requestEntity = new HttpEntity<>(headers); +// +// RestTemplate restTemplate = new RestTemplate(); +// ResponseEntity response = restTemplate.exchange("http://gamma:8081/api/external/info/v1/users/d5683b6d-a0e5-4169-b9c4-e0f82644b591", HttpMethod.GET, requestEntity, String.class); +// System.out.println("Gamma responded with " + response.getHeaders() + response.getBody()); + + HttpHeaders headers2 = new HttpHeaders(); + headers2.add(HttpHeaders.AUTHORIZATION, "pre-shared test-api-key-secret-code"); + headers2.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity requestEntity2 = new HttpEntity<>(headers2); + + RestTemplate restTemplate2 = new RestTemplate(); + try{ + ResponseEntity response2 = restTemplate2.exchange("http://gamma:8081/api/external/client/v1/users/c6a5fcf4-d8f0-4f1b-993b-e096e1832d23", HttpMethod.GET, requestEntity2, String.class); + System.out.println("Gamma responded with " + response2.getHeaders() + response2.getBody()); + }catch(Exception e) { + e.printStackTrace(); + } + } + + +} diff --git a/demos/java-client/src/main/java/it/chalmers/demo/DemoWebSecurity.java b/demos/java-client/src/main/java/it/chalmers/demo/DemoWebSecurity.java new file mode 100644 index 000000000..9bc4bb9f1 --- /dev/null +++ b/demos/java-client/src/main/java/it/chalmers/demo/DemoWebSecurity.java @@ -0,0 +1,50 @@ +package it.chalmers.demo; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; + +@EnableWebSecurity +@Configuration +public class DemoWebSecurity{ + + private final GammaAuthoritiesMapper gammaAuthoritiesMapper; + + public DemoWebSecurity(GammaAuthoritiesMapper gammaAuthoritiesMapper) { + this.gammaAuthoritiesMapper = gammaAuthoritiesMapper; + } + + @Bean + SecurityFilterChain defaultSecurityChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorization -> + authorization + .requestMatchers("/index.html").permitAll() + .anyRequest().authenticated() + ) + .exceptionHandling(e -> e + .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + ) + .csrf(c -> c + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + ) + .logout(l -> l + .logoutSuccessUrl("/").permitAll() + ) + .oauth2Login(oauth2 -> oauth2 + .loginPage("/oauth2/authorization/gamma") + .defaultSuccessUrl("/", true) + .failureHandler((request, response, exception) -> { + exception.printStackTrace(); + }) + ) + .oauth2Client(Customizer.withDefaults()); + return http.build(); + } +} \ No newline at end of file diff --git a/demos/java-client/src/main/java/it/chalmers/demo/GammaAuthoritiesMapper.java b/demos/java-client/src/main/java/it/chalmers/demo/GammaAuthoritiesMapper.java new file mode 100644 index 000000000..982243fa7 --- /dev/null +++ b/demos/java-client/src/main/java/it/chalmers/demo/GammaAuthoritiesMapper.java @@ -0,0 +1,70 @@ +package it.chalmers.demo; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Component +public class GammaAuthoritiesMapper implements GrantedAuthoritiesMapper { + + /** + * Spring always adds a default ROLE_USER role. + * I use this to get the information to create proper authorities + * The information is from the claims from when calling the userinfo endpoint from gamma + * The of the "authorities" claim is + * [ + * { + * type: "GROUP", + * authority: "digit2018" + * }, + * { + * type: "SUPERGROUP", + * authority: "digit" + * }, + * { + * type: "AUTHORITY", + * authority: "admin" + * } + * ] + * The authorities that will be created is: + * [ + * "GROUP_DIGIT2018", + * "SUPERGROUP_DIGIT", + * "AUTHORITY_ADMIN" + * ] + */ + @Override + public Collection mapAuthorities(Collection grantedAuthorities) { + Set newGrantedAuthorities = new HashSet<>(); + for (GrantedAuthority grantedAuthority : grantedAuthorities) { + if (grantedAuthority instanceof OidcUserAuthority oidcUserAuthority) { + OidcUserInfo userInfo = oidcUserAuthority.getUserInfo(); +// List> authorities = userInfo.getClaim("authorities"); +// for (Map authority : authorities) { +// newGrantedAuthorities.add( +// new SimpleGrantedAuthority( +// authority.get("type").toUpperCase() +// + "_" +// + authority.get("authority").toUpperCase() +// ) +// ); +// } + } + } + + System.out.println(newGrantedAuthorities); + + return newGrantedAuthorities; + } + +} diff --git a/demos/java-client/src/main/java/it/chalmers/demo/GammaUser.java b/demos/java-client/src/main/java/it/chalmers/demo/GammaUser.java new file mode 100644 index 000000000..88316a1b0 --- /dev/null +++ b/demos/java-client/src/main/java/it/chalmers/demo/GammaUser.java @@ -0,0 +1,10 @@ +package it.chalmers.demo; + +import java.security.Principal; + +public class GammaUser implements Principal { + @Override + public String getName() { + return null; + } +} diff --git a/demos/java-client/src/main/java/it/chalmers/demo/GammaUserService.java b/demos/java-client/src/main/java/it/chalmers/demo/GammaUserService.java new file mode 100644 index 000000000..ff92536b9 --- /dev/null +++ b/demos/java-client/src/main/java/it/chalmers/demo/GammaUserService.java @@ -0,0 +1,25 @@ +package it.chalmers.demo; + +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +@Service +public class GammaUserService implements OAuth2UserService { + + private final OidcUserService oidcUserService; + + public GammaUserService() { + oidcUserService = new OidcUserService(); + } + + @Override + public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { + OidcUser oidcUser = oidcUserService.loadUser(userRequest); + return oidcUser; + } +} diff --git a/demos/java-client/src/main/java/it/chalmers/demo/MeController.java b/demos/java-client/src/main/java/it/chalmers/demo/MeController.java new file mode 100644 index 000000000..3b601fec5 --- /dev/null +++ b/demos/java-client/src/main/java/it/chalmers/demo/MeController.java @@ -0,0 +1,31 @@ +package it.chalmers.demo; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; +import java.util.Collections; +import java.util.Map; + +@RestController +@RequestMapping +public class MeController { + + @GetMapping("/me") + public Object user(Authentication authentication) { + System.out.println("WHAT"); + System.out.println(authentication); + return authentication; + } + + @GetMapping("/lol") + public String lol() { + System.out.println("what"); + return "lol"; + } + +} diff --git a/demos/java-client/src/main/resources/application.yml b/demos/java-client/src/main/resources/application.yml new file mode 100644 index 000000000..759bc204a --- /dev/null +++ b/demos/java-client/src/main/resources/application.yml @@ -0,0 +1,26 @@ +server: + port: 3001 +logging: + level: + root: INFO +spring: + security: + oauth2: + client: + registration: + gamma: + client-id: test + client-secret: secret + client-authentication-method: client_secret_basic + authorization-grant-type: authorization_code + scope: openid,profile,email + redirect-uri: http://client:3001/login/oauth2/code/gamma + provider: gamma + client-name: gamma + provider: + gamma: + token-uri: http://gamma:8081/api/oauth2/token + authorization-uri: http://gamma:8081/api/oauth2/authorize + jwk-set-uri: http://gamma:8081/api/oauth2/jwks + user-info-uri: http://gamma:8081/api/userinfo + user-name-attribute: name diff --git a/demos/java-client/src/main/resources/static/index.html b/demos/java-client/src/main/resources/static/index.html new file mode 100644 index 000000000..1e7f5512a --- /dev/null +++ b/demos/java-client/src/main/resources/static/index.html @@ -0,0 +1,23 @@ + + + + + + Demo + + + + +

Login

+
+ With Gamma: click here + Me +
+ + + \ No newline at end of file diff --git a/demos/readme.md b/demos/readme.md new file mode 100644 index 000000000..8464308d5 --- /dev/null +++ b/demos/readme.md @@ -0,0 +1 @@ +# Demos using Gamma diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..8cebb5fc9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +version: "2" +services: + db: + image: postgres:13 + restart: always + environment: + POSTGRES_PASSWORD: example + ports: + - 5432:5432 + + adminer: + image: adminer + restart: always + ports: + - 8080:8080 + + frontend: + image: frontend:development + build: + context: ./frontend/ + dockerfile: dev.dockerfile + ports: + - 3000:3000 + volumes: + - ./frontend/src:/usr/src/app/src + - ./frontend/public:/usr/src/app/public + network_mode: host + redis: + image: redis:5.0 + restart: always + ports: + - 6379:6379 + backend: + build: + context: ./backend/ + dockerfile: dev.Dockerfile + network_mode: host + environment: + REDIS_HOST: localhost diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 000000000..ad53f1383 --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +VITE_FRONTEND_URL=http://gamma:3000 \ No newline at end of file diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 000000000..3d504cb58 --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: {browser: true, es2020: true}, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + {allowConstantExport: true}, + ], + }, +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.idea/frontend.iml b/frontend/.idea/frontend.iml new file mode 100644 index 000000000..0c8867d7e --- /dev/null +++ b/frontend/.idea/frontend.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/.idea/modules.xml b/frontend/.idea/modules.xml new file mode 100644 index 000000000..f3d93d75a --- /dev/null +++ b/frontend/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/.idea/prettier.xml b/frontend/.idea/prettier.xml new file mode 100644 index 000000000..727b8b533 --- /dev/null +++ b/frontend/.idea/prettier.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/frontend/.idea/vcs.xml b/frontend/.idea/vcs.xml new file mode 100644 index 000000000..6c0b86358 --- /dev/null +++ b/frontend/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 000000000..0d6babedd --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 000000000..c564f6192 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + Gamma + + + + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 000000000..bc0e4ee40 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,36 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite ", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@stylexjs/stylex": "^0.3.0", + "axios": "^1.6.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.1", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/react": "^18.2.42", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.13.2", + "@typescript-eslint/parser": "^6.13.2", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.32", + "prettier": "3.1.0", + "tailwindcss": "^3.3.6", + "typescript": "^5.3.3", + "vite": "^5.0.7" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 000000000..52c85e9e2 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,2383 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@stylexjs/stylex': + specifier: ^0.3.0 + version: 0.3.0 + axios: + specifier: ^1.6.2 + version: 1.6.2 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + react-router-dom: + specifier: ^6.20.1 + version: 6.20.1(react-dom@18.2.0)(react@18.2.0) + zod: + specifier: ^3.22.4 + version: 3.22.4 + +devDependencies: + '@types/react': + specifier: ^18.2.42 + version: 18.2.42 + '@types/react-dom': + specifier: ^18.2.17 + version: 18.2.17 + '@typescript-eslint/eslint-plugin': + specifier: ^6.13.2 + version: 6.13.2(@typescript-eslint/parser@6.13.2)(eslint@8.55.0)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: ^6.13.2 + version: 6.13.2(eslint@8.55.0)(typescript@5.3.3) + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.2.1(vite@5.0.7) + autoprefixer: + specifier: ^10.4.16 + version: 10.4.16(postcss@8.4.32) + eslint: + specifier: ^8.55.0 + version: 8.55.0 + eslint-plugin-react-hooks: + specifier: ^4.6.0 + version: 4.6.0(eslint@8.55.0) + eslint-plugin-react-refresh: + specifier: ^0.4.5 + version: 0.4.5(eslint@8.55.0) + postcss: + specifier: ^8.4.32 + version: 8.4.32 + prettier: + specifier: 3.1.0 + version: 3.1.0 + tailwindcss: + specifier: ^3.3.6 + version: 3.3.6 + typescript: + specifier: ^5.3.3 + version: 5.3.3 + vite: + specifier: ^5.0.7 + version: 5.0.7 + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@alloc/quick-lru@5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: true + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.20 + dev: true + + /@babel/code-frame@7.23.5: + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.23.4 + chalk: 2.4.2 + dev: true + + /@babel/compat-data@7.23.5: + resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.23.5: + resolution: {integrity: sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5) + '@babel/helpers': 7.23.5 + '@babel/parser': 7.23.5 + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.5 + '@babel/types': 7.23.5 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.23.5: + resolution: {integrity: sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.20 + jsesc: 2.5.2 + dev: true + + /@babel/helper-compilation-targets@7.22.15: + resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/helper-validator-option': 7.23.5 + browserslist: 4.22.2 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.5 + dev: true + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + dev: true + + /@babel/helper-module-imports@7.22.15: + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + dev: true + + /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + + /@babel/helper-plugin-utils@7.22.5: + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + dev: true + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + dev: true + + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.23.5: + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helpers@7.23.5: + resolution: {integrity: sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.5 + '@babel/types': 7.23.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight@7.23.4: + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser@7.23.5: + resolution: {integrity: sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.5 + dev: true + + /@babel/plugin-transform-react-jsx-self@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-react-jsx-source@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/template@7.22.15: + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/parser': 7.23.5 + '@babel/types': 7.23.5 + dev: true + + /@babel/traverse@7.23.5: + resolution: {integrity: sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.5 + '@babel/types': 7.23.5 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.23.5: + resolution: {integrity: sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + + /@esbuild/android-arm64@0.19.8: + resolution: {integrity: sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.19.8: + resolution: {integrity: sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.19.8: + resolution: {integrity: sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.19.8: + resolution: {integrity: sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.19.8: + resolution: {integrity: sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.19.8: + resolution: {integrity: sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.19.8: + resolution: {integrity: sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.19.8: + resolution: {integrity: sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.19.8: + resolution: {integrity: sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.19.8: + resolution: {integrity: sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.19.8: + resolution: {integrity: sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.19.8: + resolution: {integrity: sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.19.8: + resolution: {integrity: sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.19.8: + resolution: {integrity: sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.19.8: + resolution: {integrity: sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.19.8: + resolution: {integrity: sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.19.8: + resolution: {integrity: sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.19.8: + resolution: {integrity: sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.19.8: + resolution: {integrity: sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.19.8: + resolution: {integrity: sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.19.8: + resolution: {integrity: sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.19.8: + resolution: {integrity: sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.55.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.55.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.23.0 + ignore: 5.3.0 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.55.0: + resolution: {integrity: sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@humanwhocodes/config-array@0.11.13: + resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@2.0.1: + resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.20 + dev: true + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.20: + resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@remix-run/router@1.13.1: + resolution: {integrity: sha512-so+DHzZKsoOcoXrILB4rqDkMDy7NLMErRdOxvzvOKb507YINKUP4Di+shbTZDhSE/pBZ+vr7XGIpcOO0VLSA+Q==} + engines: {node: '>=14.0.0'} + dev: false + + /@rollup/rollup-android-arm-eabi@4.7.0: + resolution: {integrity: sha512-rGku10pL1StFlFvXX5pEv88KdGW6DHUghsxyP/aRYb9eH+74jTGJ3U0S/rtlsQ4yYq1Hcc7AMkoJOb1xu29Fxw==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.7.0: + resolution: {integrity: sha512-/EBw0cuJ/KVHiU2qyVYUhogXz7W2vXxBzeE9xtVIMC+RyitlY2vvaoysMUqASpkUtoNIHlnKTu/l7mXOPgnKOA==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.7.0: + resolution: {integrity: sha512-4VXG1bgvClJdbEYYjQ85RkOtwN8sqI3uCxH0HC5w9fKdqzRzgG39K7GAehATGS8jghA7zNoS5CjSKkDEqWmNZg==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.7.0: + resolution: {integrity: sha512-/ImhO+T/RWJ96hUbxiCn2yWI0/MeQZV/aeukQQfhxiSXuZJfyqtdHPUPrc84jxCfXTxbJLmg4q+GBETeb61aNw==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.7.0: + resolution: {integrity: sha512-zhye8POvTyUXlKbfPBVqoHy3t43gIgffY+7qBFqFxNqVtltQLtWeHNAbrMnXiLIfYmxcoL/feuLDote2tx+Qbg==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.7.0: + resolution: {integrity: sha512-RAdr3OJnUum6Vs83cQmKjxdTg31zJnLLTkjhcFt0auxM6jw00GD6IPFF42uasYPr/wGC6TRm7FsQiJyk0qIEfg==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.7.0: + resolution: {integrity: sha512-nhWwYsiJwZGq7SyR3afS3EekEOsEAlrNMpPC4ZDKn5ooYSEjDLe9W/xGvoIV8/F/+HNIY6jY8lIdXjjxfxopXw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.7.0: + resolution: {integrity: sha512-rlfy5RnQG1aop1BL/gjdH42M2geMUyVQqd52GJVirqYc787A/XVvl3kQ5NG/43KXgOgE9HXgCaEH05kzQ+hLoA==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.7.0: + resolution: {integrity: sha512-cCkoGlGWfBobdDtiiypxf79q6k3/iRVGu1HVLbD92gWV5WZbmuWJCgRM4x2N6i7ljGn1cGytPn9ZAfS8UwF6vg==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.7.0: + resolution: {integrity: sha512-R2oBf2p/Arc1m+tWmiWbpHBjEcJnHVnv6bsypu4tcKdrYTpDfl1UT9qTyfkIL1iiii5D4WHxUHCg5X0pzqmxFg==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.7.0: + resolution: {integrity: sha512-CPtgaQL1aaPc80m8SCVEoxFGHxKYIt3zQYC3AccL/SqqiWXblo3pgToHuBwR8eCP2Toa+X1WmTR/QKFMykws7g==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.7.0: + resolution: {integrity: sha512-pmioUlttNh9GXF5x2CzNa7Z8kmRTyhEzzAC+2WOOapjewMbl+3tGuAnxbwc5JyG8Jsz2+hf/QD/n5VjimOZ63g==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.7.0: + resolution: {integrity: sha512-SeZzC2QhhdBQUm3U0c8+c/P6UlRyBcLL2Xp5KX7z46WXZxzR8RJSIWL9wSUeBTgxog5LTPJuPj0WOT9lvrtP7Q==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@stylexjs/stylex@0.3.0: + resolution: {integrity: sha512-iO9ayGjZ54IXIkXOLoNKf/GWS/3EGtJaxdLxvy7nUa30eUw6qMVvEgRi58S7MT+e/FJub5xaCM/j0oDHlDgcIg==} + dependencies: + css-mediaquery: 0.1.2 + invariant: 2.2.4 + styleq: 0.1.3 + utility-types: 3.10.0 + dev: false + + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + dependencies: + '@babel/parser': 7.23.5 + '@babel/types': 7.23.5 + '@types/babel__generator': 7.6.7 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.4 + dev: true + + /@types/babel__generator@7.6.7: + resolution: {integrity: sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==} + dependencies: + '@babel/types': 7.23.5 + dev: true + + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.23.5 + '@babel/types': 7.23.5 + dev: true + + /@types/babel__traverse@7.20.4: + resolution: {integrity: sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==} + dependencies: + '@babel/types': 7.23.5 + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + + /@types/prop-types@15.7.11: + resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} + dev: true + + /@types/react-dom@18.2.17: + resolution: {integrity: sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==} + dependencies: + '@types/react': 18.2.42 + dev: true + + /@types/react@18.2.42: + resolution: {integrity: sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==} + dependencies: + '@types/prop-types': 15.7.11 + '@types/scheduler': 0.16.8 + csstype: 3.1.3 + dev: true + + /@types/scheduler@0.16.8: + resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} + dev: true + + /@types/semver@7.5.6: + resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} + dev: true + + /@typescript-eslint/eslint-plugin@6.13.2(@typescript-eslint/parser@6.13.2)(eslint@8.55.0)(typescript@5.3.3): + resolution: {integrity: sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.13.2(eslint@8.55.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.13.2 + '@typescript-eslint/type-utils': 6.13.2(eslint@8.55.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.13.2(eslint@8.55.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.13.2 + debug: 4.3.4 + eslint: 8.55.0 + graphemer: 1.4.0 + ignore: 5.3.0 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@6.13.2(eslint@8.55.0)(typescript@5.3.3): + resolution: {integrity: sha512-MUkcC+7Wt/QOGeVlM8aGGJZy1XV5YKjTpq9jK6r6/iLsGXhBVaGP5N0UYvFsu9BFlSpwY9kMretzdBH01rkRXg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.13.2 + '@typescript-eslint/types': 6.13.2 + '@typescript-eslint/typescript-estree': 6.13.2(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.13.2 + debug: 4.3.4 + eslint: 8.55.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@6.13.2: + resolution: {integrity: sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.13.2 + '@typescript-eslint/visitor-keys': 6.13.2 + dev: true + + /@typescript-eslint/type-utils@6.13.2(eslint@8.55.0)(typescript@5.3.3): + resolution: {integrity: sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.13.2(typescript@5.3.3) + '@typescript-eslint/utils': 6.13.2(eslint@8.55.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.55.0 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@6.13.2: + resolution: {integrity: sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + + /@typescript-eslint/typescript-estree@6.13.2(typescript@5.3.3): + resolution: {integrity: sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.13.2 + '@typescript-eslint/visitor-keys': 6.13.2 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@6.13.2(eslint@8.55.0)(typescript@5.3.3): + resolution: {integrity: sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 6.13.2 + '@typescript-eslint/types': 6.13.2 + '@typescript-eslint/typescript-estree': 6.13.2(typescript@5.3.3) + eslint: 8.55.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@6.13.2: + resolution: {integrity: sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.13.2 + eslint-visitor-keys: 3.4.3 + dev: true + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + + /@vitejs/plugin-react@4.2.1(vite@5.0.7): + resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/plugin-transform-react-jsx-self': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.5) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.0 + vite: 5.0.7 + transitivePeerDependencies: + - supports-color + dev: true + + /acorn-jsx@5.3.2(acorn@8.11.2): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.11.2 + dev: true + + /acorn@8.11.2: + resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + + /autoprefixer@10.4.16(postcss@8.4.32): + resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.22.2 + caniuse-lite: 1.0.30001566 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.32 + postcss-value-parser: 4.2.0 + dev: true + + /axios@1.6.2: + resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} + dependencies: + follow-redirects: 1.15.3 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browserslist@4.22.2: + resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001566 + electron-to-chromium: 1.4.609 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.22.2) + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + + /caniuse-lite@1.0.30001566: + resolution: {integrity: sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==} + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /css-mediaquery@0.1.2: + resolution: {integrity: sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==} + dev: false + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /electron-to-chromium@1.4.609: + resolution: {integrity: sha512-ihiCP7PJmjoGNuLpl7TjNA8pCQWu09vGyjlPYw1Rqww4gvNuCcmvl+44G+2QyJ6S2K4o+wbTS++Xz0YN8Q9ERw==} + dev: true + + /esbuild@0.19.8: + resolution: {integrity: sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.19.8 + '@esbuild/android-arm64': 0.19.8 + '@esbuild/android-x64': 0.19.8 + '@esbuild/darwin-arm64': 0.19.8 + '@esbuild/darwin-x64': 0.19.8 + '@esbuild/freebsd-arm64': 0.19.8 + '@esbuild/freebsd-x64': 0.19.8 + '@esbuild/linux-arm': 0.19.8 + '@esbuild/linux-arm64': 0.19.8 + '@esbuild/linux-ia32': 0.19.8 + '@esbuild/linux-loong64': 0.19.8 + '@esbuild/linux-mips64el': 0.19.8 + '@esbuild/linux-ppc64': 0.19.8 + '@esbuild/linux-riscv64': 0.19.8 + '@esbuild/linux-s390x': 0.19.8 + '@esbuild/linux-x64': 0.19.8 + '@esbuild/netbsd-x64': 0.19.8 + '@esbuild/openbsd-x64': 0.19.8 + '@esbuild/sunos-x64': 0.19.8 + '@esbuild/win32-arm64': 0.19.8 + '@esbuild/win32-ia32': 0.19.8 + '@esbuild/win32-x64': 0.19.8 + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-plugin-react-hooks@4.6.0(eslint@8.55.0): + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.55.0 + dev: true + + /eslint-plugin-react-refresh@0.4.5(eslint@8.55.0): + resolution: {integrity: sha512-D53FYKJa+fDmZMtriODxvhwrO+IOqrxoEo21gMA0sjHdU6dPVH4OhyFip9ypl8HOF5RV5KdTo+rBQLvnY2cO8w==} + peerDependencies: + eslint: '>=7' + dependencies: + eslint: 8.55.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.55.0: + resolution: {integrity: sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.55.0 + '@humanwhocodes/config-array': 0.11.13 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.23.0 + graphemer: 1.4.0 + ignore: 5.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.11.2 + acorn-jsx: 5.3.2(acorn@8.11.2) + eslint-visitor-keys: 3.4.3 + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.2.0 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.9 + keyv: 4.5.4 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + + /follow-redirects@1.15.3: + resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + + /fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globals@13.23.0: + resolution: {integrity: sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.0 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + + /ignore@5.3.0: + resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /jiti@1.21.0: + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + hasBin: true + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lilconfig@3.0.0: + resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} + engines: {node: '>=14'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: true + + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + + /postcss-import@15.1.0(postcss@8.4.32): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.32 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + dev: true + + /postcss-js@4.0.1(postcss@8.4.32): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.32 + dev: true + + /postcss-load-config@4.0.2(postcss@8.4.32): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.0.0 + postcss: 8.4.32 + yaml: 2.3.4 + dev: true + + /postcss-nested@6.0.1(postcss@8.4.32): + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.32 + postcss-selector-parser: 6.0.13 + dev: true + + /postcss-selector-parser@6.0.13: + resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@8.4.32: + resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier@3.1.0: + resolution: {integrity: sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + dev: false + + /react-refresh@0.14.0: + resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} + engines: {node: '>=0.10.0'} + dev: true + + /react-router-dom@6.20.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-npzfPWcxfQN35psS7rJgi/EW0Gx6EsNjfdJSAk73U/HqMEJZ2k/8puxfwHFgDQhBGmS3+sjnGbMdMSV45axPQw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@remix-run/router': 1.13.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-router: 6.20.1(react@18.2.0) + dev: false + + /react-router@6.20.1(react@18.2.0): + resolution: {integrity: sha512-ccvLrB4QeT5DlaxSFFYi/KR8UMQ4fcD8zBcR71Zp1kaYTC5oJKYAp1cbavzGrogwxca+ubjkd7XjFZKBW8CxPA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + dependencies: + '@remix-run/router': 1.13.1 + react: 18.2.0 + dev: false + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rollup@4.7.0: + resolution: {integrity: sha512-7Kw0dUP4BWH78zaZCqF1rPyQ8D5DSU6URG45v1dqS/faNsx9WXyess00uTOZxKr7oR/4TOjO1CPudT8L1UsEgw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.7.0 + '@rollup/rollup-android-arm64': 4.7.0 + '@rollup/rollup-darwin-arm64': 4.7.0 + '@rollup/rollup-darwin-x64': 4.7.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.7.0 + '@rollup/rollup-linux-arm64-gnu': 4.7.0 + '@rollup/rollup-linux-arm64-musl': 4.7.0 + '@rollup/rollup-linux-riscv64-gnu': 4.7.0 + '@rollup/rollup-linux-x64-gnu': 4.7.0 + '@rollup/rollup-linux-x64-musl': 4.7.0 + '@rollup/rollup-win32-arm64-msvc': 4.7.0 + '@rollup/rollup-win32-ia32-msvc': 4.7.0 + '@rollup/rollup-win32-x64-msvc': 4.7.0 + fsevents: 2.3.3 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /styleq@0.1.3: + resolution: {integrity: sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==} + dev: false + + /sucrase@3.34.0: + resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} + engines: {node: '>=8'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /tailwindcss@3.3.6: + resolution: {integrity: sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.5.3 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.0 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.32 + postcss-import: 15.1.0(postcss@8.4.32) + postcss-js: 4.0.1(postcss@8.4.32) + postcss-load-config: 4.0.2(postcss@8.4.32) + postcss-nested: 6.0.1(postcss@8.4.32) + postcss-selector-parser: 6.0.13 + resolve: 1.22.8 + sucrase: 3.34.0 + transitivePeerDependencies: + - ts-node + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /ts-api-utils@1.0.3(typescript@5.3.3): + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.3.3 + dev: true + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /update-browserslist-db@1.0.13(browserslist@4.22.2): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.22.2 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /utility-types@3.10.0: + resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==} + engines: {node: '>= 4'} + dev: false + + /vite@5.0.7: + resolution: {integrity: sha512-B4T4rJCDPihrQo2B+h1MbeGL/k/GMAHzhQ8S0LjQ142s6/+l3hHTT095ORvsshj4QCkoWu3Xtmob5mazvakaOw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.19.8 + postcss: 8.4.32 + rollup: 4.7.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true + + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: false diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs new file mode 100644 index 000000000..12a703d90 --- /dev/null +++ b/frontend/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/public/android-chrome-144x144.png b/frontend/public/android-chrome-144x144.png new file mode 100644 index 000000000..15d6a3553 Binary files /dev/null and b/frontend/public/android-chrome-144x144.png differ diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100644 index 000000000..1f5138a51 Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ diff --git a/frontend/public/browserconfig.xml b/frontend/public/browserconfig.xml new file mode 100644 index 000000000..28e88f1b3 --- /dev/null +++ b/frontend/public/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #2196f3 + + + diff --git a/frontend/public/digit18.png b/frontend/public/digit18.png new file mode 100644 index 000000000..57c2136b7 Binary files /dev/null and b/frontend/public/digit18.png differ diff --git a/frontend/public/enbarsskar.jpg b/frontend/public/enbarsskar.jpg new file mode 100644 index 000000000..2a9892329 Binary files /dev/null and b/frontend/public/enbarsskar.jpg differ diff --git a/frontend/public/favicon-16x16.png b/frontend/public/favicon-16x16.png new file mode 100644 index 000000000..9bd434c5f Binary files /dev/null and b/frontend/public/favicon-16x16.png differ diff --git a/frontend/public/favicon-32x32.png b/frontend/public/favicon-32x32.png new file mode 100644 index 000000000..40ebf2078 Binary files /dev/null and b/frontend/public/favicon-32x32.png differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 000000000..3e0f8f7a8 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/humans.txt b/frontend/public/humans.txt new file mode 100644 index 000000000..52edaa217 --- /dev/null +++ b/frontend/public/humans.txt @@ -0,0 +1 @@ +https://gamma.chalmers.it/about \ No newline at end of file diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 000000000..b9a17dfc8 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + Gamma + + + + + +
+ + diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 000000000..9ce961552 --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,16 @@ +{ + "short_name": "Gamma", + "name": "Gamma - IT-account", + "icons": [ + { + "src": "/android-icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "density": "3.0" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#2196f3", + "background_color": "#ffffff" +} diff --git a/frontend/public/mstile-150x150.png b/frontend/public/mstile-150x150.png new file mode 100644 index 000000000..2e1fd33d1 Binary files /dev/null and b/frontend/public/mstile-150x150.png differ diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 000000000..77470cb39 --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/frontend/public/safari-pinned-tab.svg b/frontend/public/safari-pinned-tab.svg new file mode 100644 index 000000000..cb647c57e --- /dev/null +++ b/frontend/public/safari-pinned-tab.svg @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/site.webmanifest b/frontend/public/site.webmanifest new file mode 100644 index 000000000..c83116326 --- /dev/null +++ b/frontend/public/site.webmanifest @@ -0,0 +1,14 @@ +{ + "name": "Gamma - IT-account", + "short_name": "Gamma", + "icons": [ + { + "src": "/android-chrome-144x144.png", + "sizes": "144x144", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/frontend/public/useragreement-en.md b/frontend/public/useragreement-en.md new file mode 100644 index 000000000..505f42136 --- /dev/null +++ b/frontend/public/useragreement-en.md @@ -0,0 +1,17 @@ +### User agreement + +“IT" refers to the organization with the legal name "Teknologsektionen Informationsteknik" and org. number 857209-9524 based in Sweden. + +This agreement refers to IT:s user account system Gamma. + +IT will collect and manage name, nickname, year of admission, CID, phone number, e-mail, added websites, commitées and societies you are and have been part of, prefered language and your profile picture. The data is managed in order to authenticate you to IT and third party services. Furthermore, the data is used to create a profile that connects to IT services as well as any third party services you approve. The data is also used if we need to contact you for matters of special interest to you. The data is also handled for statistical purposes. + +The data is saved until you choose to delete your profile or the IT section decides to delete your data. You will be notified of this at least 30 days prior. + +Name, Chalmers ID, nickname and year of admission is shared with all users of Gamma. Data may also be shared with third party services with the user’s approval. + +You have the right to withdraw your consent to the handling and request to have all data that IT has about you. +If you have any questions or want to exercise your rights you can contact: +* The board of the IT student division: styrit@chalmers.it +* The data audit group at the IT student division: dpo@chalmers.it + diff --git a/frontend/public/useragreement-sv.md b/frontend/public/useragreement-sv.md new file mode 100644 index 000000000..bc3aa92e3 --- /dev/null +++ b/frontend/public/useragreement-sv.md @@ -0,0 +1,17 @@ +### Användaravtal + +“IT” refererar till Teknologsektionen Informationsteknik, Chalmers Studentkår med org. Nummer 857209-9524. + +Detta avtal rör datahantering i IT:s kontotjänst Gamma. + +IT kommer att samla in och hantera namn, Chalmers-id, smeknamn, antagningsår på Informationsteknik, e-mail, mobiltelefonnummer, hemsidor som du lägger in, vilka organ som du är med i på sektionen, vilket språk du föredrar samt bild kopplad till dig. + +Datan hanteras i syfte att autentisera dig mot IT:s och tredjeparts-tjänster. Datan används för att skapa en profil som kan användas på IT:s tjänster samt även de tredjepartstjänster du godkänner. Datan används även ifall vi behöver kontakta dig vid ärende av särskilt intresse för dig. Datan hanteras även i statistiska syften. + +Datan sparas tills du väljer att ta bort din profil eller att IT sektionen väljer att radera din data. Du kommer att meddelas om detta 30 dagar innan. + +Namn, Chalmers-id, smeknamn och antagningsår på Informationsteknik delas med samtliga användare av gamma. All data kan även delas med tredjepartstjänster vid användarens samtycke. + +Du har rätt att dra tillbaka ditt samtycke till hanteringen, begära att få all data som IT har om dig samt att klaga till Datainspektionen vid missnöje. +* IT:s dataskyddsombud går att nå genom dpo@chalmers.it. +* Ytterst ansvarig för hanteringen går att nå genom ordf@chalmers.it. diff --git a/frontend/src/client/gamma/activation-codes.ts b/frontend/src/client/gamma/activation-codes.ts new file mode 100644 index 000000000..9ac54912f --- /dev/null +++ b/frontend/src/client/gamma/activation-codes.ts @@ -0,0 +1,26 @@ +import { AxiosInstance } from "axios"; +import * as z from "zod"; + +const getActivationCodesValidation = z.array( + z + .object({ + cid: z.string(), + token: z.string(), + createdAt: z.string(), + }) + .strict(), +); + +export class ActivationCodes { + private client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } + + public async getActivationCodes() { + return getActivationCodesValidation.parse( + (await this.client.get("/admin/user-activation")).data, + ); + } +} diff --git a/frontend/src/client/gamma/admins.ts b/frontend/src/client/gamma/admins.ts new file mode 100644 index 000000000..acc5d2dab --- /dev/null +++ b/frontend/src/client/gamma/admins.ts @@ -0,0 +1,22 @@ +import { AxiosInstance } from "axios"; +import * as z from "zod"; + +const getAdminsValidation = z.array(z.string()); + +export class Admins { + private client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } + + public async getAdmins() { + return getAdminsValidation.parse( + (await this.client.get("/admin/users/admins")).data, + ); + } + + public async setAdmin(userId: string, isAdmin: boolean) { + return this.client.put("/admin/users/admins/" + userId, { isAdmin }); + } +} diff --git a/frontend/src/client/gamma/allow-list.ts b/frontend/src/client/gamma/allow-list.ts new file mode 100644 index 000000000..defea586e --- /dev/null +++ b/frontend/src/client/gamma/allow-list.ts @@ -0,0 +1,26 @@ +import { AxiosInstance } from "axios"; +import * as z from "zod"; + +const getAllowListValidation = z.array(z.string()); + +export class AllowList { + private client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } + + public async getAllowList() { + return getAllowListValidation.parse( + (await this.client.get("/admin/users/allow-list")).data, + ); + } + + public async allow(cid: string) { + return this.client.post("/admin/users/allow-list", {cid}); + } + + public async remove(cid: string) { + return this.client.delete("/admin/users/allow-list/" + cid); + } +} diff --git a/frontend/src/client/gamma/api-keys.ts b/frontend/src/client/gamma/api-keys.ts new file mode 100644 index 000000000..68e7de020 --- /dev/null +++ b/frontend/src/client/gamma/api-keys.ts @@ -0,0 +1,67 @@ +import { AxiosInstance } from "axios"; +import * as z from "zod"; + +const getApiTypesValidation = z.array(z.string()); + +const getApiKeysValidation = z.array( + z + .object({ + id: z.string(), + svDescription: z.string(), + enDescription: z.string(), + keyType: z.string(), + prettyName: z.string(), + }) + .strict(), +); + +const getApiKeyValidation = z + .object({ + id: z.string(), + svDescription: z.string(), + enDescription: z.string(), + keyType: z.string(), + prettyName: z.string(), + }) + .strict(); + +type CreateApiKey = { + svDescription: string; + enDescription: string; + prettyName: string; + keyType: string; +}; + +export class ApiKeys { + private client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } + + public async getApiKeys() { + return getApiKeysValidation.parse( + (await this.client.get("/admin/api-keys")).data, + ); + } + + public async getApiTypes() { + return getApiTypesValidation.parse( + (await this.client.get("/admin/api-keys/types")).data, + ); + } + + public async getApiKey(id: string) { + return getApiKeyValidation.parse( + (await this.client.get("/admin/api-keys/" + id)).data, + ); + } + + public async deleteApiKey(id: string) { + return this.client.delete("/admin/api-keys/" + id); + } + + public async createApiKey(data: CreateApiKey) { + return this.client.post("/admin/api-keys", data); + } +} diff --git a/frontend/src/client/gamma/client-authorities.ts b/frontend/src/client/gamma/client-authorities.ts new file mode 100644 index 000000000..1b36354c2 --- /dev/null +++ b/frontend/src/client/gamma/client-authorities.ts @@ -0,0 +1,55 @@ +import { AxiosInstance } from "axios"; +import * as z from "zod"; + +const getAuthoritiesValidation = z.array( + z + .object({ + clientUid: z.string(), + authorityName: z.string(), + superGroups: z.array( + z + .object({ + id: z.string(), + name: z.string(), + prettyName: z.string(), + svDescription: z.string(), + enDescription: z.string(), + type: z.string(), + version: z.number(), + }) + .strict(), + ), + users: z.array(z.any()), + posts: z.array(z.any()), + }) + .strict(), +); + +export class ClientAuthorities { + private client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } + + public async getAuthorities(clientUid: string) { + return getAuthoritiesValidation.parse( + (await this.client.get("/admin/client/" + clientUid + "/authority")).data, + ); + } + + public async createAuthority(clientUid: string, authorityName: string) { + return this.client.post("/admin/client/authority", { + clientUid, + authorityName, + }); + } + + public async addSuperGroupToAuthority(data: { + clientUid: string; + authorityName: string; + superGroupId: string; + }) { + return this.client.post("/admin/client/authority/super-group", data); + } +} diff --git a/frontend/src/client/gamma/clients.ts b/frontend/src/client/gamma/clients.ts new file mode 100644 index 000000000..96a312cda --- /dev/null +++ b/frontend/src/client/gamma/clients.ts @@ -0,0 +1,99 @@ +import { AxiosInstance } from "axios"; +import * as z from "zod"; + +const getClientsValidation = z.array( + z + .object({ + clientId: z.string(), + clientUid: z.string(), + svDescription: z.string(), + enDescription: z.string(), + hasApiKey: z.boolean(), + prettyName: z.string(), + webServerRedirectUrl: z.string(), + restriction: z + .object({ + superGroups: z.array( + z.object({ + id: z.string(), + name: z.string(), + prettyName: z.string(), + svDescription: z.string(), + enDescription: z.string(), + type: z.string(), + version: z.number(), + }), + ), + }) + .strict() + .nullable(), + }) + .strict(), +); + +const getClientValidation = z + .object({ + clientId: z.string(), + clientUid: z.string(), + svDescription: z.string(), + enDescription: z.string(), + hasApiKey: z.boolean(), + prettyName: z.string(), + webServerRedirectUrl: z.string(), + restriction: z + .object({ + superGroups: z.array( + z.object({ + id: z.string(), + name: z.string(), + prettyName: z.string(), + svDescription: z.string(), + enDescription: z.string(), + type: z.string(), + version: z.number(), + }), + ), + }) + .strict() + .nullable(), + }) + .strict(); + +type CreateClient = { + webServerRedirectUrl: string; + svDescription: string; + enDescription: string; + generateApiKey: boolean; + emailScope: boolean; + restriction: { + superGroupIds: string[]; + } | null; +}; + +export class Clients { + private client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } + + public async getClients() { + return getClientsValidation.parse( + (await this.client.get("/admin/client")).data, + ); + } + + public async getClient(id: string) { + return getClientValidation.parse( + (await this.client.get("/admin/client/" + id)).data, + ); + } + + public async createClient(data: CreateClient) { + return this.client.post("/admin/client", data); + } + + public async deleteClient(clientUid: string) { + return this.client.delete("/admin/client/" + clientUid); + } +} diff --git a/frontend/src/client/gamma/create-account.ts b/frontend/src/client/gamma/create-account.ts new file mode 100644 index 000000000..ea263b677 --- /dev/null +++ b/frontend/src/client/gamma/create-account.ts @@ -0,0 +1,31 @@ +import { AxiosInstance } from "axios"; + +type CreateAccountInput = { + cid: string; + password: string; + confirmPassword: string; + nick: string; + firstName: string; + lastName: string; + email: string; + acceptanceYear: number; + userAgreement: boolean; +}; + +export class CreateAccount { + private client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } + + public enterCid(cid: string) { + return this.client.post("/allow-list/activate-cid", { + cid, + }); + } + + public createAccount(account: CreateAccountInput) { + return this.client.post("/users/create", account); + } +} diff --git a/frontend/src/client/gamma/gdpr.ts b/frontend/src/client/gamma/gdpr.ts new file mode 100644 index 000000000..2cee637b9 --- /dev/null +++ b/frontend/src/client/gamma/gdpr.ts @@ -0,0 +1,22 @@ +import { AxiosInstance } from "axios"; +import * as z from "zod"; + +const getGdprTrainedValidation = z.array(z.string()); + +export class Gdpr { + private client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } + + public async getGdprTrained() { + return getGdprTrainedValidation.parse( + (await this.client.get("/admin/gdpr")).data, + ); + } + + public async setGdprTrained(userId: string, gdprTrained: boolean) { + return this.client.put("/admin/gdpr/" + userId, { gdpr: gdprTrained }); + } +} diff --git a/frontend/src/client/gamma/groups.ts b/frontend/src/client/gamma/groups.ts new file mode 100644 index 000000000..df9893df3 --- /dev/null +++ b/frontend/src/client/gamma/groups.ts @@ -0,0 +1,101 @@ +import { AxiosInstance } from "axios"; +import * as z from "zod"; + +const getGroupsValidation = z.array( + z + .object({ + id: z.string(), + name: z.string(), + prettyName: z.string(), + superGroup: z + .object({ + id: z.string(), + version: z.number(), + type: z.string(), + name: z.string(), + prettyName: z.string(), + svDescription: z.string(), + enDescription: z.string(), + }) + .strict(), + }) + .strict(), +); + +const getGroupValidation = z + .object({ + id: z.string(), + version: z.number(), + name: z.string(), + prettyName: z.string(), + groupMembers: z.array( + z + .object({ + unofficialPostName: z.string(), + user: z + .object({ + id: z.string(), + firstName: z.string(), + lastName: z.string(), + nick: z.string(), + cid: z.string(), + acceptanceYear: z.number(), + }) + .strict(), + post: z + .object({ + id: z.string(), + svName: z.string(), + enName: z.string(), + emailPrefix: z.string(), + version: z.number(), + }) + .strict(), + }) + .strict(), + ), + superGroup: z + .object({ + id: z.string(), + version: z.number(), + type: z.string(), + name: z.string(), + prettyName: z.string(), + svDescription: z.string(), + enDescription: z.string(), + }) + .strict(), + }) + .strict(); + +type CreateGroup = { + superGroup: string; + name: string; + prettyName: string; +}; + +export class Groups { + private client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } + + public async getGroups() { + return getGroupsValidation.parse((await this.client.get("/groups")).data); + } + + public async getGroup(id: string) { + return getGroupValidation.parse( + (await this.client.get("/groups/" + id)).data, + ); + } + + public async createGroup(data: CreateGroup) { + return this.client.post("/admin/groups", data); + } + + public async deleteGroup(id: string) { + return this.client.delete("/admin/groups/" + id); + } +} diff --git a/frontend/src/client/gamma/index.ts b/frontend/src/client/gamma/index.ts new file mode 100644 index 000000000..c5309a959 --- /dev/null +++ b/frontend/src/client/gamma/index.ts @@ -0,0 +1,65 @@ +import { Users } from "./users"; +import axios, { AxiosInstance } from "axios"; +import { Groups } from "./groups"; +import { SuperGroups } from "./super-groups"; +import { Types } from "./types"; +import { Clients } from "./clients"; +import { Posts } from "./posts"; +import { AllowList } from "./allow-list"; +import { ActivationCodes } from "./activation-codes"; +import { CreateAccount } from "./create-account"; +import { Admins } from "./admins"; +import { Gdpr } from "./gdpr"; +import { ApiKeys } from "./api-keys"; +import { ClientAuthorities } from "./client-authorities"; + +export class GammaClient { + private client: AxiosInstance = axios.create({ + baseURL: "/api/internal", + }); + + public readonly users: Users; + public readonly groups: Groups; + public readonly superGroups: SuperGroups; + public readonly types: Types; + public readonly clients: Clients; + public readonly posts: Posts; + public readonly allowList: AllowList; + public readonly activationCodes: ActivationCodes; + public readonly createAccount: CreateAccount; + public readonly admins: Admins; + public readonly gdpr: Gdpr; + public readonly apiKeys: ApiKeys; + public readonly clientAuthorities: ClientAuthorities; + + constructor() { + this.client.interceptors.response.use( + (response) => response, + (error) => { + if (error.response.status === 401) { + window.location.href = "http://gamma:8081/api/login"; + } + }, + ); + + this.users = new Users(this.client); + this.groups = new Groups(this.client); + this.superGroups = new SuperGroups(this.client); + this.types = new Types(this.client); + this.clients = new Clients(this.client); + this.posts = new Posts(this.client); + this.allowList = new AllowList(this.client); + this.activationCodes = new ActivationCodes(this.client); + this.createAccount = new CreateAccount(this.client); + this.admins = new Admins(this.client); + this.gdpr = new Gdpr(this.client); + this.apiKeys = new ApiKeys(this.client); + this.clientAuthorities = new ClientAuthorities(this.client); + } + + private static gammaClientInstance = new GammaClient(); + + public static instance() { + return this.gammaClientInstance; + } +} diff --git a/frontend/src/client/gamma/posts.ts b/frontend/src/client/gamma/posts.ts new file mode 100644 index 000000000..e779e37c4 --- /dev/null +++ b/frontend/src/client/gamma/posts.ts @@ -0,0 +1,56 @@ +import { AxiosInstance } from "axios"; +import * as z from "zod"; + +const getPostsValidation = z.array( + z + .object({ + id: z.string(), + emailPrefix: z.string(), + svName: z.string(), + enName: z.string(), + version: z.number(), + }) + .strict(), +); + +const getPostValidation = z + .object({ + id: z.string(), + svName: z.string(), + enName: z.string(), + emailPrefix: z.string(), + version: z.number(), + }) + .strict(); + +type CreatePost = { + svName: string; + enName: string; + emailPrefix: string; +}; + +export class Posts { + private client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } + + public async getPosts() { + return getPostsValidation.parse((await this.client.get("/posts")).data); + } + + public async getPost(id: string) { + return getPostValidation.parse( + (await this.client.get("/posts/" + id)).data, + ); + } + + public async createPost(data: CreatePost) { + return this.client.post("/admin/posts", data); + } + + public async deletePost(id: string) { + return this.client.delete("/admin/posts/" + id); + } +} diff --git a/frontend/src/client/gamma/super-groups.ts b/frontend/src/client/gamma/super-groups.ts new file mode 100644 index 000000000..1b7f0593a --- /dev/null +++ b/frontend/src/client/gamma/super-groups.ts @@ -0,0 +1,64 @@ +import { AxiosInstance } from "axios"; +import * as z from "zod"; + +const getSuperGroupsValidation = z.array( + z + .object({ + id: z.string(), + name: z.string(), + prettyName: z.string(), + svDescription: z.string(), + enDescription: z.string(), + type: z.string(), + version: z.number(), + }) + .strict(), +); + +const getSuperGroupValidation = z + .object({ + id: z.string(), + name: z.string(), + prettyName: z.string(), + svDescription: z.string(), + enDescription: z.string(), + type: z.string(), + version: z.number(), + }) + .strict(); + +type CreateSuperGroup = { + name: string; + prettyName: string; + svDescription: string; + enDescription: string; + type: string; +}; + +export class SuperGroups { + private client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } + + public async getSuperGroups() { + return getSuperGroupsValidation.parse( + (await this.client.get("/super-groups")).data, + ); + } + + public async getSuperGroup(id: string) { + return getSuperGroupValidation.parse( + (await this.client.get("/super-groups/" + id)).data, + ); + } + + public async createSuperGroup(data: CreateSuperGroup) { + return this.client.post("/admin/super-groups", data); + } + + public async deleteSuperGroup(id: string) { + return this.client.delete("/admin/super-groups/" + id); + } +} diff --git a/frontend/src/client/gamma/types.ts b/frontend/src/client/gamma/types.ts new file mode 100644 index 000000000..e791dd627 --- /dev/null +++ b/frontend/src/client/gamma/types.ts @@ -0,0 +1,26 @@ +import { AxiosInstance } from "axios"; +import * as z from "zod"; + +const getTypesValidation = z.array(z.string()); + +export class Types { + private client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } + + public async getTypes() { + return getTypesValidation.parse( + (await this.client.get("/admin/supergrouptype")).data, + ); + } + + public async addType(type: string) { + return this.client.post("/admin/supergrouptype", { type }); + } + + public async deleteType(type: string) { + return this.client.delete("/admin/supergrouptype/" + type); + } +} diff --git a/frontend/src/client/gamma/users.ts b/frontend/src/client/gamma/users.ts new file mode 100644 index 000000000..4c228bd33 --- /dev/null +++ b/frontend/src/client/gamma/users.ts @@ -0,0 +1,134 @@ +import * as z from "zod"; +import { AxiosInstance } from "axios"; + +const getMeValidation = z + .object({ + nick: z.string(), + firstName: z.string(), + lastName: z.string(), + cid: z.string(), + email: z.string(), + id: z.string(), + acceptanceYear: z.number(), + userAgreement: z.boolean(), + language: z.enum(["EN", "SV"]), + isAdmin: z.boolean(), + groups: z.array( + z + .object({ + group: z + .object({ + id: z.string(), + name: z.string(), + prettyName: z.string(), + superGroup: z + .object({ + id: z.string(), + version: z.number(), + prettyName: z.string(), + name: z.string(), + svDescription: z.string(), + enDescription: z.string(), + type: z.string(), + }) + .strict(), + }) + .strict(), + post: z + .object({ + id: z.string(), + svName: z.string(), + enName: z.string(), + emailPrefix: z.string(), + version: z.number(), + }) + .strict(), + unofficialPostName: z.string(), + }) + .strict(), + ), + }) + .strict(); + +const getUsersValidation = z.array( + z + .object({ + cid: z.string(), + nick: z.string(), + firstName: z.string(), + lastName: z.string(), + id: z.string(), + acceptanceYear: z.number(), + }) + .strict(), +); + +const getUserValidation = z + .object({ + user: z + .object({ + cid: z.string(), + nick: z.string(), + firstName: z.string(), + lastName: z.string(), + id: z.string(), + acceptanceYear: z.number(), + }) + .strict(), + groups: z.array( + z + .object({ + group: z + .object({ + id: z.string(), + name: z.string(), + prettyName: z.string(), + superGroup: z + .object({ + id: z.string(), + version: z.number(), + prettyName: z.string(), + name: z.string(), + svDescription: z.string(), + enDescription: z.string(), + type: z.string(), + }) + .strict(), + }) + .strict(), + post: z + .object({ + id: z.string(), + svName: z.string(), + enName: z.string(), + emailPrefix: z.string(), + version: z.number(), + }) + .strict(), + }) + .strict(), + ), + }) + .strict(); + +export class Users { + private client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } + + public async getMe() { + return getMeValidation.parse((await this.client.get("/users/me")).data); + } + + public async getUsers() { + return getUsersValidation.parse((await this.client.get("/users")).data); + } + + public async getUser(id: string) { + return getUserValidation.parse( + (await this.client.get("/users/" + id)).data, + ); + } +} diff --git a/frontend/src/client/readme.md b/frontend/src/client/readme.md new file mode 100644 index 000000000..fbcced549 --- /dev/null +++ b/frontend/src/client/readme.md @@ -0,0 +1,3 @@ +Only use `Index` from `index.ts`, since it sets up everything. +Note that this client connects to `gamma.chalmers.it/api/internal/`, and thus cannot be used outside of this repository +since `/internal` is restricted to the main gamma website. \ No newline at end of file diff --git a/frontend/src/main.css b/frontend/src/main.css new file mode 100644 index 000000000..997827696 --- /dev/null +++ b/frontend/src/main.css @@ -0,0 +1,9 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + font-family: system-ui, sans-serif; + } +} \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 000000000..e11578346 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./main.css"; + +import { RouterProvider } from "react-router-dom"; +import { router } from "./router"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/frontend/src/pages/about/index.tsx b/frontend/src/pages/about/index.tsx new file mode 100644 index 000000000..6dc4822b5 --- /dev/null +++ b/frontend/src/pages/about/index.tsx @@ -0,0 +1,3 @@ +export const AboutPage = () => { + return
Made by Portals
; +}; diff --git a/frontend/src/pages/activation-codes/index.tsx b/frontend/src/pages/activation-codes/index.tsx new file mode 100644 index 000000000..d3b883696 --- /dev/null +++ b/frontend/src/pages/activation-codes/index.tsx @@ -0,0 +1,34 @@ +import { useActivationCodesLoaderData } from "./loader"; + +const tableCellStyle = "border border-slate-300 p-1"; + +export const ActivationCodesPage = () => { + const activationCodes = useActivationCodesLoaderData(); + + console.log(activationCodes); + + return ( + + + + + + + + + + {activationCodes.map((activationCode) => ( + + + + + + ))} + +
CidTokenCreated at
{activationCode.cid}{activationCode.token}{activationCode.createdAt}
+ ); +}; diff --git a/frontend/src/pages/activation-codes/loader.ts b/frontend/src/pages/activation-codes/loader.ts new file mode 100644 index 000000000..872883949 --- /dev/null +++ b/frontend/src/pages/activation-codes/loader.ts @@ -0,0 +1,14 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type ActivationCodesLoaderReturn = Awaited< + ReturnType +>; + +export const useActivationCodesLoaderData = (): ActivationCodesLoaderReturn => { + return useLoaderData() as ActivationCodesLoaderReturn; +}; + +export const activationCodesLoader = async () => { + return await GammaClient.instance().activationCodes.getActivationCodes(); +}; diff --git a/frontend/src/pages/admins/index.tsx b/frontend/src/pages/admins/index.tsx new file mode 100644 index 000000000..7011040db --- /dev/null +++ b/frontend/src/pages/admins/index.tsx @@ -0,0 +1,57 @@ +import { useAdminsLoaderData } from "./loader"; +import { Link, useRevalidator } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +const tableCellStyle = "border border-slate-300 p-1"; + +export const AdminsPage = () => { + const { admins, users } = useAdminsLoaderData(); + const revalidator = useRevalidator(); + + const setAdmin = + (userId: string) => (e: React.ChangeEvent) => { + GammaClient.instance() + .admins.setAdmin(userId, e.target.checked) + .then(() => { + revalidator.revalidate(); + }); + }; + + return ( + + + + + + + + + + + + + {users.map((user) => ( + + + + + + + + ))} + +
Set admins
First nameNickLast nameIs AdminDetails
{user.firstName}{user.nick}{user.lastName} + + + Details +
+ ); +}; diff --git a/frontend/src/pages/admins/loader.ts b/frontend/src/pages/admins/loader.ts new file mode 100644 index 000000000..b069d7eaf --- /dev/null +++ b/frontend/src/pages/admins/loader.ts @@ -0,0 +1,15 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type AdminsLoaderReturn = Awaited>; + +export const useAdminsLoaderData = (): AdminsLoaderReturn => { + return useLoaderData() as AdminsLoaderReturn; +}; + +export const adminsLoader = async () => { + return { + admins: await GammaClient.instance().admins.getAdmins(), + users: await GammaClient.instance().users.getUsers(), + }; +}; diff --git a/frontend/src/pages/allow-list/index.tsx b/frontend/src/pages/allow-list/index.tsx new file mode 100644 index 000000000..09168d72b --- /dev/null +++ b/frontend/src/pages/allow-list/index.tsx @@ -0,0 +1,72 @@ +import { useAllowListLoaderData } from "./loader"; +import * as z from "zod"; +import { useRevalidator } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +const tableCellStyle = "border border-slate-300 p-1"; + +const allowValidation = z + .object({ + cid: z.string(), + }) + .strict(); + +export const AllowListPage = () => { + const allowList = useAllowListLoaderData(); + const revalidator = useRevalidator(); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const form = Object.fromEntries(new FormData(e.currentTarget)); + + const validatedForm = allowValidation.parse(form); + GammaClient.instance() + .allowList.allow(validatedForm.cid) + .then(() => revalidator.revalidate()); + }; + + const removeAllowedCid = (cid: string) => () => { + GammaClient.instance() + .allowList.remove(cid) + .then(() => revalidator.revalidate()); + }; + + return ( +
+ + + + + + + + + {allowList.map((allowed) => ( + + + + + ))} + +
Allowed cidRemove
{allowed} + +
+ +
+
+ + + +
+
+
+ ); +}; diff --git a/frontend/src/pages/allow-list/loader.ts b/frontend/src/pages/allow-list/loader.ts new file mode 100644 index 000000000..74b3e3ed4 --- /dev/null +++ b/frontend/src/pages/allow-list/loader.ts @@ -0,0 +1,12 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type AllowListLoaderReturn = Awaited>; + +export const useAllowListLoaderData = (): AllowListLoaderReturn => { + return useLoaderData() as AllowListLoaderReturn; +}; + +export const allowListLoader = async () => { + return await GammaClient.instance().allowList.getAllowList(); +}; diff --git a/frontend/src/pages/api-keys/index.tsx b/frontend/src/pages/api-keys/index.tsx new file mode 100644 index 000000000..9fa8b15d1 --- /dev/null +++ b/frontend/src/pages/api-keys/index.tsx @@ -0,0 +1,49 @@ +import { useApiKeysLoaderData } from "./loader"; +import { Link, useRevalidator } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +const tableCellStyle = "border border-slate-300 p-1"; + +export const ApiKeysPage = () => { + const { apiKeys } = useApiKeysLoaderData(); + const revalidator = useRevalidator(); + + const deleteApiKey = (apiKeyId: string) => () => { + GammaClient.instance() + .apiKeys.deleteApiKey(apiKeyId) + .then(() => { + revalidator.revalidate(); + }); + }; + + return ( + + + + + + + + + + + {apiKeys.map((apiKey) => ( + + + + + + + ))} + +
NameTypeDetailsDelete
{apiKey.prettyName}{apiKey.keyType} + Details + + +
+ ); +}; diff --git a/frontend/src/pages/api-keys/loader.ts b/frontend/src/pages/api-keys/loader.ts new file mode 100644 index 000000000..c4634e2a8 --- /dev/null +++ b/frontend/src/pages/api-keys/loader.ts @@ -0,0 +1,15 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type ApiKeysLoaderReturn = Awaited>; + +export const useApiKeysLoaderData = (): ApiKeysLoaderReturn => { + return useLoaderData() as ApiKeysLoaderReturn; +}; + +export const apiKeysLoader = async () => { + return { + apiKeys: await GammaClient.instance().apiKeys.getApiKeys(), + types: await GammaClient.instance().apiKeys.getApiTypes(), + }; +}; diff --git a/frontend/src/pages/clients/index.tsx b/frontend/src/pages/clients/index.tsx new file mode 100644 index 000000000..edf002dac --- /dev/null +++ b/frontend/src/pages/clients/index.tsx @@ -0,0 +1,52 @@ +import { useClientsLoaderData } from "./loader"; +import { Link, useRevalidator } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +const tableCellStyle = "border border-slate-300 p-1"; + +export const ClientsPage = () => { + const clients = useClientsLoaderData(); + const revalidator = useRevalidator(); + + const deleteClient = (clientUid: string) => () => { + GammaClient.instance() + .clients.deleteClient(clientUid) + .then(() => revalidator.revalidate()); + }; + + return ( +
+ Create new client + + + + + + + + + + + {clients.map((client) => ( + + + + + + + ))} + +
NameHas restrictionsDetailsDelete
{client.prettyName} + {client.restriction === null ? "No" : "Yes"} + + Details + + +
+
+ ); +}; diff --git a/frontend/src/pages/clients/loader.ts b/frontend/src/pages/clients/loader.ts new file mode 100644 index 000000000..f47562f5a --- /dev/null +++ b/frontend/src/pages/clients/loader.ts @@ -0,0 +1,12 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type ClientsLoaderReturn = Awaited>; + +export const useClientsLoaderData = (): ClientsLoaderReturn => { + return useLoaderData() as ClientsLoaderReturn; +}; + +export const clientsLoader = async () => { + return await GammaClient.instance().clients.getClients(); +}; diff --git a/frontend/src/pages/create-account/email-sent.tsx b/frontend/src/pages/create-account/email-sent.tsx new file mode 100644 index 000000000..bd81b3cf7 --- /dev/null +++ b/frontend/src/pages/create-account/email-sent.tsx @@ -0,0 +1,16 @@ +import { Link } from "react-router-dom"; + +export const EmailSentPage = () => { + return ( +
+

+ If you have not received an email within a few minutes, you may have + entered the wrong cid. If you're sure that you have written the correct + cid and you still haven't received an email please contact digIT at + digit@chalmers.it +

+ I have not received a code + I have received a code +
+ ); +}; diff --git a/frontend/src/pages/create-account/enter-cid.tsx b/frontend/src/pages/create-account/enter-cid.tsx new file mode 100644 index 000000000..2b401cc53 --- /dev/null +++ b/frontend/src/pages/create-account/enter-cid.tsx @@ -0,0 +1,34 @@ +import * as z from "zod"; +import { GammaClient } from "../../client/gamma"; +import { useNavigate } from "react-router-dom"; + +const enterCidValidation = z + .object({ + cid: z.string(), + }) + .strict(); + +export const EnterCidPage = () => { + const navigate = useNavigate(); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const form = Object.fromEntries(new FormData(e.currentTarget)); + + const validatedForm = enterCidValidation.parse(form); + GammaClient.instance() + .createAccount.enterCid(validatedForm.cid) + .then(() => { + navigate("/create-account/email-sent"); + }); + }; + + return ( +
+
+ + +
+
+ ); +}; diff --git a/frontend/src/pages/create-account/input-new-account.tsx b/frontend/src/pages/create-account/input-new-account.tsx new file mode 100644 index 000000000..7cf8d6fec --- /dev/null +++ b/frontend/src/pages/create-account/input-new-account.tsx @@ -0,0 +1,103 @@ +import { GammaClient } from "../../client/gamma"; +import * as z from "zod"; + +const inputNewAccountValidation = z + .object({ + cid: z.string(), + code: z.string(), + password: z.string(), + confirmPassword: z.string(), + nick: z.string(), + firstName: z.string(), + lastName: z.string(), + email: z.string(), + acceptanceYear: z.coerce.number(), + userAgreement: z.coerce.boolean().refine((value) => value), + language: z.enum(["SV", "EN"]), + }) + .strict() + .refine(({ password, confirmPassword }) => password === confirmPassword); + +export const InputNewAccountPage = () => { + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const form = Object.fromEntries(new FormData(e.currentTarget)); + + const validatedForm = inputNewAccountValidation.parse(form); + GammaClient.instance() + .createAccount.createAccount(validatedForm) + .then(() => { + window.location.href = "http://gamma:8081/api/login"; + }); + }; + + return ( +
+
+ + + + + + + + + + + + + +
+
+ ); +}; diff --git a/frontend/src/pages/create-client/index.tsx b/frontend/src/pages/create-client/index.tsx new file mode 100644 index 000000000..6b871688c --- /dev/null +++ b/frontend/src/pages/create-client/index.tsx @@ -0,0 +1,141 @@ +import * as z from "zod"; +import { useNavigate } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; +import { useCreateClientsLoaderData } from "./loader"; +import { useRef, useState } from "react"; + +const createClientValidation = z + .object({ + webServerRedirectUrl: z.string(), + svDescription: z.string(), + enDescription: z.string(), + prettyName: z.string(), + generateApiKey: z.coerce.boolean(), + emailScope: z.coerce.boolean(), + restriction: z + .object({ + superGroupIds: z.array(z.string()), + }) + .strict() + .nullable(), + }) + .strict(); + +export const CreateClientPage = () => { + const { superGroups } = useCreateClientsLoaderData(); + const navigate = useNavigate(); + + const [selectedSuperGroupIds, setSelectedSuperGroupIds] = useState( + [], + ); + + const superGroupSelectRef = useRef(null); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const form = Object.fromEntries(new FormData(e.currentTarget)); + + const validatedForm = createClientValidation.parse({ + ...form, + restriction: + selectedSuperGroupIds.length > 0 + ? { + superGroupIds: selectedSuperGroupIds, + } + : null, + }); + + GammaClient.instance() + .clients.createClient(validatedForm) + .then(() => { + navigate("/clients"); + }); + }; + + return ( +
+
+ + + + + + + + +

Restricted to

+ +

Super groups

+
    + {selectedSuperGroupIds.map((superGroupId) => ( +
  • + { + superGroups.find((superGroup) => superGroup.id === superGroupId) + ?.prettyName + } +
  • + ))} +
+ + + + +
+
+ ); +}; diff --git a/frontend/src/pages/create-client/loader.ts b/frontend/src/pages/create-client/loader.ts new file mode 100644 index 000000000..f773ed005 --- /dev/null +++ b/frontend/src/pages/create-client/loader.ts @@ -0,0 +1,18 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type CreateClientsLoaderReturn = Awaited< + ReturnType +>; + +export const useCreateClientsLoaderData = (): CreateClientsLoaderReturn => { + return useLoaderData() as CreateClientsLoaderReturn; +}; + +export const createClientsLoader = async () => { + return { + superGroups: await GammaClient.instance().superGroups.getSuperGroups(), + users: await GammaClient.instance().users.getUsers(), + posts: await GammaClient.instance().posts.getPosts(), + }; +}; diff --git a/frontend/src/pages/error/index.tsx b/frontend/src/pages/error/index.tsx new file mode 100644 index 000000000..aa4f0a57d --- /dev/null +++ b/frontend/src/pages/error/index.tsx @@ -0,0 +1,49 @@ +import { isRouteErrorResponse, useRouteError } from "react-router-dom"; +import * as z from "zod"; + +export const ErrorPage = () => { + const error = useRouteError(); + + if (isRouteErrorResponse(error)) { + switch (error.status) { + case 500: { + return ( +
+
+

Error: 500

+

The Office US, TODO

+
+
+ ); + } + case 404: { + return ( +
+
+

Error: 404

+

+ The Office US, Season 7, Episode 15, @07:23 +

+
+
+ ); + } + case 403: { + return ( +
+
+

Error: 403

+

+ The Office US, Season 6, Episode 1, @04:02 +

+
+
+ ); + } + } + } else if (error instanceof z.ZodError) { + console.log(error); + return
Unexpected response from backend. Please contact digIT
; + } + return
Something went wrong :(
; +}; diff --git a/frontend/src/pages/gdpr/index.tsx b/frontend/src/pages/gdpr/index.tsx new file mode 100644 index 000000000..c75abe02b --- /dev/null +++ b/frontend/src/pages/gdpr/index.tsx @@ -0,0 +1,57 @@ +import { useGdprTrainedLoaderData } from "./loader"; +import { Link, useRevalidator } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +const tableCellStyle = "border border-slate-300 p-1"; + +export const GdprTrainedPage = () => { + const { users, gdprTrained } = useGdprTrainedLoaderData(); + const revalidator = useRevalidator(); + + const setGdprTrained = + (userId: string) => (e: React.ChangeEvent) => { + GammaClient.instance() + .gdpr.setGdprTrained(userId, e.target.checked) + .then(() => { + revalidator.revalidate(); + }); + }; + + return ( + + + + + + + + + + + + + {users.map((user) => ( + + + + + + + + ))} + +
Set gdpr trained
First nameNickLast nameHas gdpr trainedDetails
{user.firstName}{user.nick}{user.lastName} + + + Details +
+ ); +}; diff --git a/frontend/src/pages/gdpr/loader.ts b/frontend/src/pages/gdpr/loader.ts new file mode 100644 index 000000000..ce38ff7a3 --- /dev/null +++ b/frontend/src/pages/gdpr/loader.ts @@ -0,0 +1,15 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type GdprTrainedLoaderReturn = Awaited>; + +export const useGdprTrainedLoaderData = (): GdprTrainedLoaderReturn => { + return useLoaderData() as GdprTrainedLoaderReturn; +}; + +export const gdprTrainedLoader = async () => { + return { + gdprTrained: await GammaClient.instance().gdpr.getGdprTrained(), + users: await GammaClient.instance().users.getUsers(), + }; +}; diff --git a/frontend/src/pages/groups/index.tsx b/frontend/src/pages/groups/index.tsx new file mode 100644 index 000000000..dbc225ef4 --- /dev/null +++ b/frontend/src/pages/groups/index.tsx @@ -0,0 +1,97 @@ +import { useGroupsLoaderData } from "./loader"; +import { Link, useRevalidator } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; +import * as z from "zod"; + +const tableCellStyle = "border border-slate-300 p-1"; + +const createGroupValidation = z.object({ + name: z.string(), + prettyName: z.string(), + superGroup: z.string(), +}); + +export const GroupsPage = () => { + const { groups, superGroups } = useGroupsLoaderData(); + const revalidator = useRevalidator(); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const form = Object.fromEntries(new FormData(e.currentTarget)); + + const validatedForm = createGroupValidation.parse(form); + GammaClient.instance() + .groups.createGroup(validatedForm) + .then(() => revalidator.revalidate()); + }; + + const deleteGroup = (type: string) => () => { + GammaClient.instance() + .types.deleteType(type) + .then(() => { + revalidator.revalidate(); + }); + }; + + return ( +
+ + + + + + + + + + + {groups.map((group) => ( + + + + + + + ))} + +
NameSuper groupDetailsDelete
{group.prettyName} + + {group.superGroup.prettyName} + + + Details + + +
+ +
+
+ + + + +
+
+
+ ); +}; diff --git a/frontend/src/pages/groups/loader.ts b/frontend/src/pages/groups/loader.ts new file mode 100644 index 000000000..63aa01be2 --- /dev/null +++ b/frontend/src/pages/groups/loader.ts @@ -0,0 +1,15 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type GroupsLoaderReturn = Awaited>; + +export const useGroupsLoaderData = (): GroupsLoaderReturn => { + return useLoaderData() as GroupsLoaderReturn; +}; + +export const groupsLoader = async () => { + return { + superGroups: await GammaClient.instance().superGroups.getSuperGroups(), + groups: await GammaClient.instance().groups.getGroups(), + }; +}; diff --git a/frontend/src/pages/home/index.tsx b/frontend/src/pages/home/index.tsx new file mode 100644 index 000000000..e9e4dc9cf --- /dev/null +++ b/frontend/src/pages/home/index.tsx @@ -0,0 +1,55 @@ +import { FC } from "react"; +import { useHomeLoaderData } from "./loader"; +import { Link } from "react-router-dom"; + +export const HomePage: FC = () => { + const data = useHomeLoaderData(); + + return ( +
+

Welcome, {data.user.nick}

+
    +
  • + Me +
  • +
  • + Users +
  • +
  • + Groups +
  • +
  • + Super groups +
  • +
  • + Posts +
  • + {data.user.isAdmin && ( + <> +
  • + Types +
  • +
  • + Clients +
  • +
  • + Allow list +
  • +
  • + Activation codes +
  • +
  • + Admins +
  • +
  • + Gdpr trained +
  • +
  • + Api keys +
  • + + )} +
+
+ ); +}; diff --git a/frontend/src/pages/home/loader.ts b/frontend/src/pages/home/loader.ts new file mode 100644 index 000000000..6259061e5 --- /dev/null +++ b/frontend/src/pages/home/loader.ts @@ -0,0 +1,15 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type HomeLoaderReturn = Awaited>; + +export const useHomeLoaderData = (): HomeLoaderReturn => { + return useLoaderData() as HomeLoaderReturn; +}; + +export const homeLoader = async () => { + const user = await GammaClient.instance().users.getMe(); + return { + user, + }; +}; diff --git a/frontend/src/pages/me/index.tsx b/frontend/src/pages/me/index.tsx new file mode 100644 index 000000000..68134a473 --- /dev/null +++ b/frontend/src/pages/me/index.tsx @@ -0,0 +1,28 @@ +import { useMeLoaderData } from "./loader"; + +export const MePage = () => { + const me = useMeLoaderData(); + + return ( +
+
    + {"avatar"} +
  • {me.firstName + ' "' + me.nick + '" ' + me.lastName}
  • +
  • Acceptance year: {me.acceptanceYear}
  • +
  • Cid: {me.cid}
  • +
+ {me.groups.length > 0 && ( + <> +

Groups:

+
    + {me.groups.map(({ group, post }) => ( +
  • + {group.prettyName} - {post.enName}/{post.svName} +
  • + ))} +
+ + )} +
+ ); +}; diff --git a/frontend/src/pages/me/loader.ts b/frontend/src/pages/me/loader.ts new file mode 100644 index 000000000..bf1eb7722 --- /dev/null +++ b/frontend/src/pages/me/loader.ts @@ -0,0 +1,12 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type MeLoaderReturn = Awaited>; + +export const useMeLoaderData = (): MeLoaderReturn => { + return useLoaderData() as MeLoaderReturn; +}; + +export const meLoader = async () => { + return await GammaClient.instance().users.getMe(); +}; diff --git a/frontend/src/pages/posts/index.tsx b/frontend/src/pages/posts/index.tsx new file mode 100644 index 000000000..333432c9b --- /dev/null +++ b/frontend/src/pages/posts/index.tsx @@ -0,0 +1,96 @@ +import { usePostsLoaderData } from "./loader"; +import { Link, useRevalidator } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; +import * as z from "zod"; + +const tableCellStyle = "border border-slate-300 p-1"; + +const createPostValidation = z + .object({ + svName: z.string(), + enName: z.string(), + emailPrefix: z.string(), + }) + .strict(); + +export const PostsPage = () => { + const posts = usePostsLoaderData(); + const revalidator = useRevalidator(); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const form = Object.fromEntries(new FormData(e.currentTarget)); + + const validatedForm = createPostValidation.parse(form); + GammaClient.instance() + .posts.createPost(validatedForm) + .then(() => revalidator.revalidate()); + }; + + const deletePost = (id: string) => () => { + GammaClient.instance() + .posts.deletePost(id) + .then(() => { + revalidator.revalidate(); + }); + }; + + return ( +
+ + + + + + + + + + + + {posts.map((post) => ( + + + + + + + + ))} + +
Swedish nameEnglish nameEmail prefixDetailsDelete
{post.svName}{post.enName}{post.emailPrefix} + Details + + +
+ +
+
+ + + + +
+
+
+ ); +}; diff --git a/frontend/src/pages/posts/loader.ts b/frontend/src/pages/posts/loader.ts new file mode 100644 index 000000000..4070d0477 --- /dev/null +++ b/frontend/src/pages/posts/loader.ts @@ -0,0 +1,12 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type PostsLoaderReturn = Awaited>; + +export const usePostsLoaderData = (): PostsLoaderReturn => { + return useLoaderData() as PostsLoaderReturn; +}; + +export const postsLoader = async () => { + return await GammaClient.instance().posts.getPosts(); +}; diff --git a/frontend/src/pages/show-api-key/index.tsx b/frontend/src/pages/show-api-key/index.tsx new file mode 100644 index 000000000..36bf12407 --- /dev/null +++ b/frontend/src/pages/show-api-key/index.tsx @@ -0,0 +1,16 @@ +import { useShowApiKeyLoaderData } from "./loader"; + +export const ShowApiKeyPage = () => { + const apiKey = useShowApiKeyLoaderData(); + + return ( +
+
    +
  • {apiKey.prettyName}
  • +
  • {apiKey.svDescription}
  • +
  • {apiKey.enDescription}
  • +
  • {apiKey.keyType}
  • +
+
+ ); +}; diff --git a/frontend/src/pages/show-api-key/loader.ts b/frontend/src/pages/show-api-key/loader.ts new file mode 100644 index 000000000..49172819a --- /dev/null +++ b/frontend/src/pages/show-api-key/loader.ts @@ -0,0 +1,12 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type ShowApiKeyLoaderReturn = Awaited>; + +export const useShowApiKeyLoaderData = (): ShowApiKeyLoaderReturn => { + return useLoaderData() as ShowApiKeyLoaderReturn; +}; + +export const showApiKeyLoader = async (id: string) => { + return await GammaClient.instance().apiKeys.getApiKey(id); +}; diff --git a/frontend/src/pages/show-client/ManageAuthority.tsx b/frontend/src/pages/show-client/ManageAuthority.tsx new file mode 100644 index 000000000..2bb656fab --- /dev/null +++ b/frontend/src/pages/show-client/ManageAuthority.tsx @@ -0,0 +1,67 @@ +import { FC, useRef } from "react"; +import { useShowClientLoaderData } from "./loader"; +import { GammaClient } from "../../client/gamma"; +import * as z from "zod"; +import { useRevalidator } from "react-router-dom"; + +const addSuperGroupToAuthorityNameValidation = z + .object({ + superGroupId: z.string(), + }) + .strict(); + +export const ManageAuthority: FC<{ authorityName: string }> = ({ + authorityName, +}) => { + const { client, superGroups, authorities } = useShowClientLoaderData(); + const selectSuperGroupRef = useRef(null); + const revalidator = useRevalidator(); + + const addSuperGroupToAuthorityName = ( + e: React.FormEvent, + ) => { + e.preventDefault(); + const form = Object.fromEntries(new FormData(e.currentTarget)); + + const validatedForm = addSuperGroupToAuthorityNameValidation.parse(form); + GammaClient.instance() + .clientAuthorities.addSuperGroupToAuthority({ + clientUid: client.clientUid, + authorityName, + superGroupId: validatedForm.superGroupId, + }) + .then(() => revalidator.revalidate()); + }; + + const currentAuthority = authorities.find( + (authority) => authority.authorityName === authorityName, + ); + + if (currentAuthority === undefined) { + return null; + } + + return ( + <> + TODO: Add users and supergroup-posts combo +
+ Current super groups +
    + {currentAuthority.superGroups.map((superGroup) => ( +
  • {superGroup.prettyName}
  • + ))} +
+
+ + +
+
+ + ); +}; diff --git a/frontend/src/pages/show-client/index.tsx b/frontend/src/pages/show-client/index.tsx new file mode 100644 index 000000000..f5cb37509 --- /dev/null +++ b/frontend/src/pages/show-client/index.tsx @@ -0,0 +1,73 @@ +import { useShowClientLoaderData } from "./loader"; +import { useRevalidator } from "react-router-dom"; +import * as z from "zod"; +import { GammaClient } from "../../client/gamma"; +import { ManageAuthority } from "./ManageAuthority"; + +const createAuthorityNameValidation = z + .object({ + authorityName: z.string(), + }) + .strict(); + +export const ShowClientPage = () => { + const { client, authorities } = useShowClientLoaderData(); + const revalidator = useRevalidator(); + + const createAuthorityName = (e: React.FormEvent) => { + e.preventDefault(); + const form = Object.fromEntries(new FormData(e.currentTarget)); + + const validatedForm = createAuthorityNameValidation.parse(form); + GammaClient.instance() + .clientAuthorities.createAuthority( + client.clientUid, + validatedForm.authorityName, + ) + .then(() => revalidator.revalidate()); + }; + + return ( +
+
    +
  • {client.clientId}
  • +
  • {client.prettyName}
  • +
  • {client.svDescription}
  • +
  • {client.enDescription}
  • +
  • {client.webServerRedirectUrl}
  • +
  • Has Api key? {client.hasApiKey + ""}
  • +
+ {client.restriction !== null && ( + <> +

Restricted to:

+
    + {client.restriction.superGroups.map((superGroup) => ( +
  • {superGroup.prettyName}
  • + ))} +
+ + )} +

Authorities

+
    + {authorities.map((authority) => ( +
    +
  • {authority.authorityName}
  • + +
    + ))} +
+

Add new authority name

+
+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/pages/show-client/loader.ts b/frontend/src/pages/show-client/loader.ts new file mode 100644 index 000000000..67e1ab82b --- /dev/null +++ b/frontend/src/pages/show-client/loader.ts @@ -0,0 +1,19 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type ShowClientLoaderReturn = Awaited>; + +export const useShowClientLoaderData = (): ShowClientLoaderReturn => { + return useLoaderData() as ShowClientLoaderReturn; +}; + +export const showClientLoader = async (id: string) => { + return { + client: await GammaClient.instance().clients.getClient(id), + authorities: + await GammaClient.instance().clientAuthorities.getAuthorities(id), + superGroups: await GammaClient.instance().superGroups.getSuperGroups(), + users: await GammaClient.instance().users.getUsers(), + posts: await GammaClient.instance().posts.getPosts(), + }; +}; diff --git a/frontend/src/pages/show-group/index.tsx b/frontend/src/pages/show-group/index.tsx new file mode 100644 index 000000000..c721daa92 --- /dev/null +++ b/frontend/src/pages/show-group/index.tsx @@ -0,0 +1,44 @@ +import { useShowGroupLoaderData } from "./loader"; +import { Link } from "react-router-dom"; + +export const ShowGroupPage = () => { + const group = useShowGroupLoaderData(); + + return ( +
+ {"avatar"} + {"banner"} +
    +
  • {group.name}
  • +
  • {group.prettyName}
  • +
  • + + {group.superGroup.prettyName} + +
  • +
+ Members: +
    + {group.groupMembers.map((member) => ( +
  • + + {member.user.nick + + " - " + + member.post.svName + + "/" + + member.post.enName} + +
  • + ))} +
+
+ ); +}; diff --git a/frontend/src/pages/show-group/loader.ts b/frontend/src/pages/show-group/loader.ts new file mode 100644 index 000000000..6eb3d5d0f --- /dev/null +++ b/frontend/src/pages/show-group/loader.ts @@ -0,0 +1,12 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type ShowGroupLoaderReturn = Awaited>; + +export const useShowGroupLoaderData = (): ShowGroupLoaderReturn => { + return useLoaderData() as ShowGroupLoaderReturn; +}; + +export const showGroupLoader = async (id: string) => { + return await GammaClient.instance().groups.getGroup(id); +}; diff --git a/frontend/src/pages/show-post/index.tsx b/frontend/src/pages/show-post/index.tsx new file mode 100644 index 000000000..3c3fdbe73 --- /dev/null +++ b/frontend/src/pages/show-post/index.tsx @@ -0,0 +1,15 @@ +import { useShowPostLoaderData } from "./loader"; + +export const ShowPostPage = () => { + const post = useShowPostLoaderData(); + + return ( +
+
    +
  • {post.svName}
  • +
  • {post.enName}
  • +
  • {post.emailPrefix}
  • +
+
+ ); +}; diff --git a/frontend/src/pages/show-post/loader.ts b/frontend/src/pages/show-post/loader.ts new file mode 100644 index 000000000..2bac0a88a --- /dev/null +++ b/frontend/src/pages/show-post/loader.ts @@ -0,0 +1,12 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type ShowPostLoaderReturn = Awaited>; + +export const useShowPostLoaderData = (): ShowPostLoaderReturn => { + return useLoaderData() as ShowPostLoaderReturn; +}; + +export const showPostLoader = async (id: string) => { + return await GammaClient.instance().posts.getPost(id); +}; diff --git a/frontend/src/pages/show-super-group/index.tsx b/frontend/src/pages/show-super-group/index.tsx new file mode 100644 index 000000000..5e5698f33 --- /dev/null +++ b/frontend/src/pages/show-super-group/index.tsx @@ -0,0 +1,20 @@ +import { useShowSuperGroupLoaderData } from "./loader"; +import { Link } from "react-router-dom"; + +export const ShowSuperGroupPage = () => { + const superGroup = useShowSuperGroupLoaderData(); + + return ( +
+
    +
  • {superGroup.name}
  • +
  • {superGroup.prettyName}
  • +
  • + {superGroup.type} +
  • +
  • {superGroup.svDescription}
  • +
  • {superGroup.enDescription}
  • +
+
+ ); +}; diff --git a/frontend/src/pages/show-super-group/loader.ts b/frontend/src/pages/show-super-group/loader.ts new file mode 100644 index 000000000..6a32e71c6 --- /dev/null +++ b/frontend/src/pages/show-super-group/loader.ts @@ -0,0 +1,14 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type ShowSuperGroupLoaderReturn = Awaited< + ReturnType +>; + +export const useShowSuperGroupLoaderData = (): ShowSuperGroupLoaderReturn => { + return useLoaderData() as ShowSuperGroupLoaderReturn; +}; + +export const showSuperGroupLoader = async (id: string) => { + return await GammaClient.instance().superGroups.getSuperGroup(id); +}; diff --git a/frontend/src/pages/show-user/index.tsx b/frontend/src/pages/show-user/index.tsx new file mode 100644 index 000000000..b217b4066 --- /dev/null +++ b/frontend/src/pages/show-user/index.tsx @@ -0,0 +1,28 @@ +import { useShowUserLoaderData } from "./loader"; + +export const ShowUserPage = () => { + const { user, groups } = useShowUserLoaderData(); + + return ( +
+ {"avatar"} +
    +
  • {user.firstName + ' "' + user.nick + '" ' + user.lastName}
  • +
  • Acceptance year: {user.acceptanceYear}
  • +
  • Cid: {user.cid}
  • +
+ {groups.length > 0 && ( + <> +

Groups:

+
    + {groups.map(({ group, post }) => ( +
  • + {group.prettyName} - {post.enName}/{post.svName} +
  • + ))} +
+ + )} +
+ ); +}; diff --git a/frontend/src/pages/show-user/loader.ts b/frontend/src/pages/show-user/loader.ts new file mode 100644 index 000000000..12d631c7b --- /dev/null +++ b/frontend/src/pages/show-user/loader.ts @@ -0,0 +1,12 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type ShowUserLoaderReturn = Awaited>; + +export const useShowUserLoaderData = (): ShowUserLoaderReturn => { + return useLoaderData() as ShowUserLoaderReturn; +}; + +export const showUserLoader = async (id: string) => { + return await GammaClient.instance().users.getUser(id); +}; diff --git a/frontend/src/pages/super-groups/index.tsx b/frontend/src/pages/super-groups/index.tsx new file mode 100644 index 000000000..b8d724af7 --- /dev/null +++ b/frontend/src/pages/super-groups/index.tsx @@ -0,0 +1,114 @@ +import { useSuperGroupsLoaderData } from "./loader"; +import { Link, useRevalidator } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; +import * as z from "zod"; + +const tableCellStyle = "border border-slate-300 p-1"; + +const createSuperGroupValidation = z + .object({ + name: z.string(), + prettyName: z.string(), + type: z.string(), + svDescription: z.string(), + enDescription: z.string(), + }) + .strict(); + +export const SuperGroupsPage = () => { + const { superGroups, types } = useSuperGroupsLoaderData(); + + const revalidator = useRevalidator(); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const form = Object.fromEntries(new FormData(e.currentTarget)); + + const validatedForm = createSuperGroupValidation.parse(form); + GammaClient.instance() + .superGroups.createSuperGroup(validatedForm) + .then(() => revalidator.revalidate()); + }; + + const deleteSuperGroup = (id: string) => () => { + GammaClient.instance() + .superGroups.deleteSuperGroup(id) + .then(() => { + revalidator.revalidate(); + }); + }; + + return ( +
+ + + + + + + + + + + {superGroups.map((superGroup) => ( + + + + + + + ))} + +
NameTypeDetailsDelete
{superGroup.prettyName} + {superGroup.type} + + Details + + +
+ +
+
+ + + + + + +
+
+
+ ); +}; diff --git a/frontend/src/pages/super-groups/loader.ts b/frontend/src/pages/super-groups/loader.ts new file mode 100644 index 000000000..c02a355d0 --- /dev/null +++ b/frontend/src/pages/super-groups/loader.ts @@ -0,0 +1,15 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type SuperGroupsLoaderReturn = Awaited>; + +export const useSuperGroupsLoaderData = (): SuperGroupsLoaderReturn => { + return useLoaderData() as SuperGroupsLoaderReturn; +}; + +export const superGroupsLoader = async () => { + return { + types: await GammaClient.instance().types.getTypes(), + superGroups: await GammaClient.instance().superGroups.getSuperGroups(), + }; +}; diff --git a/frontend/src/pages/types/index.tsx b/frontend/src/pages/types/index.tsx new file mode 100644 index 000000000..ca9c3f185 --- /dev/null +++ b/frontend/src/pages/types/index.tsx @@ -0,0 +1,60 @@ +import { useTypesLoaderData } from "./loader"; +import * as z from "zod"; +import { useRevalidator } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +const createTypesValidation = z + .object({ + type: z.string(), + }) + .strict(); + +export const TypesPage = () => { + const types = useTypesLoaderData(); + const revalidator = useRevalidator(); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const form = Object.fromEntries(new FormData(e.currentTarget)); + + const validatedForm = createTypesValidation.parse(form); + GammaClient.instance() + .types.addType(validatedForm.type) + .then(() => revalidator.revalidate()); + }; + + const deleteType = (type: string) => () => { + GammaClient.instance() + .types.deleteType(type) + .then(() => { + revalidator.revalidate(); + }); + }; + + return ( +
+
    + {types.map((type) => ( +
  • +

    {type}

    + +
  • + ))} +
+ +
+
+ + + +
+
+
+ ); +}; diff --git a/frontend/src/pages/types/loader.ts b/frontend/src/pages/types/loader.ts new file mode 100644 index 000000000..6a43556eb --- /dev/null +++ b/frontend/src/pages/types/loader.ts @@ -0,0 +1,12 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type TypesLoaderReturn = Awaited>; + +export const useTypesLoaderData = (): TypesLoaderReturn => { + return useLoaderData() as TypesLoaderReturn; +}; + +export const typesLoader = async () => { + return await GammaClient.instance().types.getTypes(); +}; diff --git a/frontend/src/pages/users/index.tsx b/frontend/src/pages/users/index.tsx new file mode 100644 index 000000000..9b6d74987 --- /dev/null +++ b/frontend/src/pages/users/index.tsx @@ -0,0 +1,39 @@ +import { useUsersLoaderData } from "./loader"; +import { Link } from "react-router-dom"; + +const tableCellStyle = "border border-slate-300 p-1"; + +export const UsersPage = () => { + const { users } = useUsersLoaderData(); + + return ( + + + + + + + + + + + + {users.map((user) => ( + + + + + + + + ))} + +
First nameNickLast nameAcceptance yearDetails
{user.firstName}{user.nick}{user.lastName}{user.acceptanceYear} + Details +
+ ); +}; diff --git a/frontend/src/pages/users/loader.ts b/frontend/src/pages/users/loader.ts new file mode 100644 index 000000000..eae5b901e --- /dev/null +++ b/frontend/src/pages/users/loader.ts @@ -0,0 +1,15 @@ +import { useLoaderData } from "react-router-dom"; +import { GammaClient } from "../../client/gamma"; + +type UsersLoaderReturn = Awaited>; + +export const useUsersLoaderData = (): UsersLoaderReturn => { + return useLoaderData() as UsersLoaderReturn; +}; + +export const usersLoader = async () => { + const users = await GammaClient.instance().users.getUsers(); + return { + users, + }; +}; diff --git a/frontend/src/root.tsx b/frontend/src/root.tsx new file mode 100644 index 000000000..48ea68810 --- /dev/null +++ b/frontend/src/root.tsx @@ -0,0 +1,23 @@ +import { Link, Outlet } from "react-router-dom"; + +export const Root = () => { + return ( +
+
+ {"Enbärsskär"} +

+ Gamma - IT account +

+
+
+ +
+
+ ); +}; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx new file mode 100644 index 000000000..f4d1249eb --- /dev/null +++ b/frontend/src/router.tsx @@ -0,0 +1,184 @@ +import { createBrowserRouter } from "react-router-dom"; +import { AboutPage } from "./pages/about"; +import { Root } from "./root"; +import { HomePage } from "./pages/home"; +import { homeLoader } from "./pages/home/loader"; +import { ErrorPage } from "./pages/error"; +import { UsersPage } from "./pages/users"; +import { usersLoader } from "./pages/users/loader"; +import { ShowUserPage } from "./pages/show-user"; +import { showUserLoader } from "./pages/show-user/loader"; +import { GroupsPage } from "./pages/groups"; +import { groupsLoader } from "./pages/groups/loader"; +import { ShowGroupPage } from "./pages/show-group"; +import { showGroupLoader } from "./pages/show-group/loader"; +import { SuperGroupsPage } from "./pages/super-groups"; +import { superGroupsLoader } from "./pages/super-groups/loader"; +import { ShowSuperGroupPage } from "./pages/show-super-group"; +import { showSuperGroupLoader } from "./pages/show-super-group/loader"; +import { TypesPage } from "./pages/types"; +import { typesLoader } from "./pages/types/loader"; +import { ClientsPage } from "./pages/clients"; +import { clientsLoader } from "./pages/clients/loader"; +import { ShowClientPage } from "./pages/show-client"; +import { showClientLoader } from "./pages/show-client/loader"; +import { PostsPage } from "./pages/posts"; +import { postsLoader } from "./pages/posts/loader"; +import { ShowPostPage } from "./pages/show-post"; +import { showPostLoader } from "./pages/show-post/loader"; +import { AllowListPage } from "./pages/allow-list"; +import { allowListLoader } from "./pages/allow-list/loader"; +import { ActivationCodesPage } from "./pages/activation-codes"; +import { activationCodesLoader } from "./pages/activation-codes/loader"; +import { MePage } from "./pages/me"; +import { meLoader } from "./pages/me/loader"; +import { EnterCidPage } from "./pages/create-account/enter-cid"; +import { EmailSentPage } from "./pages/create-account/email-sent"; +import { InputNewAccountPage } from "./pages/create-account/input-new-account"; +import { AdminsPage } from "./pages/admins"; +import { adminsLoader } from "./pages/admins/loader"; +import { GdprTrainedPage } from "./pages/gdpr"; +import { gdprTrainedLoader } from "./pages/gdpr/loader"; +import { ApiKeysPage } from "./pages/api-keys"; +import { apiKeysLoader } from "./pages/api-keys/loader"; +import { ShowApiKeyPage } from "./pages/show-api-key"; +import { showApiKeyLoader } from "./pages/show-api-key/loader"; +import { CreateClientPage } from "./pages/create-client"; +import { createClientsLoader } from "./pages/create-client/loader"; + +export const router = createBrowserRouter([ + { + path: "/", + element: , + errorElement: , + children: [ + { path: "", element: , loader: homeLoader }, + { path: "/me", element: , loader: meLoader }, + { path: "/about", element: }, + { + path: "/create-account", + element: , + }, + { path: "/create-account/email-sent", element: }, + { path: "/create-account/input", element: }, + { + path: "/users/:id", + element: , + loader: ({ params }) => { + if (params.id === undefined) { + throw new Error("Unexpected undefined id in params"); + } + + return showUserLoader(params.id); + }, + }, + { path: "/users", element: , loader: usersLoader }, + { + path: "/groups/:id", + element: , + loader: ({ params }) => { + if (params.id === undefined) { + throw new Error("Unexpected undefined id in params"); + } + + return showGroupLoader(params.id); + }, + }, + { path: "/groups", element: , loader: groupsLoader }, + { + path: "/super-groups/:id", + element: , + loader: ({ params }) => { + if (params.id === undefined) { + throw new Error("Unexpected undefined id in params"); + } + + return showSuperGroupLoader(params.id); + }, + }, + { + path: "/super-groups", + element: , + loader: superGroupsLoader, + }, + { + path: "/types", + element: , + loader: typesLoader, + }, + { + path: "/clients/:id", + element: , + loader: ({ params }) => { + if (params.id === undefined) { + throw new Error("Unexpected undefined id in params"); + } + + return showClientLoader(params.id); + }, + }, + { + path: "/clients/create", + element: , + loader: createClientsLoader, + }, + { + path: "/clients", + element: , + loader: clientsLoader, + }, + { + path: "/posts/:id", + element: , + loader: ({ params }) => { + if (params.id === undefined) { + throw new Error("Unexpected undefined id in params"); + } + + return showPostLoader(params.id); + }, + }, + { + path: "/posts", + element: , + loader: postsLoader, + }, + { + path: "/allow-list", + element: , + loader: allowListLoader, + }, + { + path: "/activation-codes", + element: , + loader: activationCodesLoader, + }, + { + path: "/admins", + element: , + loader: adminsLoader, + }, + { + path: "/gdpr", + element: , + loader: gdprTrainedLoader, + }, + { + path: "/api-keys/:id", + element: , + loader: ({ params }) => { + if (params.id === undefined) { + throw new Error("Unexpected undefined id in params"); + } + + return showApiKeyLoader(params.id); + }, + }, + { + path: "/api-keys", + element: , + loader: apiKeysLoader, + }, + ], + }, +]); diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs new file mode 100644 index 000000000..9f680b9af --- /dev/null +++ b/frontend/tailwind.config.cjs @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./src/**/*.{html,ts,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 000000000..a7fc6fbf2 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 000000000..42872c59f --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 000000000..de9291235 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,34 @@ +import {defineConfig} from "vite"; +import react from "@vitejs/plugin-react"; + +/** @type {import('vite').UserConfig} */ +export default defineConfig({ + plugins: [react()], + server: { + origin: "http://gamma:3000", + host: "gamma", + port: 3000, + proxy: { + "/api/": { + target: "http://gamma:8081", + changeOrigin: true, + secure: false, + configure: (proxy) => { + proxy.on("error", (err) => { + console.log("proxy error", err); + }); + proxy.on("proxyReq", (proxyReq, req) => { + console.log("Sending Request to the Target:", req.method, req.url); + }); + proxy.on("proxyRes", (proxyRes, req) => { + console.log( + "Received Response from the Target:", + proxyRes.statusCode, + req.url, + ); + }); + }, + }, + }, + }, +}); diff --git a/nginx-proxy.conf b/nginx-proxy.conf new file mode 100644 index 000000000..84d117344 --- /dev/null +++ b/nginx-proxy.conf @@ -0,0 +1,16 @@ +events {} + +http { + server { + listen 80; + + location = / { + proxy_pass http://frontend:8080; + } + + location /api/* { + proxy_pass http://backend:8081; + } + + } +} diff --git a/no_backend.docker-compose.yml b/no_backend.docker-compose.yml new file mode 100644 index 000000000..5f49a3ed1 --- /dev/null +++ b/no_backend.docker-compose.yml @@ -0,0 +1,23 @@ +version: "3" +services: + db: + image: postgres:13 + restart: always + environment: + POSTGRES_PASSWORD: example + ports: + - 5432:5432 + + redis: + image: redis:5.0 + restart: always + ports: + - 6379:6379 + + gotify: + image: cthit/gotify + environment: + GOTIFY_PRE-SHARED-KEY: 123abc + GOTIFY_MOCK-MODE: "true" + ports: + - 8080:7000 diff --git a/node_modules/.modules.yaml b/node_modules/.modules.yaml new file mode 100644 index 000000000..6ccb851b3 --- /dev/null +++ b/node_modules/.modules.yaml @@ -0,0 +1,21 @@ +hoistPattern: + - '*' +hoistedDependencies: {} +included: + dependencies: true + devDependencies: true + optionalDependencies: true +injectedDeps: {} +layoutVersion: 5 +nodeLinker: isolated +packageManager: pnpm@8.11.0 +pendingBuilds: [] +prunedAt: Mon, 11 Dec 2023 19:08:07 GMT +publicHoistPattern: + - '*eslint*' + - '*prettier*' +registries: + default: https://registry.npmjs.org/ +skipped: [] +storeDir: /Users/theodor.angergard/Library/pnpm/store/v3 +virtualStoreDir: .pnpm diff --git a/node_modules/.pnpm/flat@6.0.1/node_modules/flat/LICENSE b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/LICENSE new file mode 100644 index 000000000..d99b65549 --- /dev/null +++ b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/LICENSE @@ -0,0 +1,12 @@ +Copyright (c) 2014, Hugh Kennedy +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/node_modules/.pnpm/flat@6.0.1/node_modules/flat/README.md b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/README.md new file mode 100644 index 000000000..325581cb0 --- /dev/null +++ b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/README.md @@ -0,0 +1,234 @@ +# flat [![Build Status](https://github.com/hughsk/flat/actions/workflows/main.yml/badge.svg)](https://github.com/hughsk/flat/actions/workflows/main.yml) + +Take a nested Javascript object and flatten it, or unflatten an object with +delimited keys. + +## Installation + +``` bash +$ npm install flat +``` + +## Methods + +### flatten(original, options) + +Flattens the object - it'll return an object one level deep, regardless of how +nested the original object was: + +``` javascript +import { flatten } from 'flat' + +flatten({ + key1: { + keyA: 'valueI' + }, + key2: { + keyB: 'valueII' + }, + key3: { a: { b: { c: 2 } } } +}) + +// { +// 'key1.keyA': 'valueI', +// 'key2.keyB': 'valueII', +// 'key3.a.b.c': 2 +// } +``` + +### unflatten(original, options) + +Flattening is reversible too, you can call `unflatten` on an object: + +``` javascript +import { unflatten } from 'flat' + +unflatten({ + 'three.levels.deep': 42, + 'three.levels': { + nested: true + } +}) + +// { +// three: { +// levels: { +// deep: 42, +// nested: true +// } +// } +// } +``` + +## Options + +### delimiter + +Use a custom delimiter for (un)flattening your objects, instead of `.`. + +### safe + +When enabled, both `flat` and `unflatten` will preserve arrays and their +contents. This is disabled by default. + +``` javascript +import { flatten } from 'flat' + +flatten({ + this: [ + { contains: 'arrays' }, + { preserving: { + them: 'for you' + }} + ] +}, { + safe: true +}) + +// { +// 'this': [ +// { contains: 'arrays' }, +// { preserving: { +// them: 'for you' +// }} +// ] +// } +``` + +### object + +When enabled, arrays will not be created automatically when calling unflatten, like so: + +``` javascript +unflatten({ + 'hello.you.0': 'ipsum', + 'hello.you.1': 'lorem', + 'hello.other.world': 'foo' +}, { object: true }) + +// hello: { +// you: { +// 0: 'ipsum', +// 1: 'lorem', +// }, +// other: { world: 'foo' } +// } +``` + +### overwrite + +When enabled, existing keys in the unflattened object may be overwritten if they cannot hold a newly encountered nested value: + +```javascript +unflatten({ + 'TRAVIS': 'true', + 'TRAVIS.DIR': '/home/travis/build/kvz/environmental' +}, { overwrite: true }) + +// TRAVIS: { +// DIR: '/home/travis/build/kvz/environmental' +// } +``` + +Without `overwrite` set to `true`, the `TRAVIS` key would already have been set to a string, thus could not accept the nested `DIR` element. + +This only makes sense on ordered arrays, and since we're overwriting data, should be used with care. + + +### maxDepth + +Maximum number of nested objects to flatten. + +``` javascript +import { flatten } from 'flat' + +flatten({ + key1: { + keyA: 'valueI' + }, + key2: { + keyB: 'valueII' + }, + key3: { a: { b: { c: 2 } } } +}, { maxDepth: 2 }) + +// { +// 'key1.keyA': 'valueI', +// 'key2.keyB': 'valueII', +// 'key3.a': { b: { c: 2 } } +// } +``` + +### transformKey + +Transform each part of a flat key before and after flattening. + +```javascript +import { flatten, unflatten } from 'flat' + +flatten({ + key1: { + keyA: 'valueI' + }, + key2: { + keyB: 'valueII' + }, + key3: { a: { b: { c: 2 } } } +}, { + transformKey: function(key){ + return '__' + key + '__'; + } +}) + +// { +// '__key1__.__keyA__': 'valueI', +// '__key2__.__keyB__': 'valueII', +// '__key3__.__a__.__b__.__c__': 2 +// } + +unflatten({ + '__key1__.__keyA__': 'valueI', + '__key2__.__keyB__': 'valueII', + '__key3__.__a__.__b__.__c__': 2 +}, { + transformKey: function(key){ + return key.substring(2, key.length - 2) + } +}) + +// { +// key1: { +// keyA: 'valueI' +// }, +// key2: { +// keyB: 'valueII' +// }, +// key3: { a: { b: { c: 2 } } } +// } +``` + +## Command Line Usage + +`flat` is also available as a command line tool. You can run it with [`npx`](https://docs.npmjs.com/cli/v8/commands/npx): + +```sh +npx flat foo.json +``` + +Or install the `flat` command globally: + +```sh +npm i -g flat && flat foo.json +``` + +Accepts a filename as an argument: + +```sh +flat foo.json +``` + +Also accepts JSON on stdin: + +```sh +cat foo.json | flat +``` diff --git a/node_modules/.pnpm/flat@6.0.1/node_modules/flat/cli.js b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/cli.js new file mode 100755 index 000000000..a1efc3f3f --- /dev/null +++ b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/cli.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node +import fs from 'node:fs' +import path from 'node:path' +import readline from 'node:readline' +import { flatten } from './index.js' + +const filepath = process.argv.slice(2)[0] +if (filepath) { + // Read from file + const file = path.resolve(process.cwd(), filepath) + fs.accessSync(file, fs.constants.R_OK) // allow to throw if not readable + out(JSON.parse(fs.readFileSync(file))) +} else if (process.stdin.isTTY) { + usage(0) +} else { + // Read from newline-delimited STDIN + const lines = [] + readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false + }) + .on('line', line => lines.push(line)) + .on('close', () => out(JSON.parse(lines.join('\n')))) +} + +function out (data) { + process.stdout.write(JSON.stringify(flatten(data), null, 2)) +} + +function usage (code) { + console.log(` +Usage: + +flat foo.json +cat foo.json | flat +`) + + process.exit(code || 0) +} diff --git a/node_modules/.pnpm/flat@6.0.1/node_modules/flat/index.d.ts b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/index.d.ts new file mode 100644 index 000000000..1442ce44f --- /dev/null +++ b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/index.d.ts @@ -0,0 +1,17 @@ +export interface FlattenOptions { + delimiter?: string; + maxDepth?: number; + safe?: boolean; + transformKey?: (key: string) => string; +} + +export function flatten(target: T, options?: FlattenOptions): R; + +export interface UnflattenOptions { + delimiter?: string; + object?: boolean; + overwrite?: boolean; + transformKey?: (key: string) => string; +} + +export function unflatten(target: T, options?: UnflattenOptions): R; diff --git a/node_modules/.pnpm/flat@6.0.1/node_modules/flat/index.js b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/index.js new file mode 100644 index 000000000..42e1ddc70 --- /dev/null +++ b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/index.js @@ -0,0 +1,157 @@ +function isBuffer (obj) { + return obj && + obj.constructor && + (typeof obj.constructor.isBuffer === 'function') && + obj.constructor.isBuffer(obj) +} + +function keyIdentity (key) { + return key +} + +export function flatten (target, opts) { + opts = opts || {} + + const delimiter = opts.delimiter || '.' + const maxDepth = opts.maxDepth + const transformKey = opts.transformKey || keyIdentity + const output = {} + + function step (object, prev, currentDepth) { + currentDepth = currentDepth || 1 + Object.keys(object).forEach(function (key) { + const value = object[key] + const isarray = opts.safe && Array.isArray(value) + const type = Object.prototype.toString.call(value) + const isbuffer = isBuffer(value) + const isobject = ( + type === '[object Object]' || + type === '[object Array]' + ) + + const newKey = prev + ? prev + delimiter + transformKey(key) + : transformKey(key) + + if (!isarray && !isbuffer && isobject && Object.keys(value).length && + (!opts.maxDepth || currentDepth < maxDepth)) { + return step(value, newKey, currentDepth + 1) + } + + output[newKey] = value + }) + } + + step(target) + + return output +} + +export function unflatten (target, opts) { + opts = opts || {} + + const delimiter = opts.delimiter || '.' + const overwrite = opts.overwrite || false + const transformKey = opts.transformKey || keyIdentity + const result = {} + + const isbuffer = isBuffer(target) + if (isbuffer || Object.prototype.toString.call(target) !== '[object Object]') { + return target + } + + // safely ensure that the key is + // an integer. + function getkey (key) { + const parsedKey = Number(key) + + return ( + isNaN(parsedKey) || + key.indexOf('.') !== -1 || + opts.object + ) + ? key + : parsedKey + } + + function addKeys (keyPrefix, recipient, target) { + return Object.keys(target).reduce(function (result, key) { + result[keyPrefix + delimiter + key] = target[key] + + return result + }, recipient) + } + + function isEmpty (val) { + const type = Object.prototype.toString.call(val) + const isArray = type === '[object Array]' + const isObject = type === '[object Object]' + + if (!val) { + return true + } else if (isArray) { + return !val.length + } else if (isObject) { + return !Object.keys(val).length + } + } + + target = Object.keys(target).reduce(function (result, key) { + const type = Object.prototype.toString.call(target[key]) + const isObject = (type === '[object Object]' || type === '[object Array]') + if (!isObject || isEmpty(target[key])) { + result[key] = target[key] + return result + } else { + return addKeys( + key, + result, + flatten(target[key], opts) + ) + } + }, {}) + + Object.keys(target).forEach(function (key) { + const split = key.split(delimiter).map(transformKey) + let key1 = getkey(split.shift()) + let key2 = getkey(split[0]) + let recipient = result + + while (key2 !== undefined) { + if (key1 === '__proto__') { + return + } + + const type = Object.prototype.toString.call(recipient[key1]) + const isobject = ( + type === '[object Object]' || + type === '[object Array]' + ) + + // do not write over falsey, non-undefined values if overwrite is false + if (!overwrite && !isobject && typeof recipient[key1] !== 'undefined') { + return + } + + if ((overwrite && !isobject) || (!overwrite && recipient[key1] == null)) { + recipient[key1] = ( + typeof key2 === 'number' && + !opts.object + ? [] + : {} + ) + } + + recipient = recipient[key1] + if (split.length > 0) { + key1 = getkey(split.shift()) + key2 = getkey(split[0]) + } + } + + // unflatten again for 'messy objects' + recipient[key1] = unflatten(target[key], opts) + }) + + return result +} diff --git a/node_modules/.pnpm/flat@6.0.1/node_modules/flat/node_modules/.bin/flat b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/node_modules/.bin/flat new file mode 100755 index 000000000..6d6940e5e --- /dev/null +++ b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/node_modules/.bin/flat @@ -0,0 +1,17 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -z "$NODE_PATH" ]; then + export NODE_PATH="/Users/theodor.angergard/EDF/private/Gamma/node_modules/.pnpm/flat@6.0.1/node_modules/flat/node_modules:/Users/theodor.angergard/EDF/private/Gamma/node_modules/.pnpm/flat@6.0.1/node_modules:/Users/theodor.angergard/EDF/private/Gamma/node_modules/.pnpm/node_modules" +else + export NODE_PATH="/Users/theodor.angergard/EDF/private/Gamma/node_modules/.pnpm/flat@6.0.1/node_modules/flat/node_modules:/Users/theodor.angergard/EDF/private/Gamma/node_modules/.pnpm/flat@6.0.1/node_modules:/Users/theodor.angergard/EDF/private/Gamma/node_modules/.pnpm/node_modules:$NODE_PATH" +fi +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../../cli.js" "$@" +else + exec node "$basedir/../../cli.js" "$@" +fi diff --git a/node_modules/.pnpm/flat@6.0.1/node_modules/flat/package.json b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/package.json new file mode 100644 index 000000000..b66013628 --- /dev/null +++ b/node_modules/.pnpm/flat@6.0.1/node_modules/flat/package.json @@ -0,0 +1,49 @@ +{ + "name": "flat", + "version": "6.0.1", + "type": "module", + "bin": { + "flat": "cli.js" + }, + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "files": [ + "cli.js", + "index.js", + "index.d.ts" + ], + "sideEffects": false, + "engines": { + "node": ">=18" + }, + "scripts": { + "test": "node --test && standard cli.js index.js test/test.js" + }, + "license": "BSD-3-Clause", + "description": "Take a nested Javascript object and flatten it, or unflatten an object with delimited keys", + "devDependencies": { + "standard": "^17.1.0" + }, + "repository": { + "type": "git", + "url": "git://github.com/hughsk/flat.git" + }, + "keywords": [ + "flat", + "json", + "flatten", + "unflatten", + "split", + "object", + "nested" + ], + "author": "Hugh Kennedy (https://hughsk.io)", + "bugs": { + "url": "https://github.com/hughsk/flat/issues" + }, + "homepage": "https://github.com/hughsk/flat" +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 000000000..2b9f1883a --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/prod.docker-compose.yml b/prod.docker-compose.yml new file mode 100644 index 000000000..9fb93da0a --- /dev/null +++ b/prod.docker-compose.yml @@ -0,0 +1,97 @@ +version: "3" +networks: + gamma: + +services: + db: + image: postgres:13 + restart: always + environment: + POSTGRES_USER: user # These should all be changed + POSTGRES_PASSWORD: password + POSTGRES_DB: db + networks: + - gamma + + frontend: + image: frontend:latest + build: + context: ./frontend/ + dockerfile: Dockerfile + args: + REACT_APP_BACKEND_URL: http://localhost:8080/api + depends_on: + - backend + networks: + - gamma + + backend: + build: + context: ./backend/ + dockerfile: dockerfile + environment: + # Default admin user name = admin + # Default admin password = password + + DB_USER: user + DB_PASSWORD: password + DB_HOST: db + DB_PORT: 5432 + DB_NAME: postgres + + REDIS_HOST: redis + REDIS_PASSWORD: "" + REDIS_PORT: 6379 + + GOTIFY_KEY: "123abc" + GOTIFY_URL: http://gotify:8080/mail + + SERVER_PORT: 8081 + SUCCESSFUL_LOGIN: http://localhost:8080 + CORS_ALLOWED_ORIGIN: http://localhost:8080 + BACKEND_URI: http://localhost:8080/api/ + PRODUCTION: "false" + COOKIE_DOMAIN: localhost + IS_MOCKING_CLIENT: "true" + depends_on: + - redis + - db + networks: + - gamma + + redis: + image: redis + networks: + - gamma + + adminer: + image: adminer + restart: always + networks: + - gamma + ports: + - 8082:8080 + + gotify: + image: cthit/gotify + networks: + - gamma + environment: + GOTIFY_PRE-SHARED-KEY: 123abc + GOTIFY_MOCK-MODE: "true" + + proxy: + image: nginx:1.16.0-alpine + networks: + - gamma + ports: + - 8080:80 + environment: + - NGINX_HOST=localhost + - NGINX_PORT=80 + volumes: + - ./nginx-proxy.conf:/etc/nginx/nginx.conf:ro + depends_on: + - frontend + - backend + command: [nginx-debug, '-g', 'daemon off;'] \ No newline at end of file