commit 456332f24e7ef7153402284bcc56063b41b84dcd Author: _Bastler Date: Sun Oct 3 17:07:01 2021 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..613c139 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +bin/ +target/ +.settings/ +.project +.classpath +hs_err*.log +application.properties +usernames.txt \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /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 new file mode 100755 index 0000000..dfa7849 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# we.bstly backend + +Backend of we.bstly created with Spring Framework. diff --git a/application/pom.xml b/application/pom.xml new file mode 100755 index 0000000..4f4dc73 --- /dev/null +++ b/application/pom.xml @@ -0,0 +1,142 @@ + + 4.0.0 + + de.bstly.we + webstly-main + ${revision} + + + application + application + + + + de.bstly.we + webstly-core + ${revision} + + + de.bstly.we + webstly-email + ${revision} + + + de.bstly.we + webstly-i18n + ${revision} + + + de.bstly.we + webstly-invite + ${revision} + + + de.bstly.we + webstly-jitsi + ${revision} + + + de.bstly.we + webstly-membership + ${revision} + + + de.bstly.we + webstly-minetest + ${revision} + + + de.bstly.we + webstly-oidc + ${revision} + + + de.bstly.we + webstly-partey + ${revision} + + + de.bstly.we + webstly-services + ${revision} + + + de.bstly.we + webstly-urlshortener + ${revision} + + + de.bstly.we + webstly-wireguard + ${revision} + + + + + + + db-inmemory + + + org.hsqldb + hsqldb + + + + + db-mariadb + + + org.mariadb.jdbc + mariadb-java-client + runtime + + + + + db-mysql + + + mysql + mysql-connector-java + runtime + + + + + db-postgresql + + + org.postgresql + postgresql + runtime + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + de.bstly.we.Application + we.bstly + true + ZIP + + + + build-info + + build-info + + + + + + + diff --git a/application/resources/loader.properties b/application/resources/loader.properties new file mode 100644 index 0000000..59a862e --- /dev/null +++ b/application/resources/loader.properties @@ -0,0 +1 @@ +loader.path=config/ \ No newline at end of file diff --git a/application/src/main/java/de/bstly/we/Application.java b/application/src/main/java/de/bstly/we/Application.java new file mode 100755 index 0000000..06be6e4 --- /dev/null +++ b/application/src/main/java/de/bstly/we/Application.java @@ -0,0 +1,44 @@ +/** + * + */ +package de.bstly.we; + +import java.util.EnumSet; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.SessionTrackingMode; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * + * @author _bastler@bstly.de + * + */ +@SpringBootApplication +@EnableScheduling +public class Application extends SpringBootServletInitializer { + /** + * @param args + */ + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + /* + * + * @see + * org.springframework.boot.web.servlet.support.SpringBootServletInitializer# + * onStartup(javax.servlet.ServletContext) + */ + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + super.onStartup(servletContext); + servletContext.setSessionTrackingModes(EnumSet.of(SessionTrackingMode.COOKIE)); + } + +} diff --git a/core/pom.xml b/core/pom.xml new file mode 100755 index 0000000..2da7163 --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,99 @@ + + 4.0.0 + + de.bstly.we + webstly-main + ${revision} + + + core + webstly-core + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.session + spring-session-jdbc + + + + + com.querydsl + querydsl-apt + + + + com.querydsl + querydsl-jpa + + + + + commons-validator + commons-validator + 1.7 + + + + com.google.code.gson + gson + + + + org.passay + passay + 1.6.0 + + + + dev.samstevens.totp + totp-spring-boot-starter + 1.7.1 + + + + org.bouncycastle + bcprov-jdk15on + 1.66 + + + + org.apache.commons + commons-lang3 + + + + javax.measure + unit-api + 2.1.2 + + + + org.apache.commons + commons-csv + 1.9.0 + + + diff --git a/core/src/main/java/de/bstly/we/CoreConfiguration.java b/core/src/main/java/de/bstly/we/CoreConfiguration.java new file mode 100644 index 0000000..dd8eedd --- /dev/null +++ b/core/src/main/java/de/bstly/we/CoreConfiguration.java @@ -0,0 +1,30 @@ +/** + * + */ +package de.bstly.we; + +import javax.persistence.EntityManager; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.querydsl.jpa.impl.JPAQueryFactory; + +/** + * + * @author _bastler@bstly.de + * + */ +@Configuration +public class CoreConfiguration { + + @Autowired + private EntityManager em; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(em); + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/PermissionManager.java b/core/src/main/java/de/bstly/we/businesslogic/PermissionManager.java new file mode 100755 index 0000000..0c44eec --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/PermissionManager.java @@ -0,0 +1,369 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.google.common.collect.Lists; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; + +import de.bstly.we.businesslogic.support.InstantHelper; +import de.bstly.we.model.Permission; +import de.bstly.we.model.PermissionMapping; +import de.bstly.we.model.QPermission; +import de.bstly.we.model.UserData; +import de.bstly.we.repository.PermissionRepository; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class PermissionManager implements UserDataProvider { + + @Autowired + private PermissionRepository permissionRepository; + @Autowired + private PermissionMappingManager permissionMappingManager; + private QPermission qPermission = QPermission.permission; + + DateTimeFormatter pretixOffsetDateTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssx"); + + /** + * + * @param target + * @param name + * @return + */ + public List get(Long target, String name) { + if (target != null) { + return Lists.newArrayList(permissionRepository + .findAll(qPermission.name.eq(name).and(qPermission.target.eq(target)))); + } + return Lists.newArrayList(); + } + + /** + * + * @param target + * @return + */ + public List getAllByTarget(Long target) { + if (target != null) { + return Lists.newArrayList(permissionRepository.findAll(qPermission.target.eq(target))); + } + return Lists.newArrayList(); + } + + /** + * + * @param target + * @return + */ + public List getNotExpiresByTarget(Long target) { + if (target != null) { + return Lists.newArrayList(permissionRepository.findAll(qPermission.target.eq(target) + .and(qPermission.expires.after(Instant.now()).and(qPermission.starts.isNull() + .or(qPermission.starts.before(Instant.now())))))); + } + return Lists.newArrayList(); + } + + /** + * + * @param name + * @return + */ + public List getNotExpiresByName(String name) { + if (name != null) { + return Lists.newArrayList(permissionRepository.findAll(qPermission.name.eq(name) + .and(qPermission.expires.after(Instant.now()).and(qPermission.starts.isNull() + .or(qPermission.starts.before(Instant.now())))))); + } + return Lists.newArrayList(); + } + + /** + * + * @param target + * @return + */ + public List getNotExpiresByTargetIgnoreStart(Long target) { + if (target != null) { + return Lists.newArrayList(permissionRepository.findAll( + qPermission.target.eq(target).and(qPermission.expires.after(Instant.now())))); + } + return Lists.newArrayList(); + } + + /** + * + * @param target + * @return + */ + public boolean isFullUser(Long target) { + return permissionRepository.exists(qPermission.target.eq(target) + .and(qPermission.addon.isFalse()).and(qPermission.expires.after(Instant.now()).and( + qPermission.starts.isNull().or(qPermission.starts.before(Instant.now()))))); + } + + /** + * + * @param target + * @param name + * @return + */ + public boolean hasPermission(Long target, String name) { + if (!Permissions.ROLE_ADMIN.equals(name) && hasPermission(target, Permissions.ROLE_ADMIN)) { + return true; + } + + return target != null && permissionRepository.exists(qPermission.name.eq(name) + .and(qPermission.target.eq(target)) + .and(qPermission.expires.after(Instant.now()).and( + qPermission.starts.isNull().or(qPermission.starts.before(Instant.now()))))); + } + + /** + * + * @param target + * @param name + * @param addon + * @param expires + * @return + */ + public Permission create(Long target, String name, boolean addon, Instant starts, + Instant expires) { + Permission newPermission = new Permission(); + newPermission.setTarget(target); + newPermission.setName(name); + newPermission.setAddon(addon); + newPermission.setStarts(starts); + newPermission.setExpires(expires); + + return permissionRepository.save(newPermission); + } + + /** + * + * @param permission + * @return + */ + public Permission update(Permission permission) { + Assert.isTrue( + permissionRepository.exists(qPermission.target.eq(permission.getTarget()) + .and(qPermission.name.eq(permission.getName()))), + "Permission '" + permission.getName() + "' for target + '" + permission.getTarget() + + "' not exists!"); + Permission updatePermission = permissionRepository.findOne(qPermission.target + .eq(permission.getTarget()).and(qPermission.name.eq(permission.getName()))).get(); + updatePermission.setStarts(permission.getStarts()); + updatePermission.setExpires(permission.getExpires()); + updatePermission.setAddon(permission.isAddon()); + return permissionRepository.save(updatePermission); + } + + /** + * + * @param name + * @param clone + * @return + */ + public List clone(String name, String clone) { + List permissions = Lists.newArrayList(); + + for (Permission permission : permissionRepository + .findAll(qPermission.name.eq(name).and(qPermission.expires.after(Instant.now())))) { + if (!permissionRepository.exists( + qPermission.name.eq(clone).and(qPermission.target.eq(permission.getTarget())) + .and(qPermission.expires.goe(permission.getExpires())))) { + permissions.add(create(permission.getTarget(), clone, permission.isAddon(), + permission.getStarts(), permission.getExpires())); + } + } + + return permissions; + } + + /** + * + * @param target + * @param name + */ + public void delete(Long target, String name) { + Assert.isTrue( + permissionRepository + .exists(qPermission.target.eq(target).and(qPermission.name.eq(name))), + "Permission '" + name + "' for target + '" + target + "' not exists!"); + Permission delete = permissionRepository + .findOne(qPermission.target.eq(target).and(qPermission.name.eq(name))).get(); + permissionRepository.delete(delete); + } + + /** + * + * @param target + */ + public void deleteAll(Long target) { + permissionRepository.deleteAll(permissionRepository.findAll(qPermission.target.eq(target))); + } + + /** + * + * @param name + */ + public void deleteAll(String name) { + permissionRepository.deleteAll(permissionRepository.findAll(qPermission.name.eq(name))); + } + + /** + * + * @param target + * @param item + * @param answers + */ + public void applyItem(Long target, Integer item, JsonArray answers, Instant start) { + for (Permission permission : getForItem(target, item, answers, start)) { + permissionRepository.save(permission); + } + } + + /** + * + * @param target + * @param item + * @param answers + * @return + */ + public List getForItem(Long target, Integer item, JsonArray answers, + Instant start) { + List permissions = Lists.newArrayList(); + + if (start == null) { + start = Instant.now(); + } + + for (PermissionMapping permissionMapping : permissionMappingManager.getAllByItem(item)) { + for (String name : permissionMapping.getNames()) { + Instant starts = null; + Instant expires = InstantHelper.plus(start, permissionMapping.getLifetime(), + permissionMapping.getLifetimeUnit()); + + boolean additional = true; + + for (JsonElement anwser : answers) { + if (anwser.isJsonObject() + && anwser.getAsJsonObject().has("question_identifier")) { + if (StringUtils.hasText(permissionMapping.getStartsQuestion()) + && permissionMapping.getStartsQuestion() + .equals(anwser.getAsJsonObject().get("question_identifier") + .getAsString()) + && anwser.getAsJsonObject().has("answer")) { + String dateTimeString = anwser.getAsJsonObject().get("answer") + .getAsString(); + if (StringUtils.hasText(dateTimeString)) { + dateTimeString = dateTimeString.replace(" ", "T"); + starts = OffsetDateTime.parse(dateTimeString).toInstant(); + expires = InstantHelper.plus(starts, + permissionMapping.getLifetime(), + permissionMapping.getLifetimeUnit()); + additional = false; + } + } + + if (StringUtils.hasText(permissionMapping.getExpiresQuestion()) + && permissionMapping.getExpiresQuestion() + .equals(anwser.getAsJsonObject().get("question_identifier") + .getAsString()) + && anwser.getAsJsonObject().has("answer")) { + String dateTimeString = anwser.getAsJsonObject().get("answer") + .getAsString(); + if (StringUtils.hasText(dateTimeString)) { + dateTimeString = dateTimeString.replace(" ", "T"); + expires = InstantHelper.plus( + OffsetDateTime.parse(dateTimeString).toInstant(), + permissionMapping.getLifetime(), + permissionMapping.getLifetimeUnit()); + additional = false; + } + } + } + } + + Permission permission = null; + + List existingPermissions = get(target, name); + + for (Permission existingPermission : existingPermissions) { + if (existingPermission.getStarts() == null) { + permission = existingPermission; + break; + } + } + + if (permission == null || !additional) { + permission = new Permission(); + permission.setTarget(target); + permission.setName(name); + permission.setAddon(permissionMapping.isAddon()); + permission.setStarts(starts); + permission.setExpires(expires); + } else { + permission.setExpires(InstantHelper.plus(permission.getExpires(), + permissionMapping.getLifetime(), permissionMapping.getLifetimeUnit())); + } + + if (permissionMapping.isLifetimeRound()) { + permission.setExpires(InstantHelper.truncate(permission.getExpires(), + permissionMapping.getLifetimeUnit())); + } + + permissions.add(permission); + } + } + + return permissions; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getId() + */ + @Override + public String getId() { + return "permissions"; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getUserData(java.lang.Long) + */ + @Override + public List getUserData(Long userId) { + List result = Lists.newArrayList(); + for (Permission permission : getAllByTarget(userId)) { + result.add(permission); + } + return result; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#purgeUserData(java.lang.Long) + */ + @Override + public void purgeUserData(Long userId) { + for (Permission permission : getAllByTarget(userId)) { + permissionRepository.delete(permission); + } + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/PermissionMappingManager.java b/core/src/main/java/de/bstly/we/businesslogic/PermissionMappingManager.java new file mode 100755 index 0000000..86f39a7 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/PermissionMappingManager.java @@ -0,0 +1,139 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import com.google.common.collect.Lists; + +import de.bstly.we.model.PermissionMapping; +import de.bstly.we.model.QPermissionMapping; +import de.bstly.we.repository.PermissionMappingRepository; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class PermissionMappingManager { + + @Autowired + private PermissionMappingRepository permissionMappingRepository; + private QPermissionMapping qPermissionMapping = QPermissionMapping.permissionMapping; + + /** + * + * @param item + * @return + */ + public List getAllByItem(Integer item) { + return Lists.newArrayList( + permissionMappingRepository.findAll(qPermissionMapping.item.eq(item))); + } + + /** + * + * @param name + * @return + */ + public List getAllByName(String name) { + return Lists.newArrayList( + permissionMappingRepository.findAll(qPermissionMapping.names.contains(name))); + } + + /** + * + * @param item + * @return + */ + public boolean exists(Integer item) { + return permissionMappingRepository.exists(qPermissionMapping.item.eq(item)); + } + + /** + * + * @param item + * @param name + * @return + */ + public boolean exists(Integer item, String name) { + return permissionMappingRepository.exists( + qPermissionMapping.item.eq(item).and(qPermissionMapping.names.contains(name))); + } + + /** + * + * @param item + * @param names + * @param lifetime + * @param lifetimeUnit + * @param addon + * @param product + * @return + */ + public PermissionMapping create(Integer item, Set names, Long lifetime, + ChronoUnit lifetimeUnit, boolean lifetimeRound, boolean addon, String product, + String startsQuestion, String expiresQuestion) { + for (String name : names) { + Assert.isTrue(!exists(item, name), "PermissionMapping for item '" + item + + "' with permission '" + name + "' already exists!"); + } + PermissionMapping permissionMapping = new PermissionMapping(); + permissionMapping.setItem(item); + permissionMapping.setNames(names); + permissionMapping.setLifetime(lifetime); + permissionMapping.setLifetimeUnit(lifetimeUnit); + permissionMapping.setLifetimeRound(lifetimeRound); + permissionMapping.setAddon(addon); + permissionMapping.setProduct(product); + permissionMapping.setStartsQuestion(startsQuestion); + permissionMapping.setExpiresQuestion(expiresQuestion); + return permissionMappingRepository.save(permissionMapping); + } + + /** + * + * @param permissionMapping + * @return + */ + public PermissionMapping update(PermissionMapping permissionMapping) { + return permissionMappingRepository.save(permissionMapping); + } + + /** + * + * @param item + * @param name + */ + public void delete(Long id) { + Assert.isTrue(permissionMappingRepository.existsById(id), + "Permission Mapping '" + id + "' does not exists!"); + PermissionMapping permissionMapping = permissionMappingRepository.findById(id).get(); + permissionMappingRepository.delete(permissionMapping); + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + Sort sort = descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending(); + return permissionMappingRepository.findAll(PageRequest.of(page, size, sort)); + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/Permissions.java b/core/src/main/java/de/bstly/we/businesslogic/Permissions.java new file mode 100644 index 0000000..c1a1853 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/Permissions.java @@ -0,0 +1,18 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +/** + * @author _bastler@bstly.de + * + */ +public interface Permissions { + + public static final String ROLE_ADMIN = "ROLE_ADMIN"; + public static final String ROLE_USER = "ROLE_USER"; + public static final String ROLE_MEMBER = "ROLE_MEMBER"; + public static final String ROLE_GUEST = "ROLE_GUEST"; + public static final String MAIL = "mail"; + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/PretixItemStatus.java b/core/src/main/java/de/bstly/we/businesslogic/PretixItemStatus.java new file mode 100644 index 0000000..2d838f8 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/PretixItemStatus.java @@ -0,0 +1,12 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +/** + * @author _bastler@bstly.de + * + */ +public enum PretixItemStatus { + ERROR, NOT_FOUND, PENDING, PAID, EXPIRED, CANCELED, REDEEMED +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/PretixManager.java b/core/src/main/java/de/bstly/we/businesslogic/PretixManager.java new file mode 100755 index 0000000..7b56970 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/PretixManager.java @@ -0,0 +1,542 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import java.time.Instant; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class PretixManager implements SmartInitializingSingleton { + + public static final int invalid_item = -1; + + private Logger logger = LoggerFactory.getLogger(PretixManager.class); + + @Autowired + private Environment environment; + @Autowired + private SystemPropertyManager systemPropertyManager; + protected static Gson gson = new Gson(); + + public static final String SYSTEM_PROPERTY_PRETIX_HOST = "pretix.host"; + public static final String SYSTEM_PROPERTY_PRETIX_TOKEN = "pretix.token"; + public static final String SYSTEM_PROPERTY_PRETIX_ORGANIZER = "pretix.organizer"; + public static final String SYSTEM_PROPERTY_PRETIX_EVENT = "pretix.event"; + public static final String SYSTEM_PROPERTY_PRETIX_CHECKINLIST = "pretix.checkinlist"; + public static final String SYSTEM_PROPERTY_PRETIX_QUOTA_REGISTRATIONS = "pretix.quota.registrations"; + public static final String SYSTEM_PROPERTY_PRETIX_QUOTA_ADDONS = "pretix.quota.addons"; + + public static final String SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ITEM = "pretix.membership.item"; + public static final String SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_ITEM = "pretix.membershipfee.item"; + + // membership ID management + public static final String SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ID_QUESTION = "pretix.membershipid.question"; + + public static final String SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_REFERENCE_QUESTION = "pretix.membershipfeereference.question"; + + public static final String SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_CODE_QUESTION = "pretix.membershipfeecode.question"; + + public static final String SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_INTERNAL_ID_QUESTION = "pretix.membershipinternalid.question"; + + public static final String SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ACTIVE_QUESTION = "pretix.membershipactive.question"; + + public static final String SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_DISCOUNT_PERIOD = "pretix.membership.discount.period"; + + public static final String SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_ORDER_DAYS = "pretix.membershipfee.order.days"; + + public static final String SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_REMINDER_DAYS = "pretix.membershipfee.reminder.days"; + + public static final String SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_REMINDER_QUESTION = "pretix.membershipfee.reminder.question"; + + public static final String SYSTEM_PROPERTY_PRETIX_ORDER_SENDMAIL = "pretix.order.sendmail"; + public static final String SYSTEM_PROPERTY_PRETIX_ORDER_TESTMODE = "pretix.order.testmode"; + public static final String SYSTEM_PROPERTY_PRETIX_ORDER_SIMULATE = "pretix.order.simulate"; + + public enum ITEM_STATUS { + ERROR, NOT_FOUND, PENDING, PAID, EXPIRED, CANCELED, REDEEMED + } + + private String host; + private String token; + private String organizer; + private String event; + private String checkinlist; + private int quotaRegistration; + private int quotaAddons; + protected WebClient webClient; + + /* + * @see org.springframework.beans.factory.SmartInitializingSingleton# + * afterSingletonsInstantiated() + */ + @Override + public void afterSingletonsInstantiated() { + + if (!systemPropertyManager.has(SYSTEM_PROPERTY_PRETIX_HOST)) { + systemPropertyManager.add(SYSTEM_PROPERTY_PRETIX_HOST, + environment.getProperty("we.bstly." + SYSTEM_PROPERTY_PRETIX_HOST, "")); + } + if (!systemPropertyManager.has(SYSTEM_PROPERTY_PRETIX_TOKEN)) { + systemPropertyManager.add(SYSTEM_PROPERTY_PRETIX_TOKEN, + environment.getProperty("we.bstly." + SYSTEM_PROPERTY_PRETIX_TOKEN, "")); + } + if (!systemPropertyManager.has(SYSTEM_PROPERTY_PRETIX_ORGANIZER)) { + systemPropertyManager.add(SYSTEM_PROPERTY_PRETIX_ORGANIZER, + environment.getProperty("we.bstly." + SYSTEM_PROPERTY_PRETIX_ORGANIZER, "")); + } + if (!systemPropertyManager.has(SYSTEM_PROPERTY_PRETIX_EVENT)) { + systemPropertyManager.add(SYSTEM_PROPERTY_PRETIX_EVENT, + environment.getProperty("we.bstly." + SYSTEM_PROPERTY_PRETIX_EVENT, "")); + } + if (!systemPropertyManager.has(SYSTEM_PROPERTY_PRETIX_CHECKINLIST)) { + systemPropertyManager.add(SYSTEM_PROPERTY_PRETIX_CHECKINLIST, + environment.getProperty("we.bstly." + SYSTEM_PROPERTY_PRETIX_CHECKINLIST, "")); + } + if (!systemPropertyManager.has(SYSTEM_PROPERTY_PRETIX_QUOTA_REGISTRATIONS)) { + systemPropertyManager.add(SYSTEM_PROPERTY_PRETIX_QUOTA_REGISTRATIONS, environment + .getProperty("we.bstly." + SYSTEM_PROPERTY_PRETIX_QUOTA_REGISTRATIONS, "0")); + } + if (!systemPropertyManager.has(SYSTEM_PROPERTY_PRETIX_QUOTA_ADDONS)) { + systemPropertyManager.add(SYSTEM_PROPERTY_PRETIX_QUOTA_ADDONS, environment + .getProperty("we.bstly." + SYSTEM_PROPERTY_PRETIX_QUOTA_ADDONS, "0")); + } + + if (!systemPropertyManager.has(SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ITEM)) { + systemPropertyManager.add(SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ITEM, environment + .getProperty("we.bstly." + SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ITEM, "0")); + } + if (!systemPropertyManager.has(SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_ITEM)) { + systemPropertyManager.add(SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_ITEM, environment + .getProperty("we.bstly." + SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_ITEM, "0")); + } + + buildWebClient(); + } + + /** + * + */ + public void buildWebClient() { + host = systemPropertyManager.get(SYSTEM_PROPERTY_PRETIX_HOST); + token = systemPropertyManager.get(SYSTEM_PROPERTY_PRETIX_TOKEN); + organizer = systemPropertyManager.get(SYSTEM_PROPERTY_PRETIX_ORGANIZER); + event = systemPropertyManager.get(SYSTEM_PROPERTY_PRETIX_EVENT); + checkinlist = systemPropertyManager.get(SYSTEM_PROPERTY_PRETIX_CHECKINLIST); + quotaRegistration = systemPropertyManager + .getInteger(SYSTEM_PROPERTY_PRETIX_QUOTA_REGISTRATIONS); + quotaAddons = systemPropertyManager.getInteger(SYSTEM_PROPERTY_PRETIX_QUOTA_ADDONS); + webClient = WebClient.builder().baseUrl(host) + .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .defaultHeader(HttpHeaders.AUTHORIZATION, "Token " + token).build(); + } + + /** + * + * @param secret + * @return + */ + public ITEM_STATUS getItemStatus(String secret) { + try { + JsonObject item = getCheckInItemBySecret(secret); + + if (item != null) { + if (item.get("secret").getAsString().equals(secret)) { + if (item.getAsJsonArray("checkins").size() < 1 + && "p".equals(item.get("order__status").getAsString())) { + return ITEM_STATUS.PAID; + } else if (item.getAsJsonArray("checkins").size() > 0) { + return ITEM_STATUS.REDEEMED; + } + } else { + return ITEM_STATUS.ERROR; + } + } else { + item = getOrderBySecret(secret); + if (item != null) { + logger.warn("Checked secret: '" + secret + "' without valid payment!"); + return ITEM_STATUS.ERROR; + } + } + return ITEM_STATUS.NOT_FOUND; + } catch (WebClientResponseException e) { + return ITEM_STATUS.ERROR; + } + } + + /** + * + * @param secret + * @return + */ + public JsonObject getCheckInItemBySecret(String secret) { + MultiValueMap queryParams = new LinkedMultiValueMap(); + queryParams.add("secret", secret); + JsonObject orderPositions = request( + String.format("/api/v1/organizers/%s/events/%s/checkinlists/%s/positions/", + organizer, event, checkinlist), + HttpMethod.GET, queryParams).getAsJsonObject(); + + JsonArray results = orderPositions.getAsJsonArray("results"); + if (results.size() == 1) { + return results.get(0).getAsJsonObject(); + } + + return null; + } + + /** + * + * @param code + * @return + */ + public JsonObject getOrder(String code) { + return request( + String.format("/api/v1/organizers/%s/events/%s/orders/%s/", organizer, event, code), + HttpMethod.GET).getAsJsonObject(); + } + + /** + * + * @param secret + * @return + */ + public JsonObject getOrderBySecret(String secret) { + MultiValueMap queryParams = new LinkedMultiValueMap(); + queryParams.add("secret", secret); + + JsonObject orderPositions = request( + String.format("/api/v1/organizers/%s/events/%s/orderpositions/", organizer, event), + HttpMethod.GET, queryParams).getAsJsonObject(); + + JsonArray results = orderPositions.getAsJsonArray("results"); + if (results.size() == 1) { + JsonObject orderPosition = results.get(0).getAsJsonObject(); + return request(String.format("/api/v1/organizers/%s/events/%s/orders/%s/", organizer, + event, orderPosition.get("order").getAsString()), HttpMethod.GET) + .getAsJsonObject(); + } + + return null; + } + + /** + * + * @param order + * @return + */ + public JsonObject createOrder(JsonObject order) { + return request(String.format("/api/v1/organizers/%s/events/%s/orders/", organizer, event), + HttpMethod.POST, order).getAsJsonObject(); + } + + /** + * + * @param order + * @return + */ + public JsonObject extendOrder(String code, JsonObject expire) { + return request(String.format("/api/v1/organizers/%s/events/%s/orders/%s/extend/", organizer, + event, code), HttpMethod.POST, expire).getAsJsonObject(); + } + + /** + * + * @param code + */ + public void sendEmail(String code) { + request(String.format("/api/v1/organizers/%s/events/%s/orders/%s/resend_link/", organizer, + event, code), HttpMethod.POST); + } + + /** + * + * @param secret + * @return + */ + public Instant getLastPaymentDateBySecret(String secret) { + MultiValueMap queryParams = new LinkedMultiValueMap(); + queryParams.add("secret", secret); + + JsonObject orderPositions = request( + String.format("/api/v1/organizers/%s/events/%s/orderpositions/", organizer, event), + HttpMethod.GET, queryParams).getAsJsonObject(); + + JsonArray results = orderPositions.getAsJsonArray("results"); + if (results.size() == 1) { + JsonObject orderPosition = results.get(0).getAsJsonObject(); + return getLastPaymentDateForOrder(orderPosition.get("order").getAsString()); + } + + return null; + } + + /** + * + * @param secret + * @return + */ + public Instant getLastPaymentDateForOrder(String order) { + JsonArray paymentResults = request( + String.format("/api/v1/organizers/%s/events/%s/orders/%s/payments/", organizer, + event, order), + HttpMethod.GET).getAsJsonObject().getAsJsonArray("results"); + + Instant lastDate = null; + + for (JsonElement element : paymentResults) { + JsonObject payment = element.getAsJsonObject(); + if ("confirmed".equalsIgnoreCase(payment.get("state").getAsString())) { + Instant currentDate = Instant.parse(payment.get("payment_date").getAsString()); + if (lastDate == null) { + lastDate = currentDate; + } else if (currentDate.isAfter(lastDate)) { + lastDate = currentDate; + } + } + } + + return lastDate; + } + + /** + * + * @param secret + * @return + */ + public JsonObject getCheckInItemByItem(Integer item) { + MultiValueMap queryParams = new LinkedMultiValueMap(); + queryParams.add("item", String.valueOf(item)); + JsonObject orderPositions = request( + String.format("/api/v1/organizers/%s/events/%s/checkinlists/%s/positions/", + organizer, event, checkinlist), + HttpMethod.GET, queryParams).getAsJsonObject(); + + JsonArray results = orderPositions.getAsJsonArray("results"); + if (results.size() == 1) { + return results.get(0).getAsJsonObject(); + } + + return null; + } + + /** + * + * @param secret + * @return + */ + public JsonObject redeemItem(String secret) { + if (getItemStatus(secret) == ITEM_STATUS.PAID) { + JsonObject checkIn = getCheckInPositions(secret); + return redeem(checkIn.get("id").getAsString()); + } + return null; + } + + /** + * + * @param idOrSecret + * @return + */ + public JsonObject getCheckInPositions(String idOrSecret) { + return request( + String.format("/api/v1/organizers/%s/events/%s/checkinlists/%s/positions/%s/", + organizer, event, checkinlist, idOrSecret), + HttpMethod.GET).getAsJsonObject(); + } + + /** + * + * @param idOrSecret + * @return + */ + public JsonObject redeem(String idOrSecret) { + return request(String.format( + "/api/v1/organizers/%s/events/%s/checkinlists/%s/positions/%s/redeem/", organizer, + event, checkinlist, idOrSecret), HttpMethod.POST).getAsJsonObject(); + } + + /** + * + * @return + */ + public JsonObject createRegistrationVoucher() { + return createVoucher(quotaRegistration); + } + + /** + * + * @return + */ + public JsonObject createAddOnVoucher() { + return createVoucher(quotaAddons); + } + + /** + * + * @param quotaId + * @return + */ + public JsonObject createVoucher(int quotaId) { + JsonObject voucher = new JsonObject(); + voucher.addProperty("max_usages", 1); + voucher.addProperty("quota", quotaId); + voucher.addProperty("block_quota", true); + return request(String.format("/api/v1/organizers/%s/events/%s/vouchers/", organizer, event), + HttpMethod.POST, voucher).getAsJsonObject(); + } + + /** + * + * @param secret + * @return + */ + public JsonObject getItem(Integer item) { + return request( + String.format("/api/v1/organizers/%s/events/%s/items/%s/", organizer, event, item), + HttpMethod.GET).getAsJsonObject(); + } + + public JsonArray getVariations(Integer item) { + return request(String.format("/api/v1/organizers/%s/events/%s/items/%s/variations/", + organizer, event, item), HttpMethod.GET).getAsJsonObject() + .getAsJsonArray("results"); + } + + /** + * + * @param item + * @param variationId + * @param variation + */ + public void updateVariation(Integer item, Integer variationId, JsonObject variation) { + request(String.format("/api/v1/organizers/%s/events/%s/items/%s/variations/%s/", organizer, + event, item, variationId), HttpMethod.PATCH, variation); + } + + /** + * + * @param item + * @param variation + */ + public void deleteVariation(Integer item, Integer variation) { + request(String.format("/api/v1/organizers/%s/events/%s/items/%s/variations/%s/", organizer, + event, item, variation), HttpMethod.DELETE); + } + + /** + * + * @param item + */ + public void deleteVariations(Integer item) { + for (JsonElement variationElement : getVariations(item)) { + JsonObject variation = variationElement.getAsJsonObject(); + deleteVariation(item, variation.get("id").getAsInt()); + } + } + + /** + * + * @param path + * @param method + * @return + */ + public JsonElement request(String path, HttpMethod method) { + return request(path, method, null, new LinkedMultiValueMap()); + } + + /** + * + * @param path + * @param method + * @param queryParameters + * @return + */ + public JsonElement request(String path, HttpMethod method, + MultiValueMap queryParameters) { + return request(path, method, null, queryParameters); + } + + /** + * + * @param path + * @param method + * @param queryParameters + * @return + */ + public JsonElement request(String path, HttpMethod method, JsonElement payload) { + return request(path, method, payload, new LinkedMultiValueMap()); + } + + /** + * + * @param path + * @param payload + * @param method + * @param queryParameters + * @return + */ + public JsonElement request(String path, HttpMethod method, JsonElement payload, + MultiValueMap queryParameters) { + WebClient.RequestBodySpec request = webClient.method(method) + .uri(uriBuilder -> uriBuilder.path(path).queryParams(queryParameters).build()); + + if (payload != null) { + request.bodyValue(gson.toJson(payload)); + } + + String jsonString = request.retrieve().bodyToMono(String.class).block(); + + if (StringUtils.hasText(jsonString)) { + return JsonParser.parseString(jsonString); + } + + return null; + } + + /** + * @return the organizer + */ + public String getOrganizer() { + return organizer; + } + + /** + * @return the event + */ + public String getEvent() { + return event; + } + + /** + * @return the checkinlist + */ + public String getCheckinlist() { + return checkinlist; + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/QuotaManager.java b/core/src/main/java/de/bstly/we/businesslogic/QuotaManager.java new file mode 100644 index 0000000..c2c566e --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/QuotaManager.java @@ -0,0 +1,233 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import com.google.common.collect.Lists; + +import de.bstly.we.model.QQuota; +import de.bstly.we.model.Quota; +import de.bstly.we.model.QuotaMapping; +import de.bstly.we.model.UserData; +import de.bstly.we.repository.QuotaRepository; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class QuotaManager implements UserDataProvider { + + @Autowired + private QuotaRepository quotaRepository; + @Autowired + private QuotaMappingManager quotaMappingManager; + private QQuota qQuota = QQuota.quota; + + /** + * + * @param target + * @param name + * @return + */ + public Quota get(Long target, String name) { + if (target != null && name != null) { + return quotaRepository.findOne(qQuota.name.eq(name).and(qQuota.target.eq(target))) + .orElse(null); + } + return null; + } + + /** + * + * @param name + * @return + */ + public List getAllByName(String name) { + return Lists.newArrayList(quotaRepository.findAll(qQuota.name.eq(name))); + } + + /** + * + * @param target + * @return + */ + public List getAllByTarget(Long target) { + if (target != null) { + return Lists.newArrayList(quotaRepository.findAll(qQuota.target.eq(target))); + } + return Lists.newArrayList(); + } + + /** + * + * @param target + * @return + */ + public List getNotExpiresByTarget(Long target) { + if (target != null) { + return Lists.newArrayList( + quotaRepository.findAll(qQuota.target.eq(target).and(qQuota.value.gt(0)))); + } + return Lists.newArrayList(); + } + + /** + * + * @param target + * @param name + * @return + */ + public boolean hasQuota(Long target, String name) { + return target != null && quotaRepository + .exists(qQuota.name.eq(name).and(qQuota.target.eq(target)).and(qQuota.value.gt(0))); + } + + /** + * + * @param target + * @param name + * @param addon + * @param value + * @param unit + * @return + */ + public Quota create(Long target, String name, long value, String unit, boolean disposable) { + Quota newQuota = new Quota(); + newQuota.setTarget(target); + newQuota.setName(name); + newQuota.setValue(value); + newQuota.setUnit(unit); + newQuota.setDisposable(disposable); + + return quotaRepository.save(newQuota); + } + + /** + * + * @param quota + * @return + */ + public Quota update(Quota quota) { + Assert.isTrue( + quotaRepository.exists( + qQuota.target.eq(quota.getTarget()).and(qQuota.name.eq(quota.getName()))), + "Quota '" + quota.getName() + "' for target + '" + quota.getTarget() + + "' not exists!"); + Quota updateQuota = quotaRepository + .findOne(qQuota.target.eq(quota.getTarget()).and(qQuota.name.eq(quota.getName()))) + .get(); + updateQuota.setValue(quota.getValue()); + updateQuota.setUnit(quota.getUnit()); + updateQuota.setDisposable(quota.isDisposable()); + return quotaRepository.save(updateQuota); + } + + /** + * + * @param name + * @param clone + * @return + */ + public List clone(String name, String clone, long value) { + List quotas = Lists.newArrayList(); + + for (Quota quota : quotaRepository.findAll(qQuota.name.eq(name))) { + if (!quotaRepository + .exists(qQuota.name.eq(clone).and(qQuota.target.eq(quota.getTarget())))) { + quotas.add(create(quota.getTarget(), clone, value >= 0 ? value : quota.getValue(), + quota.getUnit(), quota.isDisposable())); + } + } + + return quotas; + } + + /** + * + * @param target + * @param name + */ + public void delete(Long target, String name) { + Assert.isTrue(quotaRepository.exists(qQuota.target.eq(target).and(qQuota.name.eq(name))), + "Quota '" + name + "' for target + '" + target + "' not exists!"); + Quota delete = quotaRepository.findOne(qQuota.target.eq(target).and(qQuota.name.eq(name))) + .get(); + quotaRepository.delete(delete); + } + + /** + * + * @param target + */ + public void deleteAll(Long target) { + quotaRepository.deleteAll(quotaRepository.findAll(qQuota.target.eq(target))); + } + + /** + * + * @param name + */ + public void deleteAll(String name) { + quotaRepository.deleteAll(quotaRepository.findAll(qQuota.name.eq(name))); + } + + /** + * + * @param target + * @param item + */ + public void applyItem(Long target, Integer item) { + for (QuotaMapping quotaMapping : quotaMappingManager.getAllByItem(item)) { + Quota quota = get(target, quotaMapping.getName()); + if (quota == null) { + quota = create(target, quotaMapping.getName(), quotaMapping.getValue(), + quotaMapping.getUnit(), quotaMapping.isDisposable()); + + } else { + quota.setValue(quotaMapping.isAppend() ? quota.getValue() + quotaMapping.getValue() + : quotaMapping.getValue()); + quota = update(quota); + } + + } + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getId() + */ + @Override + public String getId() { + return "quotas"; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getUserData(java.lang.Long) + */ + @Override + public List getUserData(Long userId) { + List result = Lists.newArrayList(); + for (Quota quota : getAllByTarget(userId)) { + result.add(quota); + } + return result; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#purgeUserData(java.lang.Long) + */ + @Override + public void purgeUserData(Long userId) { + for (Quota quota : getAllByTarget(userId)) { + quotaRepository.delete(quota); + } + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/QuotaMappingManager.java b/core/src/main/java/de/bstly/we/businesslogic/QuotaMappingManager.java new file mode 100644 index 0000000..ea01998 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/QuotaMappingManager.java @@ -0,0 +1,134 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import com.google.common.collect.Lists; + +import de.bstly.we.model.QQuotaMapping; +import de.bstly.we.model.QuotaMapping; +import de.bstly.we.repository.QuotaMappingRepository; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class QuotaMappingManager { + + @Autowired + private QuotaMappingRepository quotaMappingRepository; + private QQuotaMapping qQuotaMapping = QQuotaMapping.quotaMapping; + + /** + * + * @param item + * @return + */ + public List getAllByItem(Integer item) { + return Lists + .newArrayList(quotaMappingRepository.findAll(qQuotaMapping.items.contains(item))); + } + + /** + * + * @param item + * @return + */ + public boolean exists(Integer item) { + return quotaMappingRepository.exists(qQuotaMapping.items.contains(item)); + } + + /** + * + * @param item + * @param name + * @return + */ + public boolean exists(Integer item, String name) { + return quotaMappingRepository + .exists(qQuotaMapping.items.contains(item).and(qQuotaMapping.name.eq(name))); + } + + /** + * + * @param item + * @param quota + * @param lifetime + * @return + */ + public QuotaMapping create(Set items, String name, long value, String unit, + boolean append, Set products, boolean disposable) { + for (Integer item : items) { + Assert.isTrue(!exists(item, name), "QuotaMapping for item '" + item + "' with quota '" + + name + "' already exists!"); + } + QuotaMapping quotaMapping = new QuotaMapping(); + quotaMapping.setItems(items); + quotaMapping.setName(name); + quotaMapping.setValue(value); + quotaMapping.setUnit(unit); + quotaMapping.setAppend(append); + quotaMapping.setProducts(products); + quotaMapping.setDisposable(disposable); + return quotaMappingRepository.save(quotaMapping); + } + + /** + * + * @param quotaMapping + * @return + */ + public QuotaMapping update(QuotaMapping quotaMapping) { + Assert.isTrue( + quotaMapping.getId() != null + && quotaMappingRepository.existsById(quotaMapping.getId()), + "QuotaMapping '" + quotaMapping.getId() + "' does not exists!"); + + QuotaMapping updateQuotaMapping = quotaMappingRepository.findById(quotaMapping.getId()) + .get(); + updateQuotaMapping.setProducts(quotaMapping.getProducts()); + updateQuotaMapping.setItems(quotaMapping.getItems()); + updateQuotaMapping.setValue(quotaMapping.getValue()); + updateQuotaMapping.setUnit(quotaMapping.getUnit()); + updateQuotaMapping.setAppend(quotaMapping.isAppend()); + updateQuotaMapping.setDisposable(quotaMapping.isDisposable()); + return quotaMappingRepository.save(updateQuotaMapping); + } + + /** + * + * @param item + * @param name + */ + public void delete(Long id) { + Assert.isTrue(quotaMappingRepository.existsById(id), + "QuotaMapping '" + id + "' does not exists!"); + quotaMappingRepository.deleteById(id); + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + Sort sort = descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending(); + return quotaMappingRepository.findAll(PageRequest.of(page, size, sort)); + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/Quotas.java b/core/src/main/java/de/bstly/we/businesslogic/Quotas.java new file mode 100644 index 0000000..1b76356 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/Quotas.java @@ -0,0 +1,15 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +/** + * @author _bastler@bstly.de + * + */ +public interface Quotas { + + public static final String REGISTRATION_VOUCHERS = "registration_vouchers"; + public static final String ALIAS_CREATION = "alias_creation"; + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/Setup.java b/core/src/main/java/de/bstly/we/businesslogic/Setup.java new file mode 100755 index 0000000..f5a0385 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/Setup.java @@ -0,0 +1,107 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import com.google.common.collect.Lists; + +import de.bstly.we.businesslogic.support.InstantHelper; +import de.bstly.we.controller.validation.UserModelValidator; +import de.bstly.we.model.User; +import de.bstly.we.model.UserStatus; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class Setup implements SmartInitializingSingleton { + + @Autowired + private UserManager userManager; + @Autowired + private PermissionManager permissionManager; + @Autowired + private ResourceLoader resourceLoader; + @Autowired + private SystemPropertyManager systemPropertyManager; + + protected static final long TAUSEND_JAHRE = 1000; + + @Value("${we.bstly.admin.password:}") + private String adminPassword; + @Value("${we.bstly.setup:true}") + private boolean setup; + + private Logger logger = LoggerFactory.getLogger(Setup.class); + + /* + * @see org.springframework.beans.factory.SmartInitializingSingleton# + * afterSingletonsInstantiated() + */ + @Override + public void afterSingletonsInstantiated() { + // create admin account if not found + if (!setup || !systemPropertyManager.has("setup")) { + if (!StringUtils.hasText(adminPassword)) { + adminPassword = RandomStringUtils.random(24, true, true); + logger.error("password for 'admin': " + adminPassword); + } + User admin = userManager.create("admin", adminPassword, UserStatus.SLEEP); + permissionManager.create(admin.getId(), Permissions.ROLE_ADMIN, false, null, + InstantHelper.plus(Instant.now(), TAUSEND_JAHRE, ChronoUnit.YEARS)); + + systemPropertyManager.add("setup", "true"); + } + + try { + Resource resource = resourceLoader.getResource("classpath:usernames.txt"); + + if (resource.exists()) { + BufferedReader br = new BufferedReader( + new InputStreamReader(resource.getInputStream())); + List usernames = Lists.newArrayList(); + + String line; + while ((line = br.readLine()) != null) { + if (StringUtils.hasText(line) && !usernames.contains(line) + && !line.startsWith("#")) { + usernames.add(line); + } + } + + if (systemPropertyManager.has(UserModelValidator.RESERVED_USERNAMES)) { + systemPropertyManager.update(UserModelValidator.RESERVED_USERNAMES, + org.apache.commons.lang3.StringUtils.join(usernames, ",")); + } else { + systemPropertyManager.add(UserModelValidator.RESERVED_USERNAMES, + org.apache.commons.lang3.StringUtils.join(usernames, ",")); + } + } + + } catch (IOException e) { + e.printStackTrace(); + } + + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/SystemProfileFieldManager.java b/core/src/main/java/de/bstly/we/businesslogic/SystemProfileFieldManager.java new file mode 100644 index 0000000..bcdb3cb --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/SystemProfileFieldManager.java @@ -0,0 +1,64 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; + +import de.bstly.we.model.SystemProfileField; +import de.bstly.we.repository.SystemProfileFieldRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class SystemProfileFieldManager { + + @Autowired + private SystemProfileFieldRepository systemProfileFieldRepository; + + /** + * + * @param systemProfileField + * @return + */ + public SystemProfileField save(SystemProfileField systemProfileField) { + return systemProfileFieldRepository.save(systemProfileField); + } + + /** + * + * @param name + * @return + */ + public SystemProfileField get(String name) { + return systemProfileFieldRepository.findById(name).orElse(null); + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + Sort sort = descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending(); + return systemProfileFieldRepository.findAll(PageRequest.of(page, size, sort)); + } + + /** + * + * @param name + */ + public void delete(String name) { + systemProfileFieldRepository.deleteById(name); + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/SystemPropertyManager.java b/core/src/main/java/de/bstly/we/businesslogic/SystemPropertyManager.java new file mode 100755 index 0000000..2b0f319 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/SystemPropertyManager.java @@ -0,0 +1,133 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import de.bstly.we.model.SystemProperty; +import de.bstly.we.repository.SystemPropertyRepository; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class SystemPropertyManager { + + @Autowired + private SystemPropertyRepository systemPropertyRepository; + + /** + * + * @param key + * @return + */ + public boolean has(String key) { + return systemPropertyRepository.existsById(key); + } + + /** + * + * @param key + * @return + */ + public String get(String key) { + return systemPropertyRepository.findById(key).orElse(new SystemProperty()).getValue(); + } + + /** + * + * @param key + * @param defaultValue + * @return + */ + public String get(String key, String defaultValue) { + return systemPropertyRepository.findById(key).orElse(new SystemProperty(key, defaultValue)).getValue(); + } + + /** + * + * @param key + * @return + */ + public boolean getBoolean(String key) { + return getBoolean(key, false); + } + + /** + * + * @param key + * @param defaultValue + * @return + */ + public boolean getBoolean(String key, boolean defaultValue) { + return Boolean.valueOf(get(key, String.valueOf(defaultValue))); + } + + /** + * + * @param key + * @return + */ + public int getInteger(String key) { + return getInteger(key, 0); + } + + /** + * + * @param key + * @param defaultValue + * @return + */ + public int getInteger(String key, int defaultValue) { + return Integer.valueOf(get(key, String.valueOf(defaultValue))); + } + + /** + * + * @param key + * @return + */ + public long getLong(String key) { + return getLong(key, 0L); + } + + /** + * + * @param key + * @param defaultValue + * @return + */ + public long getLong(String key, long defaultValue) { + return Long.valueOf(get(key, String.valueOf(defaultValue))); + } + + /** + * + * @param key + * @param value + */ + public void add(String key, String value) { + Assert.isTrue(!systemPropertyRepository.existsById(key), + "System Property already exists, use update method to change value!"); + systemPropertyRepository.save(new SystemProperty(key, value)); + } + + /** + * + * @param key + * @param value + */ + public void update(String key, String value) { + Assert.isTrue(systemPropertyRepository.existsById(key), + "System Property does not exists, use add method to add new!"); + SystemProperty systemProperty = systemPropertyRepository.findById(key).get(); + systemProperty.setValue(value); + systemPropertyRepository.save(systemProperty); + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/UserAliasManager.java b/core/src/main/java/de/bstly/we/businesslogic/UserAliasManager.java new file mode 100644 index 0000000..bab0c57 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/UserAliasManager.java @@ -0,0 +1,128 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import com.google.common.collect.Lists; + +import de.bstly.we.model.QUserAlias; +import de.bstly.we.model.UserAlias; +import de.bstly.we.model.UserData; +import de.bstly.we.repository.UserAliasRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class UserAliasManager implements UserDataProvider { + + @Autowired + private UserManager userManager; + @Autowired + private UserAliasRepository userAliasRepository; + private QUserAlias qUserAlias = QUserAlias.userAlias; + + /** + * + * @param id + * @return + */ + public UserAlias get(Long id) { + return userAliasRepository.findById(id).orElse(null); + } + + /** + * + * @param userAlias + * @return + */ + public UserAlias save(UserAlias userAlias) { + Assert.notNull(userAlias.getAlias(), "No alias defined!"); + Assert.notNull(userAlias.getTarget(), "No target defined!"); + Assert.notNull(userManager.get(userAlias.getTarget()), "Invalid target defined!"); + return userAliasRepository.save(userAlias); + } + + /** + * + * @param alias + * @return + */ + public UserAlias getByAlias(String alias) { + return userAliasRepository.findOne(qUserAlias.alias.eq(alias)).orElse(null); + } + + /** + * + * @param userId + * @return + */ + public List getAllByTarget(Long userId) { + return Lists.newArrayList(userAliasRepository.findAll(qUserAlias.target.eq(userId))); + } + + /** + * + * @param id + */ + public void delete(Long id) { + UserAlias userAlias = get(id); + if (userAlias != null) { + userAliasRepository.delete(userAlias); + } + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + Sort sort = descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending(); + return userAliasRepository.findAll(PageRequest.of(page, size, sort)); + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getId() + */ + @Override + public String getId() { + return "aliases"; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getUserData(java.lang.Long) + */ + @Override + public List getUserData(Long userId) { + List result = Lists.newArrayList(); + for (UserAlias userAlias : getAllByTarget(userId)) { + result.add(userAlias); + } + return result; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#purgeUserData(java.lang.Long) + */ + @Override + public void purgeUserData(Long userId) { + for (UserAlias userAlias : getAllByTarget(userId)) { + userAliasRepository.delete(userAlias); + } + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/UserDataManager.java b/core/src/main/java/de/bstly/we/businesslogic/UserDataManager.java new file mode 100644 index 0000000..218fdf8 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/UserDataManager.java @@ -0,0 +1,181 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import com.beust.jcommander.internal.Maps; +import com.google.common.collect.Lists; +import com.google.gson.Gson; + +import de.bstly.we.model.Permission; +import de.bstly.we.model.User; +import de.bstly.we.model.UserData; +import de.bstly.we.model.UserStatus; +import de.bstly.we.repository.UserRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class UserDataManager implements SmartInitializingSingleton { + + @Autowired + private ApplicationContext context; + public static final String SYSTEM_PROPERTY_USERDATA_DAYS = "userdata.days"; + public static final long SYSTEM_PROPERTY_USERDATA_DAYS_DEFAULT = 30; + + private Logger logger = LoggerFactory.getLogger(UserDataManager.class); + + @Autowired + private UserRepository userRepository; + @Autowired + private PermissionManager permissionManager; + @Autowired + private SystemPropertyManager systemPropertyManager; + + @Value("${we.bstly.userdata.purge:false}") + private boolean purge; + + /** + * UserData Provider + */ + private List providers; + private Gson gson = new Gson(); + + /* + * @see org.springframework.beans.factory.SmartInitializingSingleton# + * afterSingletonsInstantiated() + */ + @Override + public void afterSingletonsInstantiated() { + providers = Lists.newArrayList(); + + for (UserDataProvider provider : context.getBeansOfType(UserDataProvider.class).values()) { + providers.add(provider); + } + } + + /** + * + */ + @Scheduled(cron = "${we.bstly.userdata.cron:0 0 0 * * * }") + public void purge() { + long days = systemPropertyManager.getLong(SYSTEM_PROPERTY_USERDATA_DAYS, + SYSTEM_PROPERTY_USERDATA_DAYS_DEFAULT); + + Pageable pageable = PageRequest.of(0, 100, Sort.by("id")); + Page page; + do { + page = userRepository.findAll(pageable); + for (User user : page.getContent()) { + if (!UserStatus.SLEEP.equals(user.getStatus())) { + if (permissionManager.getNotExpiresByTargetIgnoreStart(user.getId()) + .isEmpty()) { + if (UserStatus.PURGE.equals(user.getStatus())) { + purge(user, !purge); + } else if (UserStatus.NORMAL.equals(user.getStatus())) { + Instant last = Instant.MIN; + for (Permission permission : permissionManager + .getAllByTarget(user.getId())) { + if (permission.getExpires().isAfter(last)) { + last = permission.getExpires(); + } + } + + if (Instant.now().minus(days, ChronoUnit.DAYS).isAfter(last)) { + purge(user, !purge); + } + } + } + } + } + pageable = page.nextPageable(); + } while (page.hasNext()); + + } + + /** + * + * @param username + */ + public void purge(User user, boolean dry) { + Long userId = user.getId(); + if (dry) { + logger.debug("Would purge all data of user '" + user.getUsername() + "' [id=" + + user.getId() + "]!"); + } else { + logger.warn("Purge all data of user '" + user.getUsername() + "' [id=" + user.getId() + + "]!"); + } + + for (UserDataProvider provider : providers) { + if (dry) { + List result = provider.getUserData(userId); + if (!result.isEmpty()) { + logger.debug("\tWould have purged '" + provider.getId() + "' data of user '" + + user.getUsername() + "' [id=" + user.getId() + "]!"); + if (logger.isTraceEnabled()) { + for (UserData userData : result) { + logger.trace("\t\t" + gson.toJson(userData)); + } + } + } + } else { + List result = provider.getUserData(userId); + if (!result.isEmpty()) { + logger.warn("\tPurge '" + provider.getId() + "' data of user '" + + user.getUsername() + "' [id=" + user.getId() + "]!"); + if (logger.isTraceEnabled()) { + for (UserData userData : result) { + logger.trace("\t\t" + gson.toJson(userData)); + } + } + provider.purgeUserData(userId); + } + } + } + + if (!dry) { + logger.warn("Purged all data of user '" + user.getUsername() + "' [id=" + user.getId() + + "]!"); + } + } + + /** + * + * @param userId + * @return + */ + public Map> get(Long userId) { + Map> userData = Maps.newHashMap(); + + for (UserDataProvider provider : providers) { + List result = provider.getUserData(userId); + if (!result.isEmpty()) { + userData.put(provider.getId(), result); + } + } + + return userData; + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/UserDataProvider.java b/core/src/main/java/de/bstly/we/businesslogic/UserDataProvider.java new file mode 100644 index 0000000..1a5a462 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/UserDataProvider.java @@ -0,0 +1,36 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import java.util.List; + +import de.bstly.we.model.UserData; + +/** + * @author _bastler@bstly.de + * + */ +public interface UserDataProvider { + + /** + * + * @return + */ + String getId(); + + /** + * + * @param userId + * @return + */ + List getUserData(Long userId); + + /** + * + * @param userId + * @return + */ + void purgeUserData(Long userId); + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/UserDomainManager.java b/core/src/main/java/de/bstly/we/businesslogic/UserDomainManager.java new file mode 100644 index 0000000..b0758b9 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/UserDomainManager.java @@ -0,0 +1,202 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import java.util.Hashtable; +import java.util.List; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.InitialDirContext; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import com.google.common.collect.Lists; +import com.querydsl.core.BooleanBuilder; + +import de.bstly.we.model.QUserDomain; +import de.bstly.we.model.UserData; +import de.bstly.we.model.UserDomain; +import de.bstly.we.repository.UserDomainRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class UserDomainManager implements UserDataProvider { + + public static final int DEFAULT_SECRET_LENGTH = 64; + + @Autowired + private UserManager userManager; + @Autowired + private UserDomainRepository userDomainRepository; + private QUserDomain qUserDomain = QUserDomain.userDomain; + + private InitialDirContext dirContext; + + /** + * + */ + public UserDomainManager() { + Hashtable env = new Hashtable(); + env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + env.put("java.naming.provider.url", "dns:"); + + try { + dirContext = new InitialDirContext(env); + } catch (NamingException e) { + e.printStackTrace(); + throw new RuntimeException("Could not crate InitialDirContext"); + } + } + + /** + * + * @param id + * @return + */ + public UserDomain get(Long id) { + return userDomainRepository.findById(id).orElse(null); + } + + /** + * + * @param userDomain + * @return + */ + public UserDomain save(UserDomain userDomain) { + Assert.notNull(userDomain.getDomain(), "No domain defined!"); + Assert.notNull(userDomain.getTarget(), "No target defined!"); + Assert.notNull(userManager.get(userDomain.getTarget()), "Invalid target defined!"); + return userDomainRepository.save(userDomain); + } + + /** + * + * @param domain + * @return + */ + public UserDomain getByDomain(String domain) { + return userDomainRepository.findOne(qUserDomain.domain.eq(domain)).orElse(null); + } + + /** + * + * @param userId + * @return + */ + public List getAllByTarget(Long userId) { + return Lists.newArrayList(userDomainRepository.findAll(qUserDomain.target.eq(userId))); + } + + /** + * + * @param id + */ + public void delete(Long id) { + UserDomain userDomain = get(id); + if (userDomain != null) { + userDomainRepository.delete(userDomain); + } + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + Sort sort = descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending(); + return userDomainRepository.findAll(PageRequest.of(page, size, sort)); + } + + /** + * + */ + @Scheduled(cron = "0 */15 * * * *") + public void validate() { + Pageable pageable = PageRequest.of(0, 100, Sort.by("id")); + BooleanBuilder filter = new BooleanBuilder(); + filter.and(qUserDomain.validated.isFalse() + .and(qUserDomain.secret.isNotNull().and(qUserDomain.secret.isNotEmpty()))); + Page page; + do { + page = userDomainRepository.findAll(filter.getValue(), pageable); + for (UserDomain userDomain : page.getContent()) { + try { + validate(userDomain); + } catch (NamingException e) { + e.printStackTrace(); + } + } + pageable = page.nextPageable(); + } while (page.hasNext()); + } + + /** + * + * @param userDomain + * @throws NamingException + */ + public boolean validate(UserDomain userDomain) throws NamingException { + Attributes attributes = dirContext.getAttributes("_bstly." + userDomain.getDomain(), + new String[] { "TXT" }); + + NamingEnumeration attributeEnumeration = attributes.getAll(); + while (attributeEnumeration.hasMore()) { + if (attributeEnumeration.next().toString().endsWith(userDomain.getSecret())) { + userDomain.setValidated(true); + save(userDomain); + return true; + } + } + + return false; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getId() + */ + @Override + public String getId() { + return "domains"; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getUserData(java.lang.Long) + */ + @Override + public List getUserData(Long userId) { + List result = Lists.newArrayList(); + for (UserDomain userDomain : getAllByTarget(userId)) { + result.add(userDomain); + } + return result; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#purgeUserData(java.lang.Long) + */ + @Override + public void purgeUserData(Long userId) { + for (UserDomain userDomain : getAllByTarget(userId)) { + userDomainRepository.delete(userDomain); + } + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/UserManager.java b/core/src/main/java/de/bstly/we/businesslogic/UserManager.java new file mode 100755 index 0000000..ab92bcb --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/UserManager.java @@ -0,0 +1,318 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletOutputStream; + +import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.Session; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.beust.jcommander.internal.Lists; + +import de.bstly.we.model.QUser; +import de.bstly.we.model.User; +import de.bstly.we.model.UserData; +import de.bstly.we.model.UserStatus; +import de.bstly.we.repository.UserRepository; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class UserManager implements UserDataProvider { + + private Logger logger = LoggerFactory.getLogger(UserManager.class); + + @Autowired + private UserRepository userRepository; + @Autowired + private PasswordEncoder passwordEncoder; + @Autowired + private UserDataManager userDataManager; + @Autowired + private FindByIndexNameSessionRepository sessionRepository; + + private QUser qUser = QUser.user; + + @Value("${we.bstly.userdata.directory}") + private String userDataDirectory; + @Value("${we.bstly.email.domain:we.bstly.de}") + private String userEmailDomain; + + /** + * @param id + * @return + */ + public User get(Long id) { + return userRepository.findById(id).orElse(null); + } + + /** + * + * @param username + * @return + */ + public User getByUsername(String username) { + return userRepository.findOne(qUser.username.equalsIgnoreCase(username)).orElse(null); + } + + /** + * + * @param resetToken + * @return + */ + public User getByResetToken(String resetToken) { + return userRepository.findOne(qUser.resetToken.eq(resetToken)).orElse(null); + } + + /** + * @param id + * @return + */ + public String getPasswordHash(Long id) { + Assert.isTrue(userRepository.existsById(id), "User with id '" + id + "' not exists!"); + return userRepository.findById(id).get().getPasswordHash(); + } + + /** + * + * @param id + * @param passwordHash + */ + public User setPassword(Long id, String password) { + Assert.isTrue(userRepository.existsById(id), "User with id '" + id + "' not exists!"); + User user = userRepository.findById(id).get(); + user.setPasswordHash(passwordEncoder.encode(password)); + return userRepository.save(user); + } + + /** + * + * @param username + * @param email + * @param password + * @param publicKey + * @return + */ + public User create(String username, String password, UserStatus status) { + Assert.isTrue(!userRepository.exists(qUser.username.equalsIgnoreCase(username)), + "Username '" + username + "' already exists!"); + User user = new User(); + user.setUsername(username); + if (StringUtils.hasText(password)) { + user.setPasswordHash(passwordEncoder.encode(password)); + } + user.setDisabled(false); + user.setLocked(false); + user.setStatus(status); + user = userRepository.save(user); + + return user; + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + Sort sort = descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending(); + return userRepository.findAll(PageRequest.of(page, size, sort)); + } + + /** + * @param user + * @return + */ + public User update(User user) { + Assert.isTrue(userRepository.existsById(user.getId()), + "User with id '" + user.getId() + "' not exists!"); + + User merge = get(user.getId()); + merge.setUsername(user.getUsername()); + merge.setStatus(user.getStatus()); + merge.setDisabled(user.isDisabled()); + merge.setLocked(user.isLocked()); + + if (merge.isDisabled() || merge.isLocked()) { + deleteSessionsForUser(merge); + } + + return userRepository.save(merge); + } + + /** + * + * @param user + */ + public void delete(User user) { + Assert.isTrue(userRepository.existsById(user.getId()), + "User with id '" + user.getId() + "' not exists!"); + + File publicKey = new File(getPublicKeyPath(user.getUsername())); + if (publicKey.exists()) { + publicKey.delete(); + } + + deleteSessionsForUser(user); + + userDataManager.purge(user, false); + } + + /** + * + * @param username + * @return + */ + public String getBstlyEmail(String username) { + return username + "@" + userEmailDomain; + } + + /** + * + * @param username + * @param publicKey + */ + public void writePublicKey(String username, String publicKey) { + if (StringUtils.hasText(publicKey)) { + if (!userDataDirectory.endsWith(File.separator)) { + userDataDirectory += File.separator; + } + + File userDir = new File(userDataDirectory + username); + + if (!userDir.exists()) { + userDir.mkdirs(); + } + + try { + String publicKeyPath = getPublicKeyPath(username); + FileWriter myWriter = new FileWriter(publicKeyPath); + myWriter.write(publicKey); + myWriter.close(); + String command = "gpg --import " + publicKeyPath; + Runtime.getRuntime().exec(command); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * + * @param username + * @return + */ + public String getPublicKeyPath(String username) { + return userDataDirectory + username + File.separator + "public.key"; + } + + /** + * @param user + * @param outputStream + */ + public void passwordReset(User user, ServletOutputStream outputStream) { + String resetToken = RandomStringUtils.random(64, true, true); + String command = "echo \"" + resetToken + "\" | gpg -ear " + + getBstlyEmail(user.getUsername()) + " --always-trust"; + + user.setResetToken(resetToken); + + try { + ProcessBuilder b = new ProcessBuilder("/bin/bash", "-c", command); + Process process = b.start(); + + BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream())); + BufferedReader errorReader = new BufferedReader( + new InputStreamReader(process.getErrorStream())); + int c; + while ((c = reader.read()) != -1) { + outputStream.write(c); + } + + String error = ""; + String line; + while ((line = errorReader.readLine()) != null) { + error += line; + } + + if (StringUtils.hasText(error)) { + logger.warn(error); + } + + reader.close(); + errorReader.close(); + + } catch (IOException e) { + return; + } + + update(user); + } + + /** + * + * @param user + */ + protected void deleteSessionsForUser(User user) { + Map usersSessions = sessionRepository + .findByPrincipalName(user.getUsername()); + for (Session session : usersSessions.values()) { + sessionRepository.deleteById(session.getId()); + } + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getId() + */ + @Override + public String getId() { + return "user"; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getUserData(java.lang.Long) + */ + @Override + public List getUserData(Long userId) { + List result = Lists.newArrayList(); + result.add(get(userId)); + return result; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#purgeUserData(java.lang.Long) + */ + @Override + public void purgeUserData(Long userId) { + userRepository.deleteById(userId); + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/UserProfileFieldManager.java b/core/src/main/java/de/bstly/we/businesslogic/UserProfileFieldManager.java new file mode 100644 index 0000000..3503be4 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/UserProfileFieldManager.java @@ -0,0 +1,143 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import com.google.common.collect.Lists; + +import de.bstly.we.model.QUserProfileField; +import de.bstly.we.model.UserData; +import de.bstly.we.model.UserProfileField; +import de.bstly.we.model.Visibility; +import de.bstly.we.repository.UserProfileFieldRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class UserProfileFieldManager implements UserDataProvider { + + @Autowired + private UserProfileFieldRepository userProfileFieldRepository; + private QUserProfileField qUserProfileField = QUserProfileField.userProfileField; + + /** + * + * @param target + * @param name + * @return + */ + public UserProfileField get(Long target, String name) { + return userProfileFieldRepository + .findOne(qUserProfileField.name.eq(name).and(qUserProfileField.target.eq(target))) + .orElse(null); + } + + /** + * + * @param target + * @return + */ + public List getAllByTarget(Long target) { + return Lists.newArrayList(userProfileFieldRepository.findAll( + qUserProfileField.target.eq(target), Sort.by("index", "name").ascending())); + } + + /** + * + * @param target + * @return + */ + public List getByTargetFiltered(Long target, List names) { + return Lists.newArrayList(userProfileFieldRepository.findAll( + qUserProfileField.target.eq(target).and(qUserProfileField.name.in(names)), + Sort.by("index", "name").ascending())); + } + + /** + * + * @param target + * @param visibilities + * @return + */ + public List getAllByTargetAndVisibilities(Long target, + List visibilities) { + return Lists.newArrayList(userProfileFieldRepository.findAll( + qUserProfileField.target.eq(target) + .and(qUserProfileField.visibility.in(visibilities)), + Sort.by("index", "name").ascending())); + } + + /** + * + * @param userProfileField + * @return + */ + public UserProfileField save(UserProfileField userProfileField) { + return userProfileFieldRepository.save(userProfileField); + } + + /** + * + * @param target + * @param name + */ + public void delete(Long target, String name) { + Assert.isTrue( + userProfileFieldRepository.exists( + qUserProfileField.target.eq(target).and(qUserProfileField.name.eq(name))), + "ProfileField '" + name + "' for target + '" + target + "' not exists!"); + UserProfileField delete = userProfileFieldRepository + .findOne(qUserProfileField.target.eq(target).and(qUserProfileField.name.eq(name))) + .get(); + userProfileFieldRepository.delete(delete); + } + + /** + * + * @param target + */ + public void deleteAll(Long target) { + userProfileFieldRepository + .deleteAll(userProfileFieldRepository.findAll(qUserProfileField.target.eq(target))); + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getId() + */ + @Override + public String getId() { + return "profilefields"; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getUserData(java.lang.Long) + */ + @Override + public List getUserData(Long userId) { + List result = Lists.newArrayList(); + for (UserProfileField userProfileField : getAllByTarget(userId)) { + result.add(userProfileField); + } + return result; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#purgeUserData(java.lang.Long) + */ + @Override + public void purgeUserData(Long userId) { + for (UserProfileField userProfileField : getAllByTarget(userId)) { + userProfileFieldRepository.delete(userProfileField); + } + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/UserProfileFields.java b/core/src/main/java/de/bstly/we/businesslogic/UserProfileFields.java new file mode 100644 index 0000000..99a0da1 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/UserProfileFields.java @@ -0,0 +1,15 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +/** + * @author _bastler@bstly.de + * + */ +public interface UserProfileFields { + + public static final String PROFILE_FIELD_EMAIL = "email"; + public static final String PROFILE_FIELD_EMAIL_PRIMARY = "primaryEmail"; + public static final String PROFILE_FIELD_LOCALE = "locale"; +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/UserTotpManager.java b/core/src/main/java/de/bstly/we/businesslogic/UserTotpManager.java new file mode 100644 index 0000000..7e987e0 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/UserTotpManager.java @@ -0,0 +1,178 @@ +/** + * + */ +package de.bstly.we.businesslogic; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.beust.jcommander.internal.Lists; + +import de.bstly.we.model.QUserTotp; +import de.bstly.we.model.User; +import de.bstly.we.model.UserData; +import de.bstly.we.model.UserTotp; +import de.bstly.we.repository.UserTotpRepository; +import de.bstly.we.security.businesslogic.SecondFactorProvider; +import dev.samstevens.totp.code.CodeVerifier; +import dev.samstevens.totp.qr.QrData; +import dev.samstevens.totp.qr.QrDataFactory; +import dev.samstevens.totp.recovery.RecoveryCodeGenerator; +import dev.samstevens.totp.secret.SecretGenerator; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class UserTotpManager implements SecondFactorProvider { + + @Autowired + private UserTotpRepository userTotpRepository; + @Autowired + private UserManager userManager; + @Autowired + private SecretGenerator secretGenerator; + @Autowired + private CodeVerifier verifier; + @Autowired + private QrDataFactory qrDataFactory; + @Autowired + private RecoveryCodeGenerator recoveryCodeGenerator; + private QUserTotp qUserTotp = QUserTotp.userTotp; + + /* + * @see de.bstly.we.security.businesslogic.SecondFactorProvider#getId() + */ + @Override + public String getId() { + return "totp"; + } + + /* + * @see + * de.bstly.we.security.businesslogic.SecondFactorProvider#supports(java.lang. + * String) + */ + @Override + public boolean supports(String provider) { + return getId().equals(provider); + } + + /* + * + * @see + * de.bstly.we.security.businesslogic.SecondFactorProvider#isEnabled(java.lang. + * Long) + */ + @Override + public boolean isEnabled(Long userId) { + return userTotpRepository + .exists(qUserTotp.target.eq(userId).and(qUserTotp.enabled.isTrue())); + } + + /* + * + * @see + * de.bstly.we.security.businesslogic.SecondFactorProvider#validate(java.lang. + * Long, java.lang.String) + */ + @Override + public boolean validate(Long userId, String code) { + UserTotp userTotp = userTotpRepository.findOne(qUserTotp.target.eq(userId)).orElse(null); + + if (userTotp != null) { + return verifier.isValidCode(userTotp.getSecret(), code); + } + + return false; + } + + /* + * @see + * de.bstly.we.security.businesslogic.SecondFactorProvider#getForUser(java.lang. + * Long) + */ + @Override + public UserTotp get(Long userId) { + return userTotpRepository.findOne(qUserTotp.target.eq(userId)).orElse(null); + } + + /* + * @see + * de.bstly.we.security.businesslogic.SecondFactorProvider#createByUser(java. + * lang.Long) + */ + @Override + public UserTotp create(Long userId) { + if (!isEnabled(userId)) { + delete(userId); + UserTotp userTotp = new UserTotp(); + userTotp.setTarget(userId); + userTotp.setSecret(secretGenerator.generate()); + + User user = userManager.get(userId); + QrData data = qrDataFactory.newBuilder().label(user.getUsername()) + .secret(userTotp.getSecret()).issuer("we.bstly").build(); + userTotp.setQrData(data.getUri()); + userTotp.setRecoveryCodes(Lists.newArrayList(recoveryCodeGenerator.generateCodes(16))); + return userTotpRepository.save(userTotp); + } + return userTotpRepository.findOne(qUserTotp.target.eq(userId)).orElse(null); + } + + /* + * @see + * de.bstly.we.security.businesslogic.SecondFactorProvider#enableByUser(java. + * lang.Long, java.lang.String) + */ + @Override + public boolean enable(Long userId, String code) { + if (validate(userId, code)) { + UserTotp userTotp = userTotpRepository.findOne(qUserTotp.target.eq(userId)) + .orElse(null); + userTotp.setEnabled(true); + userTotpRepository.save(userTotp); + return true; + } + return false; + } + + /* + * @see + * de.bstly.we.security.businesslogic.SecondFactorProvider#deleteByUser(java. + * lang.Long) + */ + @Override + public void delete(Long userId) { + UserTotp userTotp = get(userId); + + if (userTotp != null) { + userTotpRepository.delete(userTotp); + } + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getUserData(java.lang.Long) + */ + @Override + public List getUserData(Long userId) { + List result = Lists.newArrayList(); + UserTotp userTotp = get(userId); + if (userTotp != null) { + result.add(userTotp); + } + return result; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#purgeUserData(java.lang.Long) + */ + @Override + public void purgeUserData(Long userId) { + delete(userId); + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/support/AbstractModelEventListener.java b/core/src/main/java/de/bstly/we/businesslogic/support/AbstractModelEventListener.java new file mode 100644 index 0000000..4b165c9 --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/support/AbstractModelEventListener.java @@ -0,0 +1,67 @@ +/** + * + */ +package de.bstly.we.businesslogic.support; + +import javax.persistence.PostLoad; +import javax.persistence.PostPersist; +import javax.persistence.PostRemove; +import javax.persistence.PostUpdate; +import javax.persistence.PrePersist; +import javax.persistence.PreRemove; +import javax.persistence.PreUpdate; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import de.bstly.we.event.AbstractModelEvent; +import de.bstly.we.event.AbstractModelEventType; +import de.bstly.we.model.AbstractModel; + +/** + * @author Lurkars + * + */ +@Component +public class AbstractModelEventListener { + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @PrePersist + private void prePersist(AbstractModel model) { + applicationEventPublisher.publishEvent(new AbstractModelEvent(AbstractModelEventType.PRE_PERSIST, model)); + } + + @PreUpdate + private void preUpdate(AbstractModel model) { + applicationEventPublisher.publishEvent(new AbstractModelEvent(AbstractModelEventType.PRE_UPDATE, model)); + } + + @PreRemove + private void preRemove(AbstractModel model) { + applicationEventPublisher.publishEvent(new AbstractModelEvent(AbstractModelEventType.PRE_REMOVE, model)); + } + + @PostPersist + private void postPersist(AbstractModel model) { + applicationEventPublisher.publishEvent(new AbstractModelEvent(AbstractModelEventType.POST_PERSIST, model)); + } + + @PostUpdate + private void postUpdate(AbstractModel model) { + applicationEventPublisher.publishEvent(new AbstractModelEvent(AbstractModelEventType.POST_UPDATE, model)); + } + + @PostRemove + private void postRemove(AbstractModel model) { + applicationEventPublisher.publishEvent(new AbstractModelEvent(AbstractModelEventType.POST_REMOVE, model)); + } + + @PostLoad + private void postLoad(AbstractModel model) { + applicationEventPublisher.publishEvent(new AbstractModelEvent(AbstractModelEventType.POST_LOAD, model)); + } + +} diff --git a/core/src/main/java/de/bstly/we/businesslogic/support/InstantHelper.java b/core/src/main/java/de/bstly/we/businesslogic/support/InstantHelper.java new file mode 100644 index 0000000..1056b4f --- /dev/null +++ b/core/src/main/java/de/bstly/we/businesslogic/support/InstantHelper.java @@ -0,0 +1,83 @@ +/** + * + */ +package de.bstly.we.businesslogic.support; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; + +/** + * @author _bastler@bstly.de + * + */ +public class InstantHelper { + + /** + * + * @param instant + * @param amount + * @return + */ + public static Instant plus(Instant instant, TemporalAmount amount) { + return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).plus(amount).toInstant(); + } + + /** + * + * @param instant + * @param amountToAdd + * @param unit + * @return + */ + public static Instant plus(Instant instant, long amountToAdd, TemporalUnit unit) { + return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).plus(amountToAdd, unit).toInstant(); + } + + /** + * + * @param instant + * @param amount + * @return + */ + public static Instant minus(Instant instant, TemporalAmount amount) { + return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).minus(amount).toInstant(); + } + + /** + * + * @param instant + * @param amountToAdd + * @param unit + * @return + */ + public static Instant minus(Instant instant, long amountToAdd, TemporalUnit unit) { + return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).minus(amountToAdd, unit) + .toInstant(); + } + + /** + * + * @param instant + * @param unit + * @return + */ + public static Instant truncate(Instant instant, TemporalUnit unit) { + if (ChronoUnit.YEARS.equals(unit)) { + instant = instant.truncatedTo(ChronoUnit.DAYS); + return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC) + .with(ChronoField.DAY_OF_YEAR, 1L).toInstant(); + } else if (ChronoUnit.MONTHS.equals(unit)) { + instant = instant.truncatedTo(ChronoUnit.DAYS); + return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC) + .with(ChronoField.DAY_OF_MONTH, 1L).toInstant(); + } + + return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).truncatedTo(unit).toInstant(); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/Authentication2FAController.java b/core/src/main/java/de/bstly/we/controller/Authentication2FAController.java new file mode 100644 index 0000000..fefa0a0 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/Authentication2FAController.java @@ -0,0 +1,202 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.beust.jcommander.internal.Lists; + +import de.bstly.we.controller.model.SecondFactorProviderModel; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.model.SecondFactor; +import de.bstly.we.security.businesslogic.SecondFactorProvider; +import de.bstly.we.security.businesslogic.SecondFactorProviderManager; +import de.bstly.we.security.businesslogic.SecondFactorRequestProvider; +import de.bstly.we.security.model.LocalUserDetails; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/auth/2fa") +public class Authentication2FAController extends BaseController { + + @Autowired + private SecondFactorProviderManager secondFactorProviderManager; + + /** + * + * @return + */ + protected Long getPreAuthenticationId() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.getPrincipal() instanceof LocalUserDetails) { + return ((LocalUserDetails) auth.getPrincipal()).getUserId(); + } + + return null; + } + + /** + * + * @return + */ + @GetMapping + public List getEnabled() { + Long userId = getPreAuthenticationId(); + if (userId == null) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + List result = Lists.newArrayList(); + + for (SecondFactorProvider provider : secondFactorProviderManager.getEnabled(userId)) { + result.add(new SecondFactorProviderModel(provider.getId(), + provider instanceof SecondFactorRequestProvider)); + } + + return result; + } + + /** + * + * @return + */ + @GetMapping("/available") + public List getAvailable() { + Long userId = getPreAuthenticationId(); + if (userId == null) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + List result = Lists.newArrayList(); + + for (SecondFactorProvider provider : secondFactorProviderManager.getAll()) { + result.add(new SecondFactorProviderModel(provider.getId(), + provider instanceof SecondFactorRequestProvider)); + } + + for (SecondFactorProvider provider : secondFactorProviderManager.getEnabled(userId)) { + SecondFactorProviderModel enabledProvider = new SecondFactorProviderModel( + provider.getId(), provider instanceof SecondFactorRequestProvider); + result.remove(enabledProvider); + } + return result; + } + + /** + * + * @return + */ + @PreAuthorize("authentication.authenticated") + @GetMapping("/{id}") + public void isSecondFactorEnabled(@PathVariable("id") String providerId) { + SecondFactorProvider provider = secondFactorProviderManager.getProvider(providerId); + + if (provider == null) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + if (!provider.isEnabled(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED); + } + } + + /** + * + */ + @PreAuthorize("hasRole('ROLE_PRE_AUTH_USER')") + @PostMapping("/{id}") + public void requestSecondFactorMail(@PathVariable("id") String providerId) { + Long userId = getPreAuthenticationId(); + if (userId == null) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + SecondFactorProvider provider = secondFactorProviderManager.getProvider(providerId); + + if (provider == null) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + if (!(provider instanceof SecondFactorRequestProvider)) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + SecondFactorRequestProvider requestProvider = (SecondFactorRequestProvider) provider; + + requestProvider.request(userId); + } + + /** + * + * @return + */ + @PreAuthorize("authentication.authenticated") + @PutMapping("/{id}") + public SecondFactor createSecondFactor(@PathVariable("id") String providerId) { + SecondFactorProvider provider = secondFactorProviderManager.getProvider(providerId); + + if (provider == null) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + if (provider.isEnabled(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + return provider.create(getCurrentUserId()); + } + + /** + * + * @return + */ + @PreAuthorize("authentication.authenticated") + @PatchMapping("/{id}") + public void enableSecondFactor(@PathVariable("id") String providerId, + @RequestBody String token) { + SecondFactorProvider provider = secondFactorProviderManager.getProvider(providerId); + + if (provider == null) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + if (!provider.enable(getCurrentUserId(), token)) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + } + + /** + * + */ + @PreAuthorize("authentication.authenticated") + @DeleteMapping("/{id}") + public void removeSecondFactorMail(@PathVariable("id") String providerId) { + SecondFactorProvider provider = secondFactorProviderManager.getProvider(providerId); + + if (provider == null) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + provider.delete(getCurrentUserId()); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/AuthenticationController.java b/core/src/main/java/de/bstly/we/controller/AuthenticationController.java new file mode 100755 index 0000000..38d3ced --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/AuthenticationController.java @@ -0,0 +1,107 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.GetMapping; +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; + +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.controller.model.PasswordResetModel; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.controller.validation.PasswordModelValidator; +import de.bstly.we.model.User; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/auth") +public class AuthenticationController extends BaseController { + + @Autowired + private UserManager userManager; + @Autowired + private PasswordModelValidator passwordModelValidator; + + /** + * + * @return + */ + @GetMapping("/me") + public Authentication me() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + return auth; + } + + /** + * + * @param username + * @param req + * @param resp + * @throws IOException + */ + @PreAuthorize("isAnonymous()") + @PostMapping("/password/request") + public void passwordRequest(@RequestBody String username, HttpServletRequest req, + HttpServletResponse resp) throws IOException { + User user = userManager.getByUsername(username); + + if (user != null) { + resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE); + userManager.passwordReset(user, resp.getOutputStream()); + resp.getOutputStream().flush(); + resp.getOutputStream().close(); + } + } + + /** + * + * @param passwordResetModel + * @param req + * @param resp + */ + @PreAuthorize("isAnonymous()") + @PostMapping("/password/reset") + public void passwordReset(@RequestBody PasswordResetModel passwordResetModel, + HttpServletRequest req, HttpServletResponse resp) { + User user = userManager.getByResetToken(passwordResetModel.getToken().trim()); + + if (user == null) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + Errors errors = new RequestBodyErrors(passwordResetModel); + + passwordModelValidator.validate(passwordResetModel, errors); + + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + user = userManager.setPassword(user.getId(), passwordResetModel.getPassword()); + user.setResetToken(null); + userManager.update(user); + + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/BaseController.java b/core/src/main/java/de/bstly/we/controller/BaseController.java new file mode 100755 index 0000000..c96f506 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/BaseController.java @@ -0,0 +1,32 @@ +/** + * + */ +package de.bstly.we.controller; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import de.bstly.we.security.model.LocalUserDetails; + +/** + * + * @author _bastler@bstly.de + * + */ +public class BaseController { + + /** + * + * @return + */ + public Long getCurrentUserId() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof LocalUserDetails) { + LocalUserDetails details = (LocalUserDetails) auth.getPrincipal(); + return details.getUserId(); + } + + return null; + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/ItemController.java b/core/src/main/java/de/bstly/we/controller/ItemController.java new file mode 100755 index 0000000..71d5db4 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/ItemController.java @@ -0,0 +1,147 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpSession; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.gson.JsonObject; + +import de.bstly.we.businesslogic.PretixManager; +import de.bstly.we.businesslogic.PretixManager.ITEM_STATUS; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.controller.model.ItemModel; +import de.bstly.we.controller.model.ItemResultModel; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.TokenSessionManager; +import de.bstly.we.model.User; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/items") +public class ItemController extends BaseController { + + @Autowired + private TokenSessionManager tokenSessionManager; + @Autowired + private PretixManager pretixManager; + @Autowired + private UserManager userManager; + + /** + * + */ + @GetMapping + public Set getItems(HttpSession session) { + Set result = Sets.newHashSet(); + + if (tokenSessionManager.getTokenFromSession(session).isEmpty()) { + return result; + } + + for (String token : tokenSessionManager.getTokenFromSession(session)) { + JsonObject checkInItem = pretixManager.getCheckInItemBySecret(token); + JsonObject item = pretixManager.getItem(checkInItem.get("item").getAsInt()); + ItemModel itemModel = new ItemModel(); + itemModel.setSecret(token); + Map name = Maps.newHashMap(); + for (String lang : item.getAsJsonObject("name").keySet()) { + name.put(lang, item.getAsJsonObject("name").get(lang).getAsString()); + } + itemModel.setName(name); + result.add(itemModel); + } + + return result; + } + + /** + * + * @param secret + */ + @PutMapping("") + public void addItem(@RequestBody String secret, HttpSession session) { + ITEM_STATUS status = pretixManager.getItemStatus(secret); + if (status == ITEM_STATUS.PAID) { + tokenSessionManager.addTokenToSession(secret, session); + throw new EntityResponseStatusException(HttpStatus.ACCEPTED); + } else if (pretixManager.getItemStatus(secret) == ITEM_STATUS.REDEEMED) { + throw new EntityResponseStatusException(HttpStatus.GONE); + } + + throw new EntityResponseStatusException(HttpStatus.NOT_ACCEPTABLE); + } + + /** + * + * @param secret + */ + @DeleteMapping + public void removeItem(@RequestBody String secret, HttpSession session) { + tokenSessionManager.removeTokenFromSession(secret, session); + } + + /** + * + * @return + */ + @PreAuthorize("isAuthenticated()") + @PostMapping + public ItemResultModel redeem(HttpSession session) { + if (tokenSessionManager.getTokenFromSession(session).isEmpty()) { + throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED); + } + + ItemResultModel result = tokenSessionManager.applyTokens(getCurrentUserId(), + tokenSessionManager.getTokenFromSession(session)); + tokenSessionManager.removeTokensFromSession(session); + + return result; + } + + /** + * + * @param username + * @param session + */ + @PreAuthorize("isAuthenticated()") + @PostMapping("/{username}") + public void redeemForUser(@PathVariable("username") String username, HttpSession session) { + + if (tokenSessionManager.getTokenFromSession(session).isEmpty()) { + throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED); + } + + User user = userManager.getByUsername(username); + + if (user == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + tokenSessionManager.applyTokens(user.getId(), + tokenSessionManager.getTokenFromSession(session)); + tokenSessionManager.removeTokensFromSession(session); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/PermissionController.java b/core/src/main/java/de/bstly/we/controller/PermissionController.java new file mode 100644 index 0000000..1bbd261 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/PermissionController.java @@ -0,0 +1,81 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; + +import javax.servlet.http.HttpSession; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.google.common.collect.Lists; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.controller.support.TokenSessionManager; +import de.bstly.we.model.Permission; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/permissions") +public class PermissionController extends BaseController { + + @Autowired + private PermissionManager permissionManager; + @Autowired + private TokenSessionManager tokenSessionManager; + + /** + * + * @return + */ + @GetMapping + public List getPermissions() { + if (getCurrentUserId() == null) { + return Lists.newArrayList(); + } + + return permissionManager.getNotExpiresByTargetIgnoreStart(getCurrentUserId()); + } + + /** + * + * @return + */ + @GetMapping("/new") + public List getNewPermissions(HttpSession session) { + List permissions = Lists.newArrayList(); + + if (tokenSessionManager.getTokenFromSession(session).isEmpty()) { + return permissions; + } + + for (String token : tokenSessionManager.getTokenFromSession(session)) { + permissions + .addAll(tokenSessionManager.getPermissionsForToken(getCurrentUserId(), token)); + } + + return permissions; + } + + /** + * + * @return + */ + @GetMapping("/all") + public List getAllPermissions() { + if (getCurrentUserId() == null) { + return Lists.newArrayList(); + } + + return permissionManager.getAllByTarget(getCurrentUserId()); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/PermissionManagementController.java b/core/src/main/java/de/bstly/we/controller/PermissionManagementController.java new file mode 100644 index 0000000..25a9959 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/PermissionManagementController.java @@ -0,0 +1,153 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.model.Permission; +import de.bstly.we.model.User; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/permissions/manage") +public class PermissionManagementController extends BaseController { + + @Autowired + private PermissionManager permissionManager; + @Autowired + private UserManager userManager; + + /** + * + * @param username + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/{username}") + public List getPermissionsForUser(@PathVariable("username") String username) { + User user = userManager.getByUsername(username); + + if (user == null) { + throw new EntityResponseStatusException(HttpStatus.NO_CONTENT); + } + + return permissionManager.getNotExpiresByTargetIgnoreStart(user.getId()); + } + + /** + * + * @param username + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/{username}/all") + public List getAllPermissionsForUser(@PathVariable("username") String username) { + User user = userManager.getByUsername(username); + + if (user == null) { + throw new EntityResponseStatusException(HttpStatus.NO_CONTENT); + } + + return permissionManager.getAllByTarget(user.getId()); + } + + /** + * + * @param permission + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public Permission createPermission(@RequestBody Permission permission) { + return permissionManager.create(permission.getTarget(), permission.getName(), + permission.isAddon(), permission.getStarts(), permission.getExpires()); + } + + /** + * + * @param permission + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PatchMapping + public Permission updatePermission(@RequestBody Permission permission) { + if (permission.getId() == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + return permissionManager.update(permission); + } + + /** + * + * @param name + * @param clone + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/{name}/clone/{clone}") + public List clone(@PathVariable("name") String name, + @PathVariable("clone") String clone) { + if (name.equals(clone)) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + return permissionManager.clone(name, clone); + } + + /** + * + * @param permission + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping + public void deletePermission(@RequestBody Permission permission) { + if (permissionManager.get(permission.getTarget(), permission.getName()) == null) { + throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED); + + } + + permissionManager.delete(permission.getTarget(), permission.getName()); + } + + /** + * + * @param target + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/{target}") + public void deleteAll(@PathVariable("target") Long target) { + permissionManager.deleteAll(target); + } + + /** + * + * @param name + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/byname/{name}") + public void deleteAllByName(@PathVariable("name") String name) { + permissionManager.deleteAll(name); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/PermissionMappingController.java b/core/src/main/java/de/bstly/we/controller/PermissionMappingController.java new file mode 100644 index 0000000..6dcd116 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/PermissionMappingController.java @@ -0,0 +1,152 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.google.common.collect.Lists; + +import de.bstly.we.businesslogic.PermissionMappingManager; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.controller.validation.PermissionMappingValidator; +import de.bstly.we.model.PermissionMapping; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/permissions/mappings") +public class PermissionMappingController extends BaseController { + + @Autowired + private PermissionMappingManager permissionMappingManager; + @Autowired + private PermissionMappingValidator permissionMappingValidator; + + /** + * + * @param pageParameter + * @param sizeParameter + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getPermissionMappings( + @RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter) { + return permissionMappingManager.get(pageParameter.orElse(0), sizeParameter.orElse(10), "item", true); + } + + /** + * + * @param permissionMapping + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public PermissionMapping create(@RequestBody PermissionMapping permissionMapping) { + Errors errors = new RequestBodyErrors(permissionMapping); + permissionMappingValidator.validate(permissionMapping, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + return permissionMappingManager.create(permissionMapping.getItem(), + permissionMapping.getNames(), permissionMapping.getLifetime(), + permissionMapping.getLifetimeUnit(), permissionMapping.isLifetimeRound(), + permissionMapping.isAddon(), permissionMapping.getProduct(), + permissionMapping.getStartsQuestion(), permissionMapping.getExpiresQuestion()); + } + + /** + * + * @param permissionMappings + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/list") + public List createList( + @RequestBody List permissionMappings) { + List result = Lists.newArrayList(); + for (PermissionMapping permissionMapping : permissionMappings) { + Errors errors = new RequestBodyErrors(permissionMapping); + permissionMappingValidator.validate(permissionMapping, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + result.add(permissionMappingManager.create(permissionMapping.getItem(), + permissionMapping.getNames(), permissionMapping.getLifetime(), + permissionMapping.getLifetimeUnit(), permissionMapping.isLifetimeRound(), + permissionMapping.isAddon(), permissionMapping.getProduct(), + permissionMapping.getStartsQuestion(), permissionMapping.getExpiresQuestion())); + } + return result; + } + + /** + * + * @param permissionMapping + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PatchMapping + public PermissionMapping update(@RequestBody PermissionMapping permissionMapping) { + Errors errors = new RequestBodyErrors(permissionMapping); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + return permissionMappingManager.update(permissionMapping); + } + + /** + * + * @param permissionMappings + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PatchMapping("/list") + public List updateList( + @RequestBody List permissionMappings) { + List result = Lists.newArrayList(); + for (PermissionMapping permissionMapping : permissionMappings) { + Errors errors = new RequestBodyErrors(permissionMapping); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + result.add(permissionMappingManager.update(permissionMapping)); + } + return result; + } + + /** + * + * @param permissionMapping + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping + public void delete(@RequestBody PermissionMapping permissionMapping) { + permissionMappingManager.delete(permissionMapping.getId()); + } +} diff --git a/core/src/main/java/de/bstly/we/controller/PretixApiController.java b/core/src/main/java/de/bstly/we/controller/PretixApiController.java new file mode 100755 index 0000000..da85c16 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/PretixApiController.java @@ -0,0 +1,71 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.io.IOException; +import java.util.Map.Entry; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +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; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import com.google.gson.JsonIOException; + +import de.bstly.we.businesslogic.PretixManager; +import de.bstly.we.controller.model.PretixRequest; +import de.bstly.we.controller.support.EntityResponseStatusException; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/pretix") +public class PretixApiController extends BaseController { + + @Autowired + private PretixManager pretixManager; + private Gson gson = new Gson(); + + /** + * + * @param pretixRequest + * @return + * @throws IOException + * @throws JsonIOException + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/debug") + public void debug(@RequestBody PretixRequest pretixRequest, HttpServletResponse response) + throws JsonIOException, IOException { + try { + + MultiValueMap queryParemeters = new LinkedMultiValueMap(); + if (pretixRequest.getQueryParameters() != null) { + for (Entry entry : pretixRequest.getQueryParameters().entrySet()) { + queryParemeters.put(entry.getKey(), Lists.newArrayList(entry.getValue())); + } + } + + gson.toJson( + pretixManager.request(pretixRequest.getPath(), pretixRequest.getMethod(), + gson.toJsonTree(pretixRequest.getPayload()), queryParemeters), + response.getWriter()); + } catch (WebClientResponseException e) { + throw new EntityResponseStatusException(e.getMessage(), e.getStatusCode()); + } + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/QuotaController.java b/core/src/main/java/de/bstly/we/controller/QuotaController.java new file mode 100644 index 0000000..5ef7968 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/QuotaController.java @@ -0,0 +1,114 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; + +import javax.servlet.http.HttpSession; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.google.common.collect.Lists; + +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.controller.support.TokenSessionManager; +import de.bstly.we.model.Quota; +import de.bstly.we.model.QuotaMapping; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/quotas") +public class QuotaController extends BaseController { + + @Autowired + private QuotaManager quotaManager; + @Autowired + private TokenSessionManager tokenSessionManager; + + /** + * + * @return + */ + @GetMapping + public List getQuotas() { + if (getCurrentUserId() == null) { + return Lists.newArrayList(); + } + + return quotaManager.getNotExpiresByTarget(getCurrentUserId()); + } + + /** + * + * @return + */ + @GetMapping("/new") + public List getNewQuotas(HttpSession session) { + List quotas = Lists.newArrayList(); + + if (tokenSessionManager.getTokenFromSession(session).isEmpty()) { + return quotas; + } + + for (String token : tokenSessionManager.getTokenFromSession(session)) { + for (QuotaMapping quotaMapping : tokenSessionManager + .getQuotaMappingsForToken(getCurrentUserId(), token)) { + boolean added = false; + for (Quota quota : quotas) { + if (quota.getName().equals(quotaMapping.getName())) { + quota.setValue( + quotaMapping.isAppend() ? quota.getValue() + quotaMapping.getValue() + : quotaMapping.getValue()); + added = true; + } + } + + if (!added) { + if (quotaManager.hasQuota(getCurrentUserId(), quotaMapping.getName())) { + Quota quota = quotaManager.get(getCurrentUserId(), quotaMapping.getName()); + + quota.setValue( + quotaMapping.isAppend() ? quota.getValue() + quotaMapping.getValue() + : quotaMapping.getValue()); + + quotas.add(quota); + added = true; + } + if (!added) { + Quota quota = new Quota(); + quota.setName(quotaMapping.getName()); + quota.setValue(quotaMapping.getValue()); + quota.setUnit(quotaMapping.getUnit()); + quota.setDisposable(quotaMapping.isDisposable()); + quotas.add(quota); + } + } + } + + } + + return quotas; + } + + /** + * + * @return + */ + @GetMapping("/all") + public List getAllQuotas() { + if (getCurrentUserId() == null) { + return Lists.newArrayList(); + } + + return quotaManager.getAllByTarget(getCurrentUserId()); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/QuotaManagementController.java b/core/src/main/java/de/bstly/we/controller/QuotaManagementController.java new file mode 100644 index 0000000..8b18a40 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/QuotaManagementController.java @@ -0,0 +1,190 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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; + +import com.beust.jcommander.internal.Lists; + +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.model.Quota; +import de.bstly.we.model.User; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/quotas/manage") +public class QuotaManagementController extends BaseController { + + @Autowired + private QuotaManager quotaManager; + @Autowired + private UserManager userManager; + + /** + * + * @param username + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/{username}") + public List getQuotasForUser(@PathVariable("username") String username) { + User user = userManager.getByUsername(username); + + if (user == null) { + throw new EntityResponseStatusException(HttpStatus.NO_CONTENT); + } + + return quotaManager.getNotExpiresByTarget(user.getId()); + } + + /** + * + * @param username + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/{username}/all") + public List getAllQuotasForUser(@PathVariable("username") String username) { + User user = userManager.getByUsername(username); + + if (user == null) { + throw new EntityResponseStatusException(HttpStatus.NO_CONTENT); + } + + return quotaManager.getAllByTarget(user.getId()); + } + + /** + * + * @param name + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/byname/{name}") + public List getQuotasByName(@PathVariable("name") String name) { + return quotaManager.getAllByName(name); + } + + /** + * + * @param quota + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public Quota createQuota(@RequestBody Quota quota) { + if (quotaManager.get(quota.getTarget(), quota.getName()) != null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + return quotaManager.create(quota.getTarget(), quota.getName(), quota.getValue(), + quota.getUnit(), quota.isDisposable()); + } + + /** + * + * @param quota + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PatchMapping + public Quota updateQuota(@RequestBody Quota quota) { + if (quotaManager.get(quota.getTarget(), quota.getName()) == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + return quotaManager.update(quota); + } + + /** + * + * @param quotas + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PatchMapping("/list") + public List updateQuotaList(@RequestBody List quotas) { + + List result = Lists.newArrayList(); + + for (Quota quota : quotas) { + if (quotaManager.get(quota.getTarget(), quota.getName()) == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + result.add(quotaManager.update(quota)); + } + + return result; + } + + /** + * + * @param name + * @param clone + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/{name}/clone/{clone}") + public List clone(@PathVariable("name") String name, @PathVariable("clone") String clone, + @RequestBody long value) { + if (name.equals(clone)) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + return quotaManager.clone(name, clone, value); + } + + /** + * + * @param quota + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping + public void deleteQuota(@RequestBody Quota quota) { + if (quotaManager.get(quota.getTarget(), quota.getName()) == null) { + throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED); + } + + quotaManager.delete(quota.getTarget(), quota.getName()); + } + + /** + * + * @param target + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/{target}") + public void deleteAll(@PathVariable("target") Long target) { + quotaManager.deleteAll(target); + } + + /** + * + * @param name + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/byname/{name}") + public void deleteAllByName(@PathVariable("name") String name) { + quotaManager.deleteAll(name); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/QuotaMappingController.java b/core/src/main/java/de/bstly/we/controller/QuotaMappingController.java new file mode 100644 index 0000000..3e7bde0 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/QuotaMappingController.java @@ -0,0 +1,136 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.google.common.collect.Lists; + +import de.bstly.we.businesslogic.QuotaMappingManager; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.controller.validation.QuotaMappingValidator; +import de.bstly.we.model.QuotaMapping; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/quotas/mappings") +public class QuotaMappingController extends BaseController { + + @Autowired + private QuotaMappingManager quotaMappingManager; + @Autowired + private QuotaMappingValidator quotaMappingValidator; + + /** + * + * @param pageParameter + * @param sizeParameter + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getQuotaMappings( + @RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter) { + return quotaMappingManager.get(pageParameter.orElse(0), sizeParameter.orElse(10), "name", + true); + } + + /** + * + * @param userModel + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public QuotaMapping create(@RequestBody QuotaMapping quotaMapping) { + + Errors errors = new RequestBodyErrors(quotaMapping); + + quotaMappingValidator.validate(quotaMapping, errors); + + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + return quotaMappingManager.create(quotaMapping.getItems(), quotaMapping.getName(), + quotaMapping.getValue(), quotaMapping.getUnit(), quotaMapping.isAppend(), + quotaMapping.getProducts(), quotaMapping.isDisposable()); + } + + /** + * + * @param userModel + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/list") + public List createList(@RequestBody List quotaMappings) { + List result = Lists.newArrayList(); + + for (QuotaMapping quotaMapping : quotaMappings) { + Errors errors = new RequestBodyErrors(quotaMapping); + + quotaMappingValidator.validate(quotaMapping, errors); + + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + result.add(quotaMappingManager.create(quotaMapping.getItems(), quotaMapping.getName(), + quotaMapping.getValue(), quotaMapping.getUnit(), quotaMapping.isAppend(), + quotaMapping.getProducts(), quotaMapping.isDisposable())); + } + return result; + } + + /** + * + * @param userModel + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PatchMapping + public QuotaMapping update(@RequestBody QuotaMapping quotaMapping) { + + Errors errors = new RequestBodyErrors(quotaMapping); + + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + return quotaMappingManager.update(quotaMapping); + } + + /** + * + * @param userModel + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping + public void delete(@RequestBody QuotaMapping quotaMapping) { + quotaMappingManager.delete(quotaMapping.getId()); + } +} diff --git a/core/src/main/java/de/bstly/we/controller/SystemController.java b/core/src/main/java/de/bstly/we/controller/SystemController.java new file mode 100644 index 0000000..793c5e8 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/SystemController.java @@ -0,0 +1,107 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.businesslogic.PretixManager; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.model.SystemProperty; +import de.bstly.we.repository.SystemPropertyRepository; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/system") +public class SystemController extends BaseController { + + @Autowired + private SystemPropertyRepository systemPropertyRepository; + @Autowired + private PretixManager pretixManager; + + /** + * + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/pretix") + public void updatePretixClient() { + pretixManager.buildWebClient(); + } + + /** + * + * @param pageParameter + * @param sizeParameter + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/properties") + public List getProperties(@RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter) { + Sort sort = Sort.by("key").ascending(); + return systemPropertyRepository + .findAll(PageRequest.of(pageParameter.orElse(0), sizeParameter.orElse(10), sort)) + .getContent(); + } + + /** + * + * @param key + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/properties/{key}") + public SystemProperty getProperty(@PathVariable("key") String key) { + if (!systemPropertyRepository.existsById(key)) { + throw new EntityResponseStatusException(HttpStatus.NO_CONTENT); + } + + return systemPropertyRepository.findById(key).get(); + } + + /** + * + * @param systemProperty + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/properties") + public SystemProperty createOrUpdate(@RequestBody SystemProperty systemProperty) { + return systemPropertyRepository.save(systemProperty); + } + + /** + * + * @param key + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/properties/{key}") + public void deleteProperty(@PathVariable("key") String key) { + if (!systemPropertyRepository.existsById(key)) { + throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED); + } + + systemPropertyRepository.deleteById(key); + } +} diff --git a/core/src/main/java/de/bstly/we/controller/SystemProfileFieldController.java b/core/src/main/java/de/bstly/we/controller/SystemProfileFieldController.java new file mode 100644 index 0000000..34f16d1 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/SystemProfileFieldController.java @@ -0,0 +1,115 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.beust.jcommander.internal.Lists; + +import de.bstly.we.businesslogic.SystemProfileFieldManager; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.model.SystemProfileField; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/profiles/system") +public class SystemProfileFieldController extends BaseController { + + @Autowired + private SystemProfileFieldManager systemProfileFieldManager; + + /** + * + * @param pageParameter + * @param sizeParameter + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page get(@RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter) { + return systemProfileFieldManager.get(pageParameter.orElse(0), sizeParameter.orElse(10), + "name", true); + } + + /** + * + * @param name + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/{name}") + public SystemProfileField getByName(@PathVariable("name") String name) { + SystemProfileField systemProfileField = systemProfileFieldManager.get(name); + + if (systemProfileField == null) { + throw new EntityResponseStatusException(HttpStatus.NO_CONTENT); + } + + return systemProfileField; + } + + /** + * + * @param systemProfileField + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public SystemProfileField update(@RequestBody SystemProfileField systemProfileField) { + return systemProfileFieldManager.save(systemProfileField); + } + + /** + * + * @param systemProfileField + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/list") + public List updateList( + @RequestBody List systemProfileFields) { + List result = Lists.newArrayList(); + + for (SystemProfileField systemProfileField : systemProfileFields) { + result.add(systemProfileFieldManager.save(systemProfileField)); + } + + return result; + } + + /** + * + * @param name + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/{name}") + public void deleteByName(@PathVariable("name") String name) { + SystemProfileField systemProfileField = systemProfileFieldManager.get(name); + + if (systemProfileField == null) { + throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED); + } + + systemProfileFieldManager.delete(name); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/UserAliasController.java b/core/src/main/java/de/bstly/we/controller/UserAliasController.java new file mode 100644 index 0000000..f151a9a --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/UserAliasController.java @@ -0,0 +1,141 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.businesslogic.Quotas; +import de.bstly.we.businesslogic.UserAliasManager; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.controller.validation.UserAliasValidator; +import de.bstly.we.model.Quota; +import de.bstly.we.model.UserAlias; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/users/aliases") +public class UserAliasController extends BaseController { + + @Autowired + private QuotaManager quotaManager; + @Autowired + private UserAliasManager userAliasManager; + @Autowired + private UserAliasValidator userAliasValidator; + @Autowired + private PermissionManager permissionManager; + + /** + * + * @param alias + * @return + */ + @PreAuthorize("isAuthenticated()") + @PostMapping + public UserAlias createAlias(@RequestBody UserAlias userAlias) { + if (!permissionManager.isFullUser(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + Quota aliasCreation = quotaManager.get(getCurrentUserId(), Quotas.ALIAS_CREATION); + if (aliasCreation == null || aliasCreation.getValue() < 1) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + userAlias.setTarget(getCurrentUserId()); + + Errors errors = new RequestBodyErrors(userAlias); + + userAliasValidator.validate(userAlias, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + aliasCreation.setValue(aliasCreation.getValue() - 1); + quotaManager.update(aliasCreation); + + return userAliasManager.save(userAlias); + } + + /** + * + * @param alias + * @return + */ + @PreAuthorize("isAuthenticated()") + @PatchMapping + public UserAlias updateAlias(@RequestBody UserAlias userAlias) { + if (userAlias.getId() == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + UserAlias oldAlias = userAliasManager.get(userAlias.getId()); + + if (oldAlias == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + if (!oldAlias.getTarget().equals(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + oldAlias.setVisibility(userAlias.getVisibility()); + + return userAliasManager.save(oldAlias); + } + + /** + * + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping + public List getAliases() { + return userAliasManager.getAllByTarget(getCurrentUserId()); + } + + /** + * + * @param id + */ + @PreAuthorize("isAuthenticated()") + @DeleteMapping("/{id}") + public void deleteAlias(@PathVariable("id") Long id) { + UserAlias userAlias = userAliasManager.get(id); + if (userAlias == null || !userAlias.getTarget().equals(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + Quota aliasCreation = quotaManager.get(getCurrentUserId(), Quotas.ALIAS_CREATION); + if (aliasCreation == null) { + aliasCreation = quotaManager.create(getCurrentUserId(), Quotas.ALIAS_CREATION, 0, "#", + true); + } + + aliasCreation.setValue(aliasCreation.getValue() + 1); + quotaManager.update(aliasCreation); + + userAliasManager.delete(id); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/UserAliasManagementController.java b/core/src/main/java/de/bstly/we/controller/UserAliasManagementController.java new file mode 100644 index 0000000..3bc4d99 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/UserAliasManagementController.java @@ -0,0 +1,105 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.businesslogic.UserAliasManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.controller.validation.UserAliasValidator; +import de.bstly.we.model.User; +import de.bstly.we.model.UserAlias; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/users/aliases/manage") +public class UserAliasManagementController extends BaseController { + + @Autowired + private UserManager userManager; + @Autowired + private UserAliasManager userAliasManager; + @Autowired + private UserAliasValidator userAliasValidator; + + /** + * + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getAliases(@RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter) { + return userAliasManager.get(pageParameter.orElse(0), sizeParameter.orElse(10), "id", true); + } + + /** + * + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/{username}") + public List getAliasesForUser(@PathVariable("username") String username) { + User user = userManager.getByUsername(username); + + if (user == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + return userAliasManager.getAllByTarget(user.getId()); + } + + /** + * + * @param alias + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public UserAlias createOrUpdateAlias(@RequestBody UserAlias userAlias) { + Errors errors = new RequestBodyErrors(userAlias); + + userAliasValidator.validate(userAlias, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + return userAliasManager.save(userAlias); + } + + /** + * + * @param id + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/{id}") + public void deleteAlias(@PathVariable("id") Long id) { + UserAlias userAlias = userAliasManager.get(id); + if (userAlias == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + userAliasManager.delete(id); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/UserController.java b/core/src/main/java/de/bstly/we/controller/UserController.java new file mode 100755 index 0000000..0a5a903 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/UserController.java @@ -0,0 +1,257 @@ +/** + * + */ +package de.bstly.we.controller; + +import javax.servlet.http.HttpSession; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +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; + +import com.beust.jcommander.internal.Lists; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.PretixManager; +import de.bstly.we.businesslogic.PretixManager.ITEM_STATUS; +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.businesslogic.UserProfileFieldManager; +import de.bstly.we.controller.model.ItemResultModel; +import de.bstly.we.controller.model.PasswordModel; +import de.bstly.we.controller.model.UserModel; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.controller.support.TokenSessionManager; +import de.bstly.we.controller.validation.PasswordModelValidator; +import de.bstly.we.controller.validation.UserModelValidator; +import de.bstly.we.controller.validation.UserProfileFieldValidator; +import de.bstly.we.model.PermissionMapping; +import de.bstly.we.model.ProfileFieldType; +import de.bstly.we.model.User; +import de.bstly.we.model.UserProfileField; +import de.bstly.we.model.Visibility; +import de.bstly.we.model.UserStatus; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/users") +public class UserController extends BaseController { + + @Autowired + private UserManager userManager; + @Autowired + private UserProfileFieldManager userProfileFieldManager; + @Autowired + private PermissionManager permissionManager; + @Autowired + private QuotaManager quotaManager; + @Autowired + private PasswordModelValidator passwordModelValidator; + @Autowired + private UserModelValidator userModelValidator; + @Autowired + private UserProfileFieldValidator userProfileFieldValidator; + @Autowired + private TokenSessionManager tokenSessionManager; + @Autowired + private PretixManager pretixManager; + @Autowired + private PasswordEncoder passwordEncoder; + + /** + * + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping + public UserModel get() { + User user = userManager.get(getCurrentUserId()); + UserModel userModel = new UserModel(); + userModel.setUsername(user.getUsername()); + userModel.setStatus(user.getStatus()); + return userModel; + } + + /** + * + * @param userModel + * @param session + * @return + */ + @PostMapping("/model") + public UserModel checkModel(@RequestBody UserModel userModel, HttpSession session) { + Errors errors = new RequestBodyErrors(userModel); + + userModelValidator.validate(userModel, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + return userModel; + } + + /** + * + * @param userModel + * @param session + * @return + */ + @PreAuthorize("isAnonymous()") + @PostMapping + public UserModel register(@RequestBody UserModel userModel, HttpSession session) { + Errors errors = new RequestBodyErrors(userModel); + + if (tokenSessionManager.getTokenFromSession(session).isEmpty()) { + if (StringUtils.hasText(userModel.getToken())) { + String secret = userModel.getToken(); + ITEM_STATUS status = pretixManager.getItemStatus(secret); + if (status == ITEM_STATUS.PAID) { + tokenSessionManager.addTokenToSession(secret, session); + } else if (pretixManager.getItemStatus(secret) == ITEM_STATUS.REDEEMED) { + throw new EntityResponseStatusException(HttpStatus.GONE); + } + } else { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + } + + boolean register = false; + + for (String token : tokenSessionManager.getTokenFromSession(session)) { + for (PermissionMapping permissionMapping : tokenSessionManager + .getPermissionMappingsForToken(getCurrentUserId(), token)) { + if (!permissionMapping.isAddon()) { + register = true; + break; + } + } + } + + if (!register) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + userModelValidator.validate(userModel, errors); + passwordModelValidator.validate(userModel, errors); + + if (userModel.getProfileFields() == null) { + userModel.setProfileFields(Lists.newArrayList()); + } + + if (userModel.getStatus() == null) { + userModel.setStatus(UserStatus.NORMAL); + } + + for (UserProfileField userProfileField : userModel.getProfileFields()) { + userProfileFieldValidator.validate(userProfileField, errors); + } + + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + User user = userManager.create(userModel.getUsername(), userModel.getPassword(), + userModel.getStatus()); + + for (UserProfileField userProfileField : userModel.getProfileFields()) { + userProfileField.setTarget(user.getId()); + if (userProfileField.getType() == null) { + userProfileField.setType(ProfileFieldType.TEXT); + } + + if (userProfileField.getVisibility() == null) { + userProfileField.setVisibility(Visibility.PRIVATE); + } + + userProfileField = userProfileFieldManager.save(userProfileField); + } + + UserModel responseModel = new UserModel(); + responseModel.setUsername(user.getUsername()); + responseModel.setStatus(user.getStatus()); + ItemResultModel result = tokenSessionManager.applyTokens(user.getId(), + tokenSessionManager.getTokenFromSession(session)); + responseModel.setPermissionMappings(result.getPermissionMappings()); + responseModel.setPermissions(permissionManager.getNotExpiresByTarget(user.getId())); + responseModel.setQuotaMappings(result.getQuotaMappings()); + responseModel.setQuotas(quotaManager.getNotExpiresByTarget(user.getId())); + responseModel.setProfileFields(userProfileFieldManager.getAllByTarget(user.getId())); + + tokenSessionManager.removeTokensFromSession(session); + + return responseModel; + } + + /** + * + * @param passwordModel + */ + @PreAuthorize("isAuthenticated()") + @PatchMapping("/password") + public void changePassword(@RequestBody PasswordModel passwordModel) { + + Errors errors = new RequestBodyErrors(passwordModel); + + User user = userManager.get(getCurrentUserId()); + + if (!StringUtils.hasText(passwordModel.getOld()) || !passwordEncoder + .matches(passwordModel.getOld(), userManager.getPasswordHash(user.getId()))) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + passwordModelValidator.validate(passwordModel, errors); + + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + userManager.setPassword(user.getId(), passwordModel.getPassword()); + } + + /** + * + * @param userModel + */ + @PreAuthorize("isAuthenticated()") + @PatchMapping + public void update(@RequestBody UserModel userModel) { + User user = userManager.get(getCurrentUserId()); + if (StringUtils.hasText(userModel.getOld())) { + Errors errors = new RequestBodyErrors(userModel); + if (!passwordEncoder.matches(userModel.getOld(), + userManager.getPasswordHash(getCurrentUserId()))) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + passwordModelValidator.validate(userModel, errors); + + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + user = userManager.setPassword(user.getId(), userModel.getPassword()); + } + + if (userModel.getStatus() != null) { + user.setStatus(userModel.getStatus()); + } + + user = userManager.update(user); + + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/UserDomainController.java b/core/src/main/java/de/bstly/we/controller/UserDomainController.java new file mode 100644 index 0000000..539fa2c --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/UserDomainController.java @@ -0,0 +1,129 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.UserDomainManager; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.controller.validation.UserDomainValidator; +import de.bstly.we.model.UserDomain; +import de.bstly.we.model.Visibility; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/users/domains") +public class UserDomainController extends BaseController { + + @Autowired + private UserDomainManager userDomainManager; + @Autowired + private UserDomainValidator userDomainValidator; + @Autowired + private PermissionManager permissionManager; + + /** + * + * @param userDomain + * @return + */ + @PreAuthorize("isAuthenticated()") + @PostMapping + public UserDomain createDomain(@RequestBody UserDomain userDomain) { + if (!permissionManager.isFullUser(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + + if (userDomain.getVisibility() == null) { + userDomain.setVisibility(Visibility.PRIVATE); + } + + userDomain.setTarget(getCurrentUserId()); + userDomain.setValidated(false); + userDomain.setSecret( + RandomStringUtils.random(UserDomainManager.DEFAULT_SECRET_LENGTH, true, true)); + + Errors errors = new RequestBodyErrors(userDomain); + + userDomainValidator.validate(userDomain, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + return userDomainManager.save(userDomain); + } + + /** + * + * @param userDomain + * @return + */ + @PreAuthorize("isAuthenticated()") + @PatchMapping + public UserDomain updateDomain(@RequestBody UserDomain userDomain) { + if (userDomain.getId() == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + UserDomain oldDomain = userDomainManager.get(userDomain.getId()); + + if (oldDomain == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + if (!oldDomain.getTarget().equals(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + oldDomain.setVisibility(userDomain.getVisibility()); + + return userDomainManager.save(oldDomain); + } + + /** + * + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping + public List getDomains() { + return userDomainManager.getAllByTarget(getCurrentUserId()); + } + + /** + * + * @param id + */ + @PreAuthorize("isAuthenticated()") + @DeleteMapping("/{id}") + public void deleteDomain(@PathVariable("id") Long id) { + UserDomain userDomain = userDomainManager.get(id); + if (userDomain == null || !userDomain.getTarget().equals(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + userDomainManager.delete(id); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/UserDomainManagementController.java b/core/src/main/java/de/bstly/we/controller/UserDomainManagementController.java new file mode 100644 index 0000000..7c6256f --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/UserDomainManagementController.java @@ -0,0 +1,142 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; +import java.util.Optional; + +import javax.naming.NamingException; + +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.businesslogic.UserDomainManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.controller.validation.UserDomainValidator; +import de.bstly.we.model.User; +import de.bstly.we.model.UserDomain; +import de.bstly.we.model.Visibility; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/users/domains/manage") +public class UserDomainManagementController extends BaseController { + + @Autowired + private UserManager userManager; + @Autowired + private UserDomainManager userDomainManager; + @Autowired + private UserDomainValidator userDomainValidator; + + /** + * + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getDomains(@RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter) { + return userDomainManager.get(pageParameter.orElse(0), sizeParameter.orElse(10), "id", true); + } + + /** + * + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/{username}") + public List getDomainsForUser(@PathVariable("username") String username) { + User user = userManager.getByUsername(username); + + if (user == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + return userDomainManager.getAllByTarget(user.getId()); + } + + /** + * + * @param alias + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public UserDomain createOrUpdateDomain(@RequestBody UserDomain userDomain) { + + if (userDomain.getVisibility() == null) { + userDomain.setVisibility(Visibility.PRIVATE); + } + + Errors errors = new RequestBodyErrors(userDomain); + userDomainValidator.validate(userDomain, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + if (userDomain.getId() == null) { + userDomain.setValidated(false); + userDomain.setSecret( + RandomStringUtils.random(UserDomainManager.DEFAULT_SECRET_LENGTH, true, true)); + } + + return userDomainManager.save(userDomain); + } + + /** + * + * @param id + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/{id}") + public void deleteDomain(@PathVariable("id") Long id) { + UserDomain userDomain = userDomainManager.get(id); + if (userDomain == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + userDomainManager.delete(id); + } + + /** + * + * @param id + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/validate/{id}") + public void validate(@PathVariable("id") Long id) { + UserDomain userDomain = userDomainManager.get(id); + if (userDomain == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + try { + if (userDomainManager.validate(userDomain)) { + throw new EntityResponseStatusException(HttpStatus.ACCEPTED); + } + } catch (NamingException e) { + e.printStackTrace(); + } + + throw new EntityResponseStatusException(HttpStatus.NOT_ACCEPTABLE); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/UserManagementController.java b/core/src/main/java/de/bstly/we/controller/UserManagementController.java new file mode 100644 index 0000000..6a76848 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/UserManagementController.java @@ -0,0 +1,229 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.time.Instant; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.beust.jcommander.internal.Lists; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.businesslogic.UserDataManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.businesslogic.UserProfileFieldManager; +import de.bstly.we.businesslogic.support.InstantHelper; +import de.bstly.we.controller.model.UserModel; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.controller.validation.PasswordModelValidator; +import de.bstly.we.controller.validation.UserModelValidator; +import de.bstly.we.controller.validation.UserProfileFieldValidator; +import de.bstly.we.model.Permission; +import de.bstly.we.model.PermissionMapping; +import de.bstly.we.model.ProfileFieldType; +import de.bstly.we.model.Quota; +import de.bstly.we.model.QuotaMapping; +import de.bstly.we.model.User; +import de.bstly.we.model.UserProfileField; +import de.bstly.we.model.UserStatus; +import de.bstly.we.model.Visibility; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/users/manage") +public class UserManagementController extends BaseController { + + @Autowired + private UserManager userManager; + @Autowired + private PermissionManager permissionManager; + @Autowired + private QuotaManager quotaManager; + @Autowired + private UserProfileFieldManager userProfileFieldManager; + @Autowired + private PasswordModelValidator passwordModelValidator; + @Autowired + private UserModelValidator userModelValidator; + @Autowired + private UserProfileFieldValidator userProfileFieldValidator; + @Autowired + private UserDataManager userDataManager; + + /** + * + * @param pageParameter + * @param sizeParameter + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getUsers(@RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter) { + return userManager.get(pageParameter.orElse(0), sizeParameter.orElse(10), "username", true); + } + + /** + * + * @param username + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/{username}") + public User getUserByUsername(@PathVariable("username") String username) { + User user = userManager.getByUsername(username); + + if (user == null) { + throw new EntityResponseStatusException(HttpStatus.NO_CONTENT); + } + + return user; + } + + /** + * + * @param userModel + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public User create(@RequestBody UserModel userModel) { + Errors errors = new RequestBodyErrors(userModel); + userModelValidator.validate(userModel, errors); + passwordModelValidator.validate(userModel, errors); + + if (userModel.getStatus() == null) { + userModel.setStatus(UserStatus.SLEEP); + } + + if (userModel.getProfileFields() == null) { + userModel.setProfileFields(Lists.newArrayList()); + } + + for (UserProfileField userProfileField : userModel.getProfileFields()) { + userProfileFieldValidator.validate(userProfileField, errors); + } + + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + User user = userManager.create(userModel.getUsername(), userModel.getPassword(), + userModel.getStatus()); + + if (userModel.getPermissionMappings() != null) { + for (PermissionMapping permissionMapping : userModel.getPermissionMappings()) { + for (String name : permissionMapping.getNames()) { + permissionManager.create(user.getId(), name, permissionMapping.isAddon(), null, + InstantHelper.plus(Instant.now(), permissionMapping.getLifetime(), + permissionMapping.getLifetimeUnit())); + } + } + } + + if (userModel.getPermissions() != null) { + for (Permission permission : userModel.getPermissions()) { + permissionManager.create(user.getId(), permission.getName(), permission.isAddon(), + permission.getStarts(), permission.getExpires()); + } + } + + if (userModel.getQuotaMappings() != null) { + for (QuotaMapping quotaMapping : userModel.getQuotaMappings()) { + quotaManager.create(user.getId(), quotaMapping.getName(), quotaMapping.getValue(), + quotaMapping.getUnit(), quotaMapping.isDisposable()); + } + } + + if (userModel.getQuotas() != null) { + for (Quota quota : userModel.getQuotas()) { + quotaManager.create(user.getId(), quota.getName(), quota.getValue(), + quota.getUnit(), quota.isDisposable()); + } + } + + for (UserProfileField userProfileField : userModel.getProfileFields()) { + userProfileField.setTarget(user.getId()); + if (userProfileField.getType() == null) { + userProfileField.setType(ProfileFieldType.TEXT); + } + + if (userProfileField.getVisibility() == null) { + userProfileField.setVisibility(Visibility.PRIVATE); + } + + userProfileField = userProfileFieldManager.save(userProfileField); + } + + return user; + } + + /** + * + * @param user + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PatchMapping + public User update(@RequestBody User user) { + return userManager.update(user); + } + + /** + * + * @param username + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/{username}") + public void deleteUserByUsername(@PathVariable("username") String username) { + User user = userManager.getByUsername(username); + + if (user == null) { + throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED); + } + + userManager.delete(user); + } + + /** + * + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/purge") + public void purge(@RequestParam("username") Optional username, + @RequestParam("dry") Optional dry) { + + if (username.isPresent()) { + User user = userManager.getByUsername(username.get()); + + if (user == null) { + throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED); + } + + userDataManager.purge(user, dry.isPresent() && dry.get()); + } else { + userDataManager.purge(); + } + } +} diff --git a/core/src/main/java/de/bstly/we/controller/UserProfileFieldController.java b/core/src/main/java/de/bstly/we/controller/UserProfileFieldController.java new file mode 100644 index 0000000..512c2ad --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/UserProfileFieldController.java @@ -0,0 +1,284 @@ +/** + * + */ +package de.bstly.we.controller; + +import java.util.List; +import java.util.Optional; + +import org.apache.commons.lang3.RandomUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.beust.jcommander.internal.Lists; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.UserAliasManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.businesslogic.UserProfileFieldManager; +import de.bstly.we.controller.model.ProfileModel; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.controller.validation.UserProfileFieldValidator; +import de.bstly.we.model.ProfileFieldType; +import de.bstly.we.model.User; +import de.bstly.we.model.UserAlias; +import de.bstly.we.model.UserProfileField; +import de.bstly.we.model.Visibility; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/profiles") +public class UserProfileFieldController extends BaseController { + + @Autowired + private UserProfileFieldManager userProfileFieldManager; + @Autowired + private UserManager userManager; + @Autowired + private UserAliasManager userAliasManager; + @Autowired + private UserProfileFieldValidator userProfileFieldModelValidator; + @Autowired + private PermissionManager permissionManager; + + /** + * + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping + public List get(@RequestParam("filter") Optional filter) { + if (filter.isEmpty()) { + return userProfileFieldManager.getAllByTarget(getCurrentUserId()); + } + + return userProfileFieldManager.getByTargetFiltered(getCurrentUserId(), + Lists.newArrayList(filter.get().split(","))); + } + + /** + * + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping("/field/{name}") + public UserProfileField getField(@PathVariable("name") String name) { + UserProfileField userProfileField = userProfileFieldManager.get(getCurrentUserId(), name); + + if (userProfileField == null) { + throw new EntityResponseStatusException(HttpStatus.NO_CONTENT); + } + + return userProfileField; + } + + /** + * + * @return + */ + @GetMapping("/{username}") + public ProfileModel getForUser(@PathVariable("username") String username) { + + Long currentUserId = getCurrentUserId(); + User user = userManager.getByUsername(username); + + if (user == null) { + UserAlias userAlias = userAliasManager.getByAlias(username); + if (userAlias != null) { + switch (userAlias.getVisibility()) { + case PRIVATE: + if (currentUserId != null && currentUserId.equals(userAlias.getTarget())) { + user = userManager.get(userAlias.getTarget()); + } + break; + case PROTECTED: + if (currentUserId != null && permissionManager.isFullUser(getCurrentUserId())) { + user = userManager.get(userAlias.getTarget()); + } + break; + case PUBLIC: + user = userManager.get(userAlias.getTarget()); + break; + } + } + } + + if (user == null) { + throttleForbidden(); + } + + List profileFields = Lists.newArrayList(); + + List visibilities = Lists.newArrayList(Visibility.PUBLIC); + + if (currentUserId != null) { + visibilities.add(Visibility.PROTECTED); + if (currentUserId.equals(user.getId())) { + visibilities.add(Visibility.PRIVATE); + } + + } + + profileFields.addAll( + userProfileFieldManager.getAllByTargetAndVisibilities(user.getId(), visibilities)); + + if (profileFields.isEmpty()) { + throttleForbidden(); + } + + List aliases = Lists.newArrayList(); + + for (UserAlias userAlias : userAliasManager.getAllByTarget(user.getId())) { + switch (userAlias.getVisibility()) { + case PRIVATE: + if (currentUserId != null && currentUserId.equals(user.getId())) { + aliases.add(userAlias.getAlias()); + } + break; + case PROTECTED: + if (currentUserId != null) { + aliases.add(userAlias.getAlias()); + } + break; + case PUBLIC: + aliases.add(userAlias.getAlias()); + break; + } + } + + ProfileModel profileModel = new ProfileModel(); + + profileModel.setUsername(user.getUsername()); + profileModel.setProfileFields(profileFields); + profileModel.setAliases(aliases); + + return profileModel; + } + + /** + * + * @return + */ + @GetMapping("/{username}/field/{name}") + public UserProfileField getFieldForUser(@PathVariable("username") String username, + @PathVariable("name") String name) { + + User user = userManager.getByUsername(username); + + if (user == null) { + throttleForbidden(); + } + + UserProfileField userProfileField = userProfileFieldManager.get(user.getId(), name); + + if (userProfileField == null) { + throttleForbidden(); + } + + Long currentUserId = getCurrentUserId(); + + if (currentUserId == null && !Visibility.PUBLIC.equals(userProfileField.getVisibility())) { + throttleForbidden(); + } + + if (currentUserId != null && !currentUserId.equals(user.getId()) + && Visibility.PRIVATE.equals(userProfileField.getVisibility())) { + throttleForbidden(); + } + + return userProfileField; + } + + /** + * + * @param userProfileField + * @return + */ + @PreAuthorize("isAuthenticated()") + @PostMapping + public UserProfileField createOrupdate(@RequestBody UserProfileField userProfileField) { + + UserProfileField oldUserProfileField = userProfileFieldManager.get(getCurrentUserId(), + userProfileField.getName()); + + userProfileField.setTarget(getCurrentUserId()); + + Errors errors = new RequestBodyErrors(userProfileField); + + userProfileFieldModelValidator.validate(userProfileField, errors); + + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + if (oldUserProfileField == null) { + if (userProfileField.getType() == null) { + userProfileField.setType(ProfileFieldType.TEXT); + } + + if (userProfileField.getVisibility() == null) { + userProfileField.setVisibility(Visibility.PRIVATE); + } + + return userProfileFieldManager.save(userProfileField); + } else { + oldUserProfileField.setValue(userProfileField.getValue()); + oldUserProfileField.setBlob(userProfileField.getBlob()); + + if (userProfileField.getType() != null) { + oldUserProfileField.setType(userProfileField.getType()); + } + + if (userProfileField.getVisibility() != null) { + oldUserProfileField.setVisibility(userProfileField.getVisibility()); + } + + if (userProfileField.getIndex() != null) { + oldUserProfileField.setIndex(userProfileField.getIndex()); + } + + return userProfileFieldManager.save(oldUserProfileField); + } + + } + + /** + * + * @param name + */ + @PreAuthorize("isAuthenticated()") + @DeleteMapping("/{name}") + public void delete(@PathVariable("name") String name) { + if (userProfileFieldManager.get(getCurrentUserId(), name) == null) { + throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED); + } + + userProfileFieldManager.delete(getCurrentUserId(), name); + } + + /** + * + */ + protected void throttleForbidden() { + try { + Thread.sleep(RandomUtils.nextInt(10, 500)); + } catch (InterruptedException e) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } +} diff --git a/core/src/main/java/de/bstly/we/controller/VoucherController.java b/core/src/main/java/de/bstly/we/controller/VoucherController.java new file mode 100644 index 0000000..7551677 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/VoucherController.java @@ -0,0 +1,75 @@ +/** + * + */ +package de.bstly.we.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.PretixManager; +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.businesslogic.Quotas; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.model.Quota; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/vouchers") +public class VoucherController extends BaseController { + + @Autowired + private QuotaManager quotaManager; + @Autowired + private PretixManager pretixManager; + @Autowired + private PermissionManager permissionManager; + + /** + * + * @return + */ + @PreAuthorize("isAuthenticated()") + @PostMapping("/registration") + public String getRegistrationVoucher() { + Quota registrationVouchers = quotaManager.get(getCurrentUserId(), + Quotas.REGISTRATION_VOUCHERS); + if (registrationVouchers == null || registrationVouchers.getValue() < 1) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + if (!permissionManager.isFullUser(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + String result = pretixManager.createRegistrationVoucher().get("code").getAsString(); + + registrationVouchers.setValue(registrationVouchers.getValue() - 1); + quotaManager.update(registrationVouchers); + + return result; + } + + /** + * + * @return + */ + @PreAuthorize("isAuthenticated()") + @PostMapping("/addon") + public String getAddonVoucher() { + if (!permissionManager.isFullUser(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + return pretixManager.createAddOnVoucher().get("code").getAsString(); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/debug/MigrationController.java b/core/src/main/java/de/bstly/we/controller/debug/MigrationController.java new file mode 100644 index 0000000..09baff3 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/debug/MigrationController.java @@ -0,0 +1,64 @@ +/** + * + */ +package de.bstly.we.controller.debug; + +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.Session; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.model.User; +import de.bstly.we.repository.UserRepository; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/migration") +public class MigrationController extends BaseController { + + @Autowired + private UserRepository userRepository; + @Autowired + private FindByIndexNameSessionRepository sessionRepository; + + /** + * + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/sessions") + public void deleteAllSessions() { + Pageable pageable = PageRequest.of(0, 100, Sort.by("id")); + Page page; + do { + page = userRepository.findAll(pageable); + for (User user : page.getContent()) { + try { + Map usersSessions = sessionRepository + .findByPrincipalName(user.getUsername()); + for (Session session : usersSessions.values()) { + sessionRepository.deleteById(session.getId()); + + } + } catch (Exception e) { + e.printStackTrace(); + } + + } + pageable = page.nextPageable(); + } while (page.hasNext()); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/model/ItemModel.java b/core/src/main/java/de/bstly/we/controller/model/ItemModel.java new file mode 100644 index 0000000..fa02541 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/model/ItemModel.java @@ -0,0 +1,45 @@ +/** + * + */ +package de.bstly.we.controller.model; + +import java.util.Map; + +/** + * @author _bastler@bstly.de + * + */ +public class ItemModel { + + private Map name; + private String secret; + + /** + * @return the name + */ + public Map getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(Map name) { + this.name = name; + } + + /** + * @return the secret + */ + public String getSecret() { + return secret; + } + + /** + * @param secret the secret to set + */ + public void setSecret(String secret) { + this.secret = secret; + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/model/ItemResultModel.java b/core/src/main/java/de/bstly/we/controller/model/ItemResultModel.java new file mode 100644 index 0000000..51454e4 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/model/ItemResultModel.java @@ -0,0 +1,48 @@ +/** + * + */ +package de.bstly.we.controller.model; + +import java.util.List; + +import de.bstly.we.model.PermissionMapping; +import de.bstly.we.model.QuotaMapping; + +/** + * @author _bastler@bstly.de + * + */ +public class ItemResultModel { + + private List permissionMappings; + private List quotaMappings; + + /** + * @return the permissionMappings + */ + public List getPermissionMappings() { + return permissionMappings; + } + + /** + * @param permissionMappings the permissionMappings to set + */ + public void setPermissionMappings(List permissionMappings) { + this.permissionMappings = permissionMappings; + } + + /** + * @return the quotaMappings + */ + public List getQuotaMappings() { + return quotaMappings; + } + + /** + * @param quotaMappings the quotaMappings to set + */ + public void setQuotaMappings(List quotaMappings) { + this.quotaMappings = quotaMappings; + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/model/LoginModel.java b/core/src/main/java/de/bstly/we/controller/model/LoginModel.java new file mode 100755 index 0000000..197a02d --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/model/LoginModel.java @@ -0,0 +1,44 @@ +/** + * + */ +package de.bstly.we.controller.model; + +/** + * + * @author _bastler@bstly.de + * + */ +public class LoginModel { + + private String username; + private String password; + + /** + * @return the username + */ + public String getUsername() { + return username; + } + + /** + * @param username the username to set + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * @return the password + */ + public String getPassword() { + return password; + } + + /** + * @param password the password to set + */ + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/model/PagesResult.java b/core/src/main/java/de/bstly/we/controller/model/PagesResult.java new file mode 100644 index 0000000..3546f8d --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/model/PagesResult.java @@ -0,0 +1,90 @@ +/** + * + */ +package de.bstly.we.controller.model; + +import java.util.List; + +/** + * @author _bastler@bstly.de + * + */ +public class PagesResult { + + private List result; + private int size; + private int page; + private int totalElements; + private int totalPages; + + /** + * @return the result + */ + public List getResult() { + return result; + } + + /** + * @param result the result to set + */ + public void setResult(List result) { + this.result = result; + } + + /** + * @return the size + */ + public int getSize() { + return size; + } + + /** + * @param size the size to set + */ + public void setSize(int size) { + this.size = size; + } + + /** + * @return the page + */ + public int getPage() { + return page; + } + + /** + * @param page the page to set + */ + public void setPage(int page) { + this.page = page; + } + + /** + * @return the totalElements + */ + public int getTotalElements() { + return totalElements; + } + + /** + * @param totalElements the totalElements to set + */ + public void setTotalElements(int totalElements) { + this.totalElements = totalElements; + } + + /** + * @return the totalPages + */ + public int getTotalPages() { + return totalPages; + } + + /** + * @param totalPages the totalPages to set + */ + public void setTotalPages(int totalPages) { + this.totalPages = totalPages; + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/model/PasswordModel.java b/core/src/main/java/de/bstly/we/controller/model/PasswordModel.java new file mode 100755 index 0000000..b4d43a1 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/model/PasswordModel.java @@ -0,0 +1,58 @@ +/** + * + */ +package de.bstly.we.controller.model; + +/** + * + * @author _bastler@bstly.de + * + */ +public class PasswordModel { + + private String old; + private String password; + private String password2; + + /** + * @return the old + */ + public String getOld() { + return old; + } + + /** + * @param old the old to set + */ + public void setOld(String old) { + this.old = old; + } + + /** + * @return the password + */ + public String getPassword() { + return password; + } + + /** + * @param password the password to set + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * @return the password2 + */ + public String getPassword2() { + return password2; + } + + /** + * @param password2 the password2 to set + */ + public void setPassword2(String password2) { + this.password2 = password2; + } +} diff --git a/core/src/main/java/de/bstly/we/controller/model/PasswordResetModel.java b/core/src/main/java/de/bstly/we/controller/model/PasswordResetModel.java new file mode 100644 index 0000000..d218422 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/model/PasswordResetModel.java @@ -0,0 +1,28 @@ +/** + * + */ +package de.bstly.we.controller.model; + +/** + * @author _bastler@bstly.de + * + */ +public class PasswordResetModel extends PasswordModel { + + private String token; + + /** + * @return the token + */ + public String getToken() { + return token; + } + + /** + * @param token the token to set + */ + public void setToken(String token) { + this.token = token; + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/model/PretixRequest.java b/core/src/main/java/de/bstly/we/controller/model/PretixRequest.java new file mode 100644 index 0000000..fddb906 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/model/PretixRequest.java @@ -0,0 +1,78 @@ +/** + * + */ +package de.bstly.we.controller.model; + +import java.util.Map; + +import org.springframework.http.HttpMethod; + +/** + * + * @author _bastler@bstly.de + * + */ +public class PretixRequest { + + private String path; + private HttpMethod method; + private Map queryParameters; + private Object payload; + + /** + * @return the path + */ + public String getPath() { + return path; + } + + /** + * @param path the path to set + */ + public void setPath(String path) { + this.path = path; + } + + /** + * @return the method + */ + public HttpMethod getMethod() { + return method; + } + + /** + * @param method the method to set + */ + public void setMethod(HttpMethod method) { + this.method = method; + } + + /** + * @return the queryParameters + */ + public Map getQueryParameters() { + return queryParameters; + } + + /** + * @param queryParameters the queryParameters to set + */ + public void setQueryParameters(Map queryParameters) { + this.queryParameters = queryParameters; + } + + /** + * @return the payload + */ + public Object getPayload() { + return payload; + } + + /** + * @param payload the payload to set + */ + public void setPayload(Object payload) { + this.payload = payload; + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/model/ProfileModel.java b/core/src/main/java/de/bstly/we/controller/model/ProfileModel.java new file mode 100644 index 0000000..2dc9a72 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/model/ProfileModel.java @@ -0,0 +1,62 @@ +/** + * + */ +package de.bstly.we.controller.model; + +import java.util.List; + +import de.bstly.we.model.UserProfileField; + +/** + * @author _bastler@bstly.de + * + */ +public class ProfileModel { + + private String username; + private List aliases; + private List profileFields; + + /** + * @return the username + */ + public String getUsername() { + return username; + } + + /** + * @param username the username to set + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * @return the aliases + */ + public List getAliases() { + return aliases; + } + + /** + * @param aliases the aliases to set + */ + public void setAliases(List aliases) { + this.aliases = aliases; + } + + /** + * @return the profileFields + */ + public List getProfileFields() { + return profileFields; + } + + /** + * @param profileFields the profileFields to set + */ + public void setProfileFields(List profileFields) { + this.profileFields = profileFields; + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/model/SecondFactorProviderModel.java b/core/src/main/java/de/bstly/we/controller/model/SecondFactorProviderModel.java new file mode 100644 index 0000000..59f7aa4 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/model/SecondFactorProviderModel.java @@ -0,0 +1,53 @@ +/** + * + */ +package de.bstly.we.controller.model; + +/** + * @author _bastler@bstly.de + * + */ +public class SecondFactorProviderModel { + + private String id; + private boolean request; + + /** + * @param provider + * @param request + */ + public SecondFactorProviderModel(String id, boolean request) { + super(); + this.id = id; + this.request = request; + } + + /** + * @return the id + */ + public String getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(String id) { + this.id = id; + } + + /** + * @return the request + */ + public boolean isRequest() { + return request; + } + + /** + * @param request the request to set + */ + public void setRequest(boolean request) { + this.request = request; + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/model/UserModel.java b/core/src/main/java/de/bstly/we/controller/model/UserModel.java new file mode 100755 index 0000000..ae4b426 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/model/UserModel.java @@ -0,0 +1,143 @@ +/** + * + */ +package de.bstly.we.controller.model; + +import java.util.List; + +import de.bstly.we.model.Permission; +import de.bstly.we.model.PermissionMapping; +import de.bstly.we.model.Quota; +import de.bstly.we.model.QuotaMapping; +import de.bstly.we.model.UserProfileField; +import de.bstly.we.model.UserStatus; + +/** + * + * @author _bastler@bstly.de + * + */ +public class UserModel extends PasswordModel { + + private String username; + private List permissions; + private List permissionMappings; + private List quotas; + private List quotaMappings; + private UserStatus status; + private String token; + private List profileFields; + + /** + * @return the username + */ + public String getUsername() { + return username; + } + + /** + * @param username the username to set + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * @return the permissions + */ + public List getPermissions() { + return permissions; + } + + /** + * @param permissions the permissions to set + */ + public void setPermissions(List permissions) { + this.permissions = permissions; + } + + /** + * @return the permissionMappings + */ + public List getPermissionMappings() { + return permissionMappings; + } + + /** + * @param permissionMappings the permissionMappings to set + */ + public void setPermissionMappings(List permissionMappings) { + this.permissionMappings = permissionMappings; + } + + /** + * @return the quotas + */ + public List getQuotas() { + return quotas; + } + + /** + * @param quotas the quotas to set + */ + public void setQuotas(List quotas) { + this.quotas = quotas; + } + + /** + * @return the quotaMappings + */ + public List getQuotaMappings() { + return quotaMappings; + } + + /** + * @param quotaMappings the quotaMappings to set + */ + public void setQuotaMappings(List quotaMappings) { + this.quotaMappings = quotaMappings; + } + + /** + * @return the status + */ + public UserStatus getStatus() { + return status; + } + + /** + * @param status the status to set + */ + public void setStatus(UserStatus status) { + this.status = status; + } + + /** + * @return the token + */ + public String getToken() { + return token; + } + + /** + * @param token the token to set + */ + public void setToken(String token) { + this.token = token; + } + + /** + * @return the profileFields + */ + public List getProfileFields() { + return profileFields; + } + + /** + * @param profileFields the profileFields to set + */ + public void setProfileFields(List profileFields) { + this.profileFields = profileFields; + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/support/ControllerExceptionHandler.java b/core/src/main/java/de/bstly/we/controller/support/ControllerExceptionHandler.java new file mode 100644 index 0000000..4ac80f9 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/support/ControllerExceptionHandler.java @@ -0,0 +1,47 @@ +/** + * + */ +package de.bstly.we.controller.support; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +/** + * + * @author _bastler@bstly.de + * + */ +@ControllerAdvice +public class ControllerExceptionHandler extends ResponseEntityExceptionHandler { + + /** + * + * @param exception + * @param request + * @return + */ + @ExceptionHandler(value = { EntityResponseStatusException.class }) + protected ResponseEntity handleResponseEntityStatusException(RuntimeException exception, + WebRequest request) { + EntityResponseStatusException entityResponseStatusException = (EntityResponseStatusException) exception; + return handleExceptionInternal(exception, entityResponseStatusException.getBody(), new HttpHeaders(), + entityResponseStatusException.getStatus(), request); + } + +// /** +// * +// * @param exception +// * @param request +// * @return +// */ +// @ExceptionHandler(value = { IllegalArgumentException.class }) +// protected ResponseEntity handleRIllegalArgumentException(RuntimeException exception, WebRequest request) { +// IllegalArgumentException illegalArgumentException = (IllegalArgumentException) exception; +// return handleExceptionInternal(exception, illegalArgumentException.getMessage(), new HttpHeaders(), +// HttpStatus.BAD_REQUEST, request); +// } +} diff --git a/core/src/main/java/de/bstly/we/controller/support/EntityResponseStatusException.java b/core/src/main/java/de/bstly/we/controller/support/EntityResponseStatusException.java new file mode 100644 index 0000000..0ce309a --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/support/EntityResponseStatusException.java @@ -0,0 +1,87 @@ +/** + * + */ +package de.bstly.we.controller.support; + +import javax.annotation.Nullable; + +import org.springframework.core.NestedExceptionUtils; +import org.springframework.core.NestedRuntimeException; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +/** + * + * @author _bastler@bstly.de + * + */ +public class EntityResponseStatusException extends NestedRuntimeException { + + /** + * default serialVersionUID + */ + private static final long serialVersionUID = 1L; + + private final HttpStatus status; + + @Nullable + private final Object body; + + /** + * + * @param status + */ + public EntityResponseStatusException(HttpStatus status) { + this(null, status); + } + + /** + * + * @param body + * @param status + */ + public EntityResponseStatusException(@Nullable Object body, HttpStatus status) { + this(body, status, null); + } + + /** + * + * @param body + * @param status + * @param cause + */ + public EntityResponseStatusException(@Nullable Object body, HttpStatus status, @Nullable Throwable cause) { + super(null, cause); + Assert.notNull(status, "HttpStatus is required"); + this.status = status; + this.body = body; + } + + /** + * + * @return + */ + public HttpStatus getStatus() { + return this.status; + } + + /** + * + * @return + */ + @Nullable + public Object getBody() { + return this.body; + } + + /** + * + * @return + */ + @Override + public String getMessage() { + String msg = this.status + (this.body != null ? " \"" + this.body + "\"" : ""); + return NestedExceptionUtils.buildMessage(msg, getCause()); + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/support/JsonStringBodyControllerAdvice.java b/core/src/main/java/de/bstly/we/controller/support/JsonStringBodyControllerAdvice.java new file mode 100644 index 0000000..84cb13d --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/support/JsonStringBodyControllerAdvice.java @@ -0,0 +1,107 @@ +/** + * + */ +package de.bstly.we.controller.support; + +import java.io.IOException; +import java.lang.reflect.Type; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import com.google.gson.Gson; +import com.google.gson.JsonPrimitive; + +/** + * @author _bastler@bstly.de + * + */ +@ControllerAdvice +public class JsonStringBodyControllerAdvice implements RequestBodyAdvice, ResponseBodyAdvice { + + private Gson gson = new Gson(); + + /* + * @see org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice# + * supports(org.springframework.core.MethodParameter, java.lang.reflect.Type, + * java.lang.Class) + */ + @Override + public boolean supports(MethodParameter methodParameter, Type targetType, + Class> converterType) { + return targetType instanceof Class && String.class.equals((Class) targetType); + } + + /* + * @see org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice# + * beforeBodyRead(org.springframework.http.HttpInputMessage, + * org.springframework.core.MethodParameter, java.lang.reflect.Type, + * java.lang.Class) + */ + @Override + public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, + Class> converterType) throws IOException { + return inputMessage; + } + + /* + * @see org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice# + * afterBodyRead(java.lang.Object, org.springframework.http.HttpInputMessage, + * org.springframework.core.MethodParameter, java.lang.reflect.Type, + * java.lang.Class) + */ + @Override + public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, + Class> converterType) { + body = ((String) body).replaceAll("^\"|\"$", ""); + return body; + } + + /* + * @see org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice# + * handleEmptyBody(java.lang.Object, org.springframework.http.HttpInputMessage, + * org.springframework.core.MethodParameter, java.lang.reflect.Type, + * java.lang.Class) + */ + @Override + public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class> converterType) { + return body; + } + + /* + * @see + * org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice# + * supports(org.springframework.core.MethodParameter, java.lang.Class) + */ + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return converterType == StringHttpMessageConverter.class; + } + + /* + * @see + * org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice# + * beforeBodyWrite(java.lang.Object, org.springframework.core.MethodParameter, + * org.springframework.http.MediaType, java.lang.Class, + * org.springframework.http.server.ServerHttpRequest, + * org.springframework.http.server.ServerHttpResponse) + */ + @Override + public String beforeBodyWrite(String body, MethodParameter returnType, MediaType selectedContentType, + Class> selectedConverterType, ServerHttpRequest request, + ServerHttpResponse response) { + response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + return gson.toJson(new JsonPrimitive(body)); + } + +} \ No newline at end of file diff --git a/core/src/main/java/de/bstly/we/controller/support/RequestBodyErrors.java b/core/src/main/java/de/bstly/we/controller/support/RequestBodyErrors.java new file mode 100644 index 0000000..a0a2488 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/support/RequestBodyErrors.java @@ -0,0 +1,49 @@ +/** + * + */ +package de.bstly.we.controller.support; + +import org.springframework.lang.Nullable; +import org.springframework.validation.AbstractBindingResult; + +/** + * + * @author _bastler@bstly.de + * + */ +@SuppressWarnings("serial") +public class RequestBodyErrors extends AbstractBindingResult { + + @Nullable + private final Object target; + + /** + * + * @param target + * @param objectName + */ + public RequestBodyErrors(@Nullable Object target) { + super("request-body"); + this.target = target; + } + + /* + * @see org.springframework.validation.AbstractBindingResult#getTarget() + */ + @Override + public Object getTarget() { + return target; + } + + /* + * @see + * org.springframework.validation.AbstractBindingResult#getActualFieldValue(java + * .lang.String) + */ + @Override + protected Object getActualFieldValue(String field) { + // Not necessary + return null; + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/support/TokenSessionManager.java b/core/src/main/java/de/bstly/we/controller/support/TokenSessionManager.java new file mode 100644 index 0000000..58c90fa --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/support/TokenSessionManager.java @@ -0,0 +1,277 @@ +/** + * + */ +package de.bstly.we.controller.support; + +import java.time.Instant; +import java.util.List; +import java.util.Set; + +import javax.servlet.http.HttpSession; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.gson.JsonObject; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.PermissionMappingManager; +import de.bstly.we.businesslogic.PretixManager; +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.businesslogic.QuotaMappingManager; +import de.bstly.we.controller.model.ItemResultModel; +import de.bstly.we.model.Permission; +import de.bstly.we.model.PermissionMapping; +import de.bstly.we.model.QuotaMapping; +import de.bstly.we.security.model.LocalUserDetails; +import de.bstly.we.security.token.LocalAnonymousAuthenticationToken; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class TokenSessionManager { + + public static final String HTTP_SESSION_ATTRIBUTE_TOKENS = "tokens"; + + @Autowired + private PermissionMappingManager permissionMappingManager; + @Autowired + private PermissionManager permissionManager; + @Autowired + private QuotaMappingManager quotaMappingManager; + @Autowired + private QuotaManager quotaManager; + @Autowired + private PretixManager pretixManager; + + /** + * + * @param userId + * @param token + * @return + */ + public List getPermissionMappingsForToken(Long userId, String token) { + List permissionMappings = Lists.newArrayList(); + + try { + JsonObject result = pretixManager.getCheckInItemBySecret(token); + if (result != null && result.get("secret").getAsString().equals(token) + && result.getAsJsonArray("checkins").size() < 1 + && "p".equals(result.get("order__status").getAsString())) { + int item = result.get("item").getAsInt(); + permissionMappings.addAll(permissionMappingManager.getAllByItem(item)); + } + } catch (Exception e) { + // ignore + } + return permissionMappings; + } + + /** + * + * @param userId + * @param token + * @return + */ + public List getPermissionsForToken(Long userId, String token) { + List permissions = Lists.newArrayList(); + + try { + JsonObject orderPosition = pretixManager.getCheckInItemBySecret(token); + if (orderPosition != null && orderPosition.get("secret").getAsString().equals(token) + && orderPosition.getAsJsonArray("checkins").size() < 1 + && "p".equals(orderPosition.get("order__status").getAsString())) { + int item = orderPosition.get("item").getAsInt(); + + Instant lastPaymentDate = pretixManager + .getLastPaymentDateForOrder(orderPosition.get("order").getAsString()); + + if (lastPaymentDate == null) { + throw new Exception("This should not happen!"); + } + + permissions.addAll(permissionManager.getForItem(userId, item, + orderPosition.get("answers").getAsJsonArray(), lastPaymentDate)); + } + } catch (Exception e) { + // ignore + e.printStackTrace(); + } + return permissions; + } + + /** + * + * @param userId + * @param token + * @return + */ + public List getQuotaMappingsForToken(Long userId, String token) { + List quotaMappings = Lists.newArrayList(); + + try { + JsonObject result = pretixManager.getCheckInItemBySecret(token); + if (result != null && result.get("secret").getAsString().equals(token) + && result.getAsJsonArray("checkins").size() < 1 + && "p".equals(result.get("order__status").getAsString())) { + int item = result.get("item").getAsInt(); + quotaMappings.addAll(quotaMappingManager.getAllByItem(item)); + } + } catch (Exception e) { + // ignore + } + return quotaMappings; + } + + /** + * + * @param userId + * @param tokens + * @return + */ + public ItemResultModel applyTokens(Long userId, Set tokens) { + ItemResultModel itemResultModel = new ItemResultModel(); + List permissionMappings = Lists.newArrayList(); + List quotaMappings = Lists.newArrayList(); + for (String token : tokens) { + try { + JsonObject result = pretixManager.redeem(token); + if ("ok".equals(result.get("status").getAsString())) { + JsonObject position = result.get("position").getAsJsonObject(); + int item = position.get("item").getAsInt(); + + Instant lastPaymentDate = pretixManager + .getLastPaymentDateForOrder(position.get("order").getAsString()); + + if (lastPaymentDate == null) { + throw new Exception("This should not happen!"); + } + + permissionManager.applyItem(userId, item, + position.get("answers").getAsJsonArray(), lastPaymentDate); + permissionMappings.addAll(permissionMappingManager.getAllByItem(item)); + quotaManager.applyItem(userId, item); + quotaMappings.addAll(quotaMappingManager.getAllByItem(item)); + } + } catch (Exception e) { + // ignore + } + } + + itemResultModel.setPermissionMappings(permissionMappings); + itemResultModel.setQuotaMappings(quotaMappings); + + return itemResultModel; + } + + /** + * + * @param session + * @return + */ + public Set getTokenFromSession(HttpSession session) { + + Set tokens = Sets.newHashSet(); + + Object sessionAttribute = session.getAttribute(HTTP_SESSION_ATTRIBUTE_TOKENS); + + if (sessionAttribute != null && sessionAttribute instanceof String) { + for (String token : ((String) sessionAttribute).split(",")) { + if (StringUtils.hasText(token)) { + tokens.add(token); + } + } + } + + return tokens; + } + + /** + * + * @param secret + * @return + */ + public void addTokenToSession(String secret, HttpSession session) { + + String tokens = ""; + + Object sessionAttribute = session.getAttribute(HTTP_SESSION_ATTRIBUTE_TOKENS); + + if (sessionAttribute != null && sessionAttribute instanceof String) { + tokens = (String) sessionAttribute; + } + + if (StringUtils.hasLength(tokens)) { + tokens += "," + secret; + } else { + tokens = secret; + } + + session.setAttribute(HTTP_SESSION_ATTRIBUTE_TOKENS, tokens); + + } + + /** + * + * @param secret + * @return + */ + public void removeTokenFromSession(String secret, HttpSession session) { + String tokens = ""; + + Object sessionAttribute = session.getAttribute(HTTP_SESSION_ATTRIBUTE_TOKENS); + + if (sessionAttribute != null && sessionAttribute instanceof String) { + for (String token : ((String) sessionAttribute).split(",")) { + if (!token.equals(secret)) { + if (StringUtils.hasLength(tokens)) { + tokens += "," + secret; + } else { + tokens = secret; + } + } + } + } + + session.setAttribute(HTTP_SESSION_ATTRIBUTE_TOKENS, tokens); + } + + /** + * + * @param secret + * @return + */ + public void removeTokensFromSession(HttpSession session) { + session.setAttribute(HTTP_SESSION_ATTRIBUTE_TOKENS, null); + } + + /** + * + * @param auth + * @param details + * @return + */ + protected Authentication createNewAuth(Authentication auth, LocalUserDetails details) { + Authentication newAuth = null; + if (auth instanceof UsernamePasswordAuthenticationToken) { + newAuth = new UsernamePasswordAuthenticationToken(details, auth.getCredentials(), + details.getAuthorities()); + } else { + newAuth = new LocalAnonymousAuthenticationToken(details); + } + + if (!auth.isAuthenticated()) { + newAuth.setAuthenticated(false); + } + return newAuth; + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/validation/PasswordModelValidator.java b/core/src/main/java/de/bstly/we/controller/validation/PasswordModelValidator.java new file mode 100755 index 0000000..2d3c44a --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/validation/PasswordModelValidator.java @@ -0,0 +1,101 @@ +/** + * + */ +package de.bstly.we.controller.validation; + +import java.util.ArrayList; +import java.util.List; + +import org.passay.CharacterRule; +import org.passay.EnglishCharacterData; +import org.passay.LengthRule; +import org.passay.PasswordData; +import org.passay.PasswordValidator; +import org.passay.Rule; +import org.passay.RuleResult; +import org.passay.RuleResultDetail; +import org.passay.WhitespaceRule; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import de.bstly.we.businesslogic.SystemPropertyManager; +import de.bstly.we.controller.model.PasswordModel; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class PasswordModelValidator implements Validator { + + @Autowired + private SystemPropertyManager systemPropertyManager; + + public static final String SYSTEM_PROPERTY_PASSWORD_RULE_WHITESPACE = "password.rule.whitespace"; + public static final String SYSTEM_PROPERTY_PASSWORD_RULE_LENGTH = "password.rule.length"; + public static final String SYSTEM_PROPERTY_PASSWORD_RULE_UPPERCASE = "password.rule.uppercase"; + public static final String SYSTEM_PROPERTY_PASSWORD_RULE_DIGIT = "password.rule.digit"; + public static final String SYSTEM_PROPERTY_PASSWORD_RULE_SPECIAL = "password.rule.special"; + + /* + * @see org.springframework.validation.Validator#supports(java.lang.Class) + */ + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(PasswordModel.class); + } + + /* + * + * @see org.springframework.validation.Validator#validate(java.lang.Object, + * org.springframework.validation.Errors) + */ + @Override + public void validate(Object target, Errors errors) { + PasswordModel passwordModel = (PasswordModel) target; + + List rules = new ArrayList(); + + if (systemPropertyManager.getBoolean(SYSTEM_PROPERTY_PASSWORD_RULE_WHITESPACE, true)) { + rules.add(new WhitespaceRule()); + } + + int length = systemPropertyManager.getInteger(SYSTEM_PROPERTY_PASSWORD_RULE_LENGTH, 8); + if (length > 0) { + rules.add(new LengthRule(length, 4096)); + } + + int uppercase = systemPropertyManager.getInteger(SYSTEM_PROPERTY_PASSWORD_RULE_UPPERCASE, 1); + if (uppercase > 0) { + rules.add(new CharacterRule(EnglishCharacterData.UpperCase, uppercase)); + } + + int digit = systemPropertyManager.getInteger(SYSTEM_PROPERTY_PASSWORD_RULE_DIGIT, 1); + if (digit > 0) { + rules.add(new CharacterRule(EnglishCharacterData.Digit, digit)); + } + + int special = systemPropertyManager.getInteger(SYSTEM_PROPERTY_PASSWORD_RULE_SPECIAL, 1); + if (special > 0) { + rules.add(new CharacterRule(EnglishCharacterData.Special, special)); + } + + PasswordValidator validator = new PasswordValidator(rules); + PasswordData password = new PasswordData(passwordModel.getPassword()); + RuleResult result = validator.validate(password); + + if (!result.isValid()) { + for (RuleResultDetail ruleResultDetail : result.getDetails()) { + errors.rejectValue("password", ruleResultDetail.getErrorCode()); + } + } + + if (!passwordModel.getPassword().equals(passwordModel.getPassword2())) { + errors.rejectValue("password2", "NOT_MATCH"); + } + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/validation/PermissionMappingValidator.java b/core/src/main/java/de/bstly/we/controller/validation/PermissionMappingValidator.java new file mode 100755 index 0000000..ef209ac --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/validation/PermissionMappingValidator.java @@ -0,0 +1,53 @@ +/** + * + */ +package de.bstly.we.controller.validation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import de.bstly.we.businesslogic.PermissionMappingManager; +import de.bstly.we.model.PermissionMapping; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class PermissionMappingValidator implements Validator { + + @Autowired + private PermissionMappingManager permissionMappingManager; + + /* + * @see org.springframework.validation.Validator#supports(java.lang.Class) + */ + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(PermissionMapping.class); + } + + /* + * + * @see org.springframework.validation.Validator#validate(java.lang.Object, + * org.springframework.validation.Errors) + */ + @Override + public void validate(Object target, Errors errors) { + PermissionMapping permissionMapping = (PermissionMapping) target; + + for (String name : permissionMapping.getNames()) + if (permissionMappingManager.exists(permissionMapping.getItem(), name)) { + errors.rejectValue("item", "ALREADY_EXISTS"); + errors.rejectValue("names." + name, "ALREADY_EXISTS"); + } + + if (permissionMapping.getLifetime() <= 0) { + errors.rejectValue("lifetime", "TOO_SHORT"); + } + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/validation/QuotaMappingValidator.java b/core/src/main/java/de/bstly/we/controller/validation/QuotaMappingValidator.java new file mode 100644 index 0000000..bd5c9bc --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/validation/QuotaMappingValidator.java @@ -0,0 +1,54 @@ +/** + * + */ +package de.bstly.we.controller.validation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import de.bstly.we.businesslogic.QuotaMappingManager; +import de.bstly.we.model.QuotaMapping; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class QuotaMappingValidator implements Validator { + + @Autowired + private QuotaMappingManager quotaMappingManager; + + /* + * @see org.springframework.validation.Validator#supports(java.lang.Class) + */ + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(QuotaMapping.class); + } + + /* + * + * @see org.springframework.validation.Validator#validate(java.lang.Object, + * org.springframework.validation.Errors) + */ + @Override + public void validate(Object target, Errors errors) { + QuotaMapping quotaMapping = (QuotaMapping) target; + + for (Integer item : quotaMapping.getItems()) { + if (quotaMappingManager.exists(item, quotaMapping.getName())) { + errors.rejectValue("item", "ALREADY_EXISTS"); + errors.rejectValue("name", "ALREADY_EXISTS"); + } + } + + if (quotaMapping.getValue() <= 0) { + errors.rejectValue("value", "TOO_SHORT"); + } + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/validation/UserAliasValidator.java b/core/src/main/java/de/bstly/we/controller/validation/UserAliasValidator.java new file mode 100644 index 0000000..2ca6848 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/validation/UserAliasValidator.java @@ -0,0 +1,59 @@ +/** + * + */ +package de.bstly.we.controller.validation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import de.bstly.we.model.UserAlias; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class UserAliasValidator implements Validator { + + @Autowired + private UserModelValidator userModelValidator; + + /* + * @see org.springframework.validation.Validator#supports(java.lang.Class) + */ + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(UserAlias.class); + } + + /* + * @see org.springframework.validation.Validator#validate(java.lang.Object, + * org.springframework.validation.Errors) + */ + @Override + public void validate(Object target, Errors errors) { + UserAlias userAlias = (UserAlias) target; + + if (!StringUtils.hasText(userAlias.getAlias())) { + errors.rejectValue("alias", "REQUIRED"); + return; + } + + if (userAlias.getTarget() == null) { + errors.rejectValue("target", "REQUIRED"); + return; + } + + if (userAlias.getVisibility() == null) { + errors.rejectValue("visibility", "REQUIRED"); + return; + } + + userModelValidator.validateUsername(userAlias.getAlias(), "alias", errors); + + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/validation/UserDomainValidator.java b/core/src/main/java/de/bstly/we/controller/validation/UserDomainValidator.java new file mode 100644 index 0000000..480cc72 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/validation/UserDomainValidator.java @@ -0,0 +1,73 @@ +/** + * + */ +package de.bstly.we.controller.validation; + +import org.apache.commons.validator.routines.DomainValidator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import de.bstly.we.businesslogic.UserDomainManager; +import de.bstly.we.model.UserDomain; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class UserDomainValidator implements Validator { + + private DomainValidator domainValidator = DomainValidator.getInstance(); + @Autowired + private UserDomainManager userDomainManager; + + /* + * @see org.springframework.validation.Validator#supports(java.lang.Class) + */ + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(UserDomain.class); + } + + /* + * @see org.springframework.validation.Validator#validate(java.lang.Object, + * org.springframework.validation.Errors) + */ + @Override + public void validate(Object target, Errors errors) { + UserDomain userDomain = (UserDomain) target; + + if (!StringUtils.hasText(userDomain.getDomain())) { + errors.rejectValue("domain", "REQUIRED"); + return; + } + + if (!domainValidator.isValid(userDomain.getDomain())) { + errors.rejectValue("domain", "NOT_VALID"); + return; + } + + UserDomain existingDomain = userDomainManager.getByDomain(userDomain.getDomain()); + + if (existingDomain != null && (userDomain.getId() == null + || !(existingDomain.getId().equals(userDomain.getId())))) { + errors.rejectValue("domain", "NOT_VALID"); + return; + } + + if (userDomain.getTarget() == null) { + errors.rejectValue("target", "REQUIRED"); + return; + } + + if (userDomain.getVisibility() == null) { + errors.rejectValue("visibility", "REQUIRED"); + return; + } + + } + +} diff --git a/core/src/main/java/de/bstly/we/controller/validation/UserModelValidator.java b/core/src/main/java/de/bstly/we/controller/validation/UserModelValidator.java new file mode 100755 index 0000000..e7870a5 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/validation/UserModelValidator.java @@ -0,0 +1,91 @@ +/** + * + */ +package de.bstly.we.controller.validation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import de.bstly.we.businesslogic.SystemPropertyManager; +import de.bstly.we.businesslogic.UserAliasManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.controller.model.UserModel; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class UserModelValidator implements Validator { + + public static final String RESERVED_USERNAMES = "usernames.reserved"; + + protected static final String localPart = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^-]+(?:\\\\.[A-Z0-9_!#$%&'*+/=?`{|}~^-]+)*$"; + + @Autowired + private UserManager userManager; + @Autowired + private UserAliasManager userAliasManager; + @Autowired + private SystemPropertyManager systemPropertyManager; + + /* + * @see org.springframework.validation.Validator#supports(java.lang.Class) + */ + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(UserModel.class); + } + + /* + * + * @see org.springframework.validation.Validator#validate(java.lang.Object, + * org.springframework.validation.Errors) + */ + @Override + public void validate(Object target, Errors errors) { + UserModel userModel = (UserModel) target; + + if (!StringUtils.hasText(userModel.getUsername())) { + errors.rejectValue("username", "REQUIRED"); + return; + } + + validateUsername(userModel.getUsername(), "username", errors); + } + + /** + * + * @param username + * @param field + * @param errors + */ + public void validateUsername(String username, String field, Errors errors) { + for (String systemUsername : systemPropertyManager.get(RESERVED_USERNAMES, "").split(",")) { + if (StringUtils.hasText(systemUsername) + && (username.toLowerCase().equals(systemUsername) + || username.toLowerCase().matches(systemUsername))) { + errors.rejectValue(field, "NOT_VALID"); + break; + } + } + + if (userManager.getByUsername(username) != null) { + errors.rejectValue(field, "NOT_VALID"); + return; + } + + if (userAliasManager.getByAlias(username) != null) { + errors.rejectValue(field, "NOT_VALID"); + return; + } + + if (!username.matches(localPart)) { + errors.rejectValue(field, "NOT_VALID"); + } + } +} diff --git a/core/src/main/java/de/bstly/we/controller/validation/UserProfileFieldValidator.java b/core/src/main/java/de/bstly/we/controller/validation/UserProfileFieldValidator.java new file mode 100644 index 0000000..4dd29d3 --- /dev/null +++ b/core/src/main/java/de/bstly/we/controller/validation/UserProfileFieldValidator.java @@ -0,0 +1,157 @@ +/** + * + */ +package de.bstly.we.controller.validation; + +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.List; + +import org.apache.commons.validator.routines.DoubleValidator; +import org.apache.commons.validator.routines.EmailValidator; +import org.apache.commons.validator.routines.UrlValidator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import com.google.common.collect.Lists; +import com.querydsl.core.BooleanBuilder; + +import de.bstly.we.businesslogic.SystemProfileFieldManager; +import de.bstly.we.model.ProfileFieldType; +import de.bstly.we.model.QUserProfileField; +import de.bstly.we.model.SystemProfileField; +import de.bstly.we.model.UserProfileField; +import de.bstly.we.repository.UserProfileFieldRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class UserProfileFieldValidator implements Validator { + + @Autowired + private SystemProfileFieldManager systemProfileFieldManager; + @Autowired + private UserProfileFieldRepository userProfileFieldRepository; + private QUserProfileField qUserProfileField = QUserProfileField.userProfileField; + + private DoubleValidator doubleValidator = DoubleValidator.getInstance(); + private EmailValidator emailValidator = EmailValidator.getInstance(); + private UrlValidator urlValidator = new UrlValidator(UrlValidator.ALLOW_ALL_SCHEMES); + private List validBoolean = Lists.newArrayList("true", "True", "TRUE", "1", "false", + "False", "FALSE", "0"); + + /* + * @see org.springframework.validation.Validator#supports(java.lang.Class) + */ + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(UserProfileField.class); + } + + /* + * @see org.springframework.validation.Validator#validate(java.lang.Object, + * org.springframework.validation.Errors) + */ + @Override + public void validate(Object target, Errors errors) { + UserProfileField userProfileField = (UserProfileField) target; + + if (userProfileField.getName() == null || userProfileField.getName().length() < 3) { + errors.rejectValue("name", "TOO_SHORT"); + } else if (userProfileField.getName().length() > 255) { + errors.rejectValue("name", "TOO_LONG"); + } + + SystemProfileField systemProfileField = systemProfileFieldManager + .get(userProfileField.getName()); + + if (systemProfileField != null) { + if (!systemProfileField.getType().equals(userProfileField.getType())) { + errors.rejectValue("type", "INVALID_TYPE", userProfileField.getName()); + return; + } + + if (systemProfileField.isUniqueValue()) { + BooleanBuilder builder = new BooleanBuilder(); + builder.and(qUserProfileField.name.eq(userProfileField.getName())); + + if (userProfileField.getTarget() != null) { + builder.and(qUserProfileField.target.ne(userProfileField.getTarget())); + } + + if (ProfileFieldType.BLOB.equals(userProfileField.getType())) { + if (userProfileField.getBlob() == null) { + errors.rejectValue("blob", "NOT_SPECIFIED", userProfileField.getName()); + return; + } + builder.and(qUserProfileField.blob.eq(userProfileField.getBlob())); + } else if (userProfileField.getValue() == null) { + errors.rejectValue("value", "NOT_SPECIFIED", userProfileField.getName()); + } else { + builder.and(qUserProfileField.value.eq(userProfileField.getValue())); + } + + if (userProfileFieldRepository.exists(builder.getValue())) { + errors.rejectValue("value", "NOT_VALID", userProfileField.getName()); + return; + } + } + } + + switch (userProfileField.getType()) { + case BOOL: + if (!validBoolean.contains(userProfileField.getValue())) { + errors.rejectValue("value", "INVALID_BOOLEAN", userProfileField.getName()); + } + break; + case DATE: + try { + Instant.parse(userProfileField.getValue()); + } catch (DateTimeParseException e) { + errors.rejectValue("value", "INVALID_DATE", userProfileField.getName()); + } + break; + case DATETIME: + try { + Instant.parse(userProfileField.getValue()); + } catch (DateTimeParseException e) { + errors.rejectValue("value", "INVALID_DATE", userProfileField.getName()); + } + break; + case TIME: + try { + Instant.parse(userProfileField.getValue()); + } catch (DateTimeParseException e) { + errors.rejectValue("value", "INVALID_DATE", userProfileField.getName()); + } + break; + case EMAIL: + if (!emailValidator.isValid(userProfileField.getValue())) { + errors.rejectValue("value", "INVALID_EMAIL", userProfileField.getName()); + } + break; + case NUMBER: + if (!doubleValidator.isValid(userProfileField.getValue())) { + errors.rejectValue("value", "INVALID_NUMBER", userProfileField.getName()); + } + break; + case TEXT: + if (userProfileField.getValue() != null && userProfileField.getValue().length() > 255) { + errors.rejectValue("value", "TOO_LONG", userProfileField.getName()); + } + break; + case URL: + if (!urlValidator.isValid(userProfileField.getValue())) { + errors.rejectValue("value", "INVALID_URL", userProfileField.getName()); + } + break; + case BLOB: + break; + } + } + +} diff --git a/core/src/main/java/de/bstly/we/event/AbstractModelEvent.java b/core/src/main/java/de/bstly/we/event/AbstractModelEvent.java new file mode 100644 index 0000000..9d53da0 --- /dev/null +++ b/core/src/main/java/de/bstly/we/event/AbstractModelEvent.java @@ -0,0 +1,63 @@ +/** + * + */ +package de.bstly.we.event; + +import org.springframework.context.ApplicationEvent; + +import de.bstly.we.model.AbstractModel; + +/** + * @author Lurkars + * + */ +public class AbstractModelEvent extends ApplicationEvent { + + /** + * default serialVersionUID + */ + private static final long serialVersionUID = 1L; + + private AbstractModelEventType type; + private AbstractModel model; + + /** + * + * @param type + * @param model + */ + public AbstractModelEvent(AbstractModelEventType type, AbstractModel model) { + super(model); + this.type = type; + this.model = model; + } + + /** + * @return the type + */ + public AbstractModelEventType getType() { + return type; + } + + /** + * @param type the type to set + */ + public void setType(AbstractModelEventType type) { + this.type = type; + } + + /** + * @return the model + */ + public AbstractModel getModel() { + return model; + } + + /** + * @param model the model to set + */ + public void setModel(AbstractModel model) { + this.model = model; + } + +} diff --git a/core/src/main/java/de/bstly/we/event/AbstractModelEventType.java b/core/src/main/java/de/bstly/we/event/AbstractModelEventType.java new file mode 100644 index 0000000..edb72dd --- /dev/null +++ b/core/src/main/java/de/bstly/we/event/AbstractModelEventType.java @@ -0,0 +1,14 @@ +/** + * + */ +package de.bstly.we.event; + +/** + * @author Lurkars + * + */ +public enum AbstractModelEventType { + + PRE_PERSIST, PRE_UPDATE, PRE_REMOVE, POST_PERSIST, POST_UPDATE, POST_REMOVE, POST_LOAD + +} diff --git a/core/src/main/java/de/bstly/we/model/AbstractModel.java b/core/src/main/java/de/bstly/we/model/AbstractModel.java new file mode 100644 index 0000000..26b5b3f --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/AbstractModel.java @@ -0,0 +1,12 @@ +/** + * + */ +package de.bstly.we.model; + +/** + * @author Lurkars + * + */ +public interface AbstractModel { + +} diff --git a/core/src/main/java/de/bstly/we/model/Permission.java b/core/src/main/java/de/bstly/we/model/Permission.java new file mode 100755 index 0000000..d660064 --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/Permission.java @@ -0,0 +1,123 @@ +/** + * + */ +package de.bstly.we.model; + +import java.time.Instant; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "permissions") +public class Permission implements UserData { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + @Column(name = "name", nullable = false) + private String name; + @Column(name = "target", nullable = false) + private Long target; + @Column(name = "addon", columnDefinition = "boolean default false") + private boolean addon; + @Column(name = "starts", nullable = true) + private Instant starts; + @Column(name = "expires") + private Instant expires; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the target + */ + public Long getTarget() { + return target; + } + + /** + * @param target the target to set + */ + public void setTarget(Long target) { + this.target = target; + } + + /** + * @return the addon + */ + public boolean isAddon() { + return addon; + } + + /** + * @param addon the addon to set + */ + public void setAddon(boolean addon) { + this.addon = addon; + } + + /** + * @return the starts + */ + public Instant getStarts() { + return starts; + } + + /** + * @param starts the starts to set + */ + public void setStarts(Instant starts) { + this.starts = starts; + } + + /** + * @return the expires + */ + public Instant getExpires() { + return expires; + } + + /** + * @param expires the expires to set + */ + public void setExpires(Instant expires) { + this.expires = expires; + } + +} diff --git a/core/src/main/java/de/bstly/we/model/PermissionMapping.java b/core/src/main/java/de/bstly/we/model/PermissionMapping.java new file mode 100755 index 0000000..cb9052f --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/PermissionMapping.java @@ -0,0 +1,214 @@ +/** + * + */ +package de.bstly.we.model; + +import java.time.temporal.ChronoUnit; +import java.util.Set; + +import javax.persistence.AttributeConverter; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.Converter; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; + +/** + * + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "permission_mappings") +public class PermissionMapping { + + public static final ChronoUnit DEFAULT_LIFETIME_UNIT = ChronoUnit.DAYS; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + @Column(name = "product") + private String product; + @Column(name = "item", nullable = false) + private Integer item; + @ElementCollection + @LazyCollection(LazyCollectionOption.FALSE) + @CollectionTable(name = "permission_mappings_names") + private Set names; + @Column(name = "addon", columnDefinition = "boolean default false") + private boolean addon; + @Column(name = "lifetime") + private Long lifetime; + @Column(name = "lifetime_unit") + @Convert(converter = ChronoUnitConverter.class) + private ChronoUnit lifetimeUnit = DEFAULT_LIFETIME_UNIT; + @Column(name = "lifetime_round", columnDefinition = "boolean default false") + private boolean lifetimeRound; + @Column(name = "starts_question") + private String startsQuestion; + @Column(name = "expires_question") + private String expiresQuestion; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the product + */ + public String getProduct() { + return product; + } + + /** + * @param product the product to set + */ + public void setProduct(String product) { + this.product = product; + } + + /** + * @return the item + */ + public Integer getItem() { + return item; + } + + /** + * @param item the item to set + */ + public void setItem(Integer item) { + this.item = item; + } + + /** + * @return the names + */ + public Set getNames() { + return names; + } + + /** + * @param names the names to set + */ + public void setNames(Set names) { + this.names = names; + } + + /** + * @return the addon + */ + public boolean isAddon() { + return addon; + } + + /** + * @param addon the addon to set + */ + public void setAddon(boolean addon) { + this.addon = addon; + } + + /** + * @return the lifetime + */ + public Long getLifetime() { + return lifetime; + } + + /** + * @param lifetime the lifetime to set + */ + public void setLifetime(Long lifetime) { + this.lifetime = lifetime; + } + + /** + * @return the lifetimeUnit + */ + public ChronoUnit getLifetimeUnit() { + return lifetimeUnit; + } + + /** + * @param lifetimeUnit the lifetimeUnit to set + */ + public void setLifetimeUnit(ChronoUnit lifetimeUnit) { + this.lifetimeUnit = lifetimeUnit; + } + + /** + * @return the lifetimeRound + */ + public boolean isLifetimeRound() { + return lifetimeRound; + } + + /** + * @param lifetimeRound the lifetimeRound to set + */ + public void setLifetimeRound(boolean lifetimeRound) { + this.lifetimeRound = lifetimeRound; + } + + /** + * @return the startsQuestion + */ + public String getStartsQuestion() { + return startsQuestion; + } + + /** + * @param startsQuestion the startsQuestion to set + */ + public void setStartsQuestion(String startsQuestion) { + this.startsQuestion = startsQuestion; + } + + /** + * @return the expiresQuestion + */ + public String getExpiresQuestion() { + return expiresQuestion; + } + + /** + * @param expiresQuestion the expiresQuestion to set + */ + public void setExpiresQuestion(String expiresQuestion) { + this.expiresQuestion = expiresQuestion; + } + + @Converter + public static class ChronoUnitConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(ChronoUnit chronoUnit) { + return chronoUnit.name(); + } + + @Override + public ChronoUnit convertToEntityAttribute(String value) { + return ChronoUnit.valueOf(value); + } + } + +} diff --git a/core/src/main/java/de/bstly/we/model/PersistentLogin.java b/core/src/main/java/de/bstly/we/model/PersistentLogin.java new file mode 100644 index 0000000..9b11991 --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/PersistentLogin.java @@ -0,0 +1,88 @@ +/** + * + */ +package de.bstly.we.model; + +import java.time.Instant; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "persistent_logins") +public class PersistentLogin { + + @Column(name = "username", length = 64, nullable = false) + private String username; + @Id + @Column(name = "series", length = 64) + private String series; + @Column(name = "token", length = 64, nullable = false) + private String token; + @Column(name = "last_used", nullable = false) + private Instant last_used; + + /** + * @return the username + */ + public String getUsername() { + return username; + } + + /** + * @param username the username to set + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * @return the series + */ + public String getSeries() { + return series; + } + + /** + * @param series the series to set + */ + public void setSeries(String series) { + this.series = series; + } + + /** + * @return the token + */ + public String getToken() { + return token; + } + + /** + * @param token the token to set + */ + public void setToken(String token) { + this.token = token; + } + + /** + * @return the last_used + */ + public Instant getLast_used() { + return last_used; + } + + /** + * @param last_used the last_used to set + */ + public void setLast_used(Instant last_used) { + this.last_used = last_used; + } + +} diff --git a/core/src/main/java/de/bstly/we/model/ProfileFieldType.java b/core/src/main/java/de/bstly/we/model/ProfileFieldType.java new file mode 100644 index 0000000..5d84936 --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/ProfileFieldType.java @@ -0,0 +1,14 @@ +/** + * + */ +package de.bstly.we.model; + +/** + * @author _bastler@bstly.de + * + */ +public enum ProfileFieldType { + + TEXT, NUMBER, DATE, URL, EMAIL, BOOL, BLOB, DATETIME, TIME + +} diff --git a/core/src/main/java/de/bstly/we/model/Quota.java b/core/src/main/java/de/bstly/we/model/Quota.java new file mode 100644 index 0000000..0b9a7c2 --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/Quota.java @@ -0,0 +1,120 @@ +/** + * + */ +package de.bstly.we.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "quotas") +public class Quota implements UserData { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", updatable = false) + private Long id; + @Column(name = "target", nullable = false) + private Long target; + @Column(name = "name", nullable = false) + private String name; + @Column(name = "value", nullable = false) + private long value; + @Column(name = "unit", nullable = true) + private String unit; + @Column(name = "disposable", columnDefinition = "boolean default false") + private boolean disposable; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the target + */ + public Long getTarget() { + return target; + } + + /** + * @param target the target to set + */ + public void setTarget(Long target) { + this.target = target; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the value + */ + public long getValue() { + return value; + } + + /** + * @param value the value to set + */ + public void setValue(long value) { + this.value = value; + } + + /** + * @return the unit + */ + public String getUnit() { + return unit; + } + + /** + * @param unit the unit to set + */ + public void setUnit(String unit) { + this.unit = unit; + } + + /** + * @return the disposable + */ + public boolean isDisposable() { + return disposable; + } + + /** + * @param disposable the disposable to set + */ + public void setDisposable(boolean disposable) { + this.disposable = disposable; + } + +} diff --git a/core/src/main/java/de/bstly/we/model/QuotaMapping.java b/core/src/main/java/de/bstly/we/model/QuotaMapping.java new file mode 100644 index 0000000..08a621a --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/QuotaMapping.java @@ -0,0 +1,164 @@ +/** + * + */ +package de.bstly.we.model; + +import java.util.Set; + +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; + +/** + * + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "quota_mappings") +public class QuotaMapping { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", updatable = false) + private Long id; + @ElementCollection + @LazyCollection(LazyCollectionOption.FALSE) + @CollectionTable(name = "quota_mappings_products") + private Set products; + @ElementCollection + @LazyCollection(LazyCollectionOption.FALSE) + @CollectionTable(name = "quota_mappings_items") + private Set items; + @Column(name = "name", nullable = false) + private String name; + @Column(name = "value", nullable = false) + private long value; + @Column(name = "unit", nullable = true) + private String unit; + @Column(name = "append", columnDefinition = "boolean default false") + private boolean append; + @Column(name = "disposable", columnDefinition = "boolean default false") + private boolean disposable; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the products + */ + public Set getProducts() { + return products; + } + + /** + * @param products the products to set + */ + public void setProducts(Set products) { + this.products = products; + } + + /** + * @return the items + */ + public Set getItems() { + return items; + } + + /** + * @param items the items to set + */ + public void setItems(Set items) { + this.items = items; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the value + */ + public long getValue() { + return value; + } + + /** + * @param value the value to set + */ + public void setValue(long value) { + this.value = value; + } + + /** + * @return the unit + */ + public String getUnit() { + return unit; + } + + /** + * @param unit the unit to set + */ + public void setUnit(String unit) { + this.unit = unit; + } + + /** + * @return the append + */ + public boolean isAppend() { + return append; + } + + /** + * @param append the append to set + */ + public void setAppend(boolean append) { + this.append = append; + } + + /** + * @return the disposable + */ + public boolean isDisposable() { + return disposable; + } + + /** + * @param disposable the disposable to set + */ + public void setDisposable(boolean disposable) { + this.disposable = disposable; + } + +} diff --git a/core/src/main/java/de/bstly/we/model/SecondFactor.java b/core/src/main/java/de/bstly/we/model/SecondFactor.java new file mode 100644 index 0000000..51bc455 --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/SecondFactor.java @@ -0,0 +1,12 @@ +/** + * + */ +package de.bstly.we.model; + +/** + * @author _bastler@bstly.de + * + */ +public interface SecondFactor extends UserData { + +} diff --git a/core/src/main/java/de/bstly/we/model/SystemProfileField.java b/core/src/main/java/de/bstly/we/model/SystemProfileField.java new file mode 100644 index 0000000..2b644ba --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/SystemProfileField.java @@ -0,0 +1,69 @@ +/** + * + */ +package de.bstly.we.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "system_profile_fields") +public class SystemProfileField { + + @Id + @Column(name = "name", nullable = false) + private String name; + @Column(name = "type", nullable = false) + private ProfileFieldType type; + @Column(name = "unique_value", columnDefinition = "boolean default false") + private boolean uniqueValue; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the type + */ + public ProfileFieldType getType() { + return type; + } + + /** + * @param type the type to set + */ + public void setType(ProfileFieldType type) { + this.type = type; + } + + /** + * @return the uniqueValue + */ + public boolean isUniqueValue() { + return uniqueValue; + } + + /** + * @param uniqueValue the uniqueValue to set + */ + public void setUniqueValue(boolean uniqueValue) { + this.uniqueValue = uniqueValue; + } + +} diff --git a/core/src/main/java/de/bstly/we/model/SystemProperty.java b/core/src/main/java/de/bstly/we/model/SystemProperty.java new file mode 100755 index 0000000..3de74dc --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/SystemProperty.java @@ -0,0 +1,73 @@ +/** + * + */ +package de.bstly.we.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; + +/** + * + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "system_properties") +public class SystemProperty { + + @Id + @Column(name = "id") + private String key; + @Lob + @Column(name = "value") + private String value; + + /** + * + */ + public SystemProperty() { + super(); + } + + /** + * @param key + * @param value + */ + public SystemProperty(String key, String value) { + super(); + this.key = key; + this.value = value; + } + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @param key the key to set + */ + public void setKey(String key) { + this.key = key; + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @param value the value to set + */ + public void setValue(String value) { + this.value = value; + } + +} diff --git a/core/src/main/java/de/bstly/we/model/User.java b/core/src/main/java/de/bstly/we/model/User.java new file mode 100755 index 0000000..376bd14 --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/User.java @@ -0,0 +1,156 @@ +/** + * + */ +package de.bstly.we.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "users", uniqueConstraints = @UniqueConstraint(columnNames = { "username" })) +public class User implements UserData { + + @Id + @Column(name = "id", updatable = false, unique = true, nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(name = "username", unique = true, nullable = false) + private String username; + @JsonIgnore + @Column(name = "password", nullable = false) + private String passwordHash; + @Column(name = "disabled", columnDefinition = "boolean default false") + private boolean disabled; + @Column(name = "locked", columnDefinition = "boolean default false") + private boolean locked; + @Column(name = "status", nullable = false) + private UserStatus status; + @Column(name = "secret") + private String secret; + @Column(name = "reset_token") + private String resetToken; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the username + */ + public String getUsername() { + return username; + } + + /** + * @param username the username to set + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * @return the passwordHash + */ + public String getPasswordHash() { + return passwordHash; + } + + /** + * @param passwordHash the passwordHash to set + */ + public void setPasswordHash(String passwordHash) { + this.passwordHash = passwordHash; + } + + /** + * @return the disabled + */ + public boolean isDisabled() { + return disabled; + } + + /** + * @param disabled the disabled to set + */ + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + /** + * @return the locked + */ + public boolean isLocked() { + return locked; + } + + /** + * @param locked the locked to set + */ + public void setLocked(boolean locked) { + this.locked = locked; + } + + /** + * @return the status + */ + public UserStatus getStatus() { + return status; + } + + /** + * @param status the status to set + */ + public void setStatus(UserStatus status) { + this.status = status; + } + + /** + * @return the secret + */ + public String getSecret() { + return secret; + } + + /** + * @param secret the secret to set + */ + public void setSecret(String secret) { + this.secret = secret; + } + + /** + * @return the resetToken + */ + public String getResetToken() { + return resetToken; + } + + /** + * @param resetToken the resetToken to set + */ + public void setResetToken(String resetToken) { + this.resetToken = resetToken; + } +} diff --git a/core/src/main/java/de/bstly/we/model/UserAlias.java b/core/src/main/java/de/bstly/we/model/UserAlias.java new file mode 100644 index 0000000..55d63ba --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/UserAlias.java @@ -0,0 +1,89 @@ +/** + * + */ +package de.bstly.we.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "aliases", uniqueConstraints = @UniqueConstraint(columnNames = { "alias" })) +public class UserAlias implements UserData { + + @Id + @Column(name = "id", updatable = false, unique = true, nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(name = "target", nullable = false) + private Long target; + @Column(name = "alias", unique = true, nullable = false) + private String alias; + @Column(name = "visibility", nullable = false) + private Visibility visibility = Visibility.PROTECTED; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the target + */ + public Long getTarget() { + return target; + } + + /** + * @param target the target to set + */ + public void setTarget(Long target) { + this.target = target; + } + + /** + * @return the alias + */ + public String getAlias() { + return alias; + } + + /** + * @param alias the alias to set + */ + public void setAlias(String alias) { + this.alias = alias; + } + + /** + * @return the visibility + */ + public Visibility getVisibility() { + return visibility; + } + + /** + * @param visibility the visibility to set + */ + public void setVisibility(Visibility visibility) { + this.visibility = visibility; + } + +} diff --git a/core/src/main/java/de/bstly/we/model/UserData.java b/core/src/main/java/de/bstly/we/model/UserData.java new file mode 100644 index 0000000..19af100 --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/UserData.java @@ -0,0 +1,12 @@ +/** + * + */ +package de.bstly.we.model; + +/** + * @author _bastler@bstly.de + * + */ +public interface UserData { + +} diff --git a/core/src/main/java/de/bstly/we/model/UserDomain.java b/core/src/main/java/de/bstly/we/model/UserDomain.java new file mode 100644 index 0000000..cb96817 --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/UserDomain.java @@ -0,0 +1,120 @@ +/** + * + */ +package de.bstly.we.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "domains", uniqueConstraints = @UniqueConstraint(columnNames = { "domain" })) +public class UserDomain implements UserData { + + @Id + @Column(name = "id", updatable = false, unique = true, nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(name = "target", nullable = false) + private Long target; + @Column(name = "domain", unique = true, nullable = false) + private String domain; + @Column(name = "visibility", nullable = false) + private Visibility visibility = Visibility.PROTECTED; + @Column(name = "secret") + private String secret; + @Column(name = "validated", columnDefinition = "boolean default false") + private boolean validated; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the target + */ + public Long getTarget() { + return target; + } + + /** + * @param target the target to set + */ + public void setTarget(Long target) { + this.target = target; + } + + /** + * @return the domain + */ + public String getDomain() { + return domain; + } + + /** + * @param domain the domain to set + */ + public void setDomain(String domain) { + this.domain = domain; + } + + /** + * @return the visibility + */ + public Visibility getVisibility() { + return visibility; + } + + /** + * @param visibility the visibility to set + */ + public void setVisibility(Visibility visibility) { + this.visibility = visibility; + } + + /** + * @return the secret + */ + public String getSecret() { + return secret; + } + + /** + * @param secret the secret to set + */ + public void setSecret(String secret) { + this.secret = secret; + } + + /** + * @return the validated + */ + public boolean isValidated() { + return validated; + } + + /** + * @param validated the validated to set + */ + public void setValidated(boolean validated) { + this.validated = validated; + } +} diff --git a/core/src/main/java/de/bstly/we/model/UserProfileField.java b/core/src/main/java/de/bstly/we/model/UserProfileField.java new file mode 100644 index 0000000..aec0c41 --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/UserProfileField.java @@ -0,0 +1,208 @@ +/** + * + */ +package de.bstly.we.model; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.Lob; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import de.bstly.we.model.UserProfileField.UserProfileFieldId; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +@IdClass(UserProfileFieldId.class) +@Table(name = "profile_fields", uniqueConstraints = @UniqueConstraint(columnNames = { "target", + "name" })) +public class UserProfileField implements UserData { + + @Id + @Column(name = "name", nullable = false) + private String name; + @Id + @Column(name = "target", nullable = false) + private Long target; + @Column(name = "value", nullable = true) + private String value; + @Lob + @Column(name = "blob_value", nullable = true) + private String blob; + @Column(name = "type", nullable = false) + private ProfileFieldType type; + @Column(name = "visibility", nullable = false) + private Visibility visibility; + @Column(name = "order_index", nullable = true) + private Integer index = 0; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the target + */ + public Long getTarget() { + return target; + } + + /** + * @param target the target to set + */ + public void setTarget(Long target) { + this.target = target; + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @param value the value to set + */ + public void setValue(String value) { + this.value = value; + } + + /** + * @return the blob + */ + public String getBlob() { + return blob; + } + + /** + * @param blob the blob to set + */ + public void setBlob(String blob) { + this.blob = blob; + } + + /** + * @return the type + */ + public ProfileFieldType getType() { + return type; + } + + /** + * @param type the type to set + */ + public void setType(ProfileFieldType type) { + this.type = type; + } + + /** + * @return the visibility + */ + public Visibility getVisibility() { + return visibility; + } + + /** + * @param visibility the visibility to set + */ + public void setVisibility(Visibility visibility) { + this.visibility = visibility; + } + + /** + * @return the index + */ + public Integer getIndex() { + return index; + } + + /** + * @param index the index to set + */ + public void setIndex(Integer index) { + this.index = index; + } + + /** + * + * @author _bastler@bstly.de + * + */ + public static class UserProfileFieldId implements Serializable { + + /** + * default serialVersionUID + */ + private static final long serialVersionUID = 1L; + private Long target; + private String name; + + /** + * @return the target + */ + public Long getTarget() { + return target; + } + + /** + * @param target the target to set + */ + public void setTarget(Long target) { + this.target = target; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /* + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof UserProfileFieldId) { + UserProfileFieldId userProfileFieldId = (UserProfileFieldId) obj; + return this.name.equals(userProfileFieldId.getName()) + && this.target.equals(userProfileFieldId.getTarget()); + } + + return false; + } + + /* + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return this.name.hashCode() * this.target.hashCode(); + } + } +} diff --git a/core/src/main/java/de/bstly/we/model/UserStatus.java b/core/src/main/java/de/bstly/we/model/UserStatus.java new file mode 100644 index 0000000..bea138c --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/UserStatus.java @@ -0,0 +1,14 @@ +/** + * + */ +package de.bstly.we.model; + +/** + * @author _bastler@bstly.de + * + */ +public enum UserStatus { + + NORMAL, SLEEP, PURGE + +} diff --git a/core/src/main/java/de/bstly/we/model/UserTotp.java b/core/src/main/java/de/bstly/we/model/UserTotp.java new file mode 100644 index 0000000..20ee48c --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/UserTotp.java @@ -0,0 +1,128 @@ +/** + * + */ +package de.bstly.we.model; + +import java.util.List; + +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "user_totps", uniqueConstraints = @UniqueConstraint(columnNames = { "target" })) +public class UserTotp implements SecondFactor { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + private Long target; + private boolean enabled; + @Column(name = "totp_secret", nullable = false) + private String secret; + @Column(name = "totp_qr_data", nullable = false) + private String qrData; + @ElementCollection + @LazyCollection(LazyCollectionOption.FALSE) + @CollectionTable(name = "user_totps_recovery_codes") + private List recoveryCodes; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the target + */ + public Long getTarget() { + return target; + } + + /** + * @param target the target to set + */ + public void setTarget(Long target) { + this.target = target; + } + + /** + * @return the enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** + * @param enabled the enabled to set + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * @return the secret + */ + public String getSecret() { + return secret; + } + + /** + * @param secret the secret to set + */ + public void setSecret(String secret) { + this.secret = secret; + } + + /** + * @return the qrData + */ + public String getQrData() { + return qrData; + } + + /** + * @param qrData the qrData to set + */ + public void setQrData(String qrData) { + this.qrData = qrData; + } + + /** + * @return the recoveryCodes + */ + public List getRecoveryCodes() { + return recoveryCodes; + } + + /** + * @param recoveryCodes the recoveryCodes to set + */ + public void setRecoveryCodes(List recoveryCodes) { + this.recoveryCodes = recoveryCodes; + } + +} diff --git a/core/src/main/java/de/bstly/we/model/Visibility.java b/core/src/main/java/de/bstly/we/model/Visibility.java new file mode 100644 index 0000000..4a18dc5 --- /dev/null +++ b/core/src/main/java/de/bstly/we/model/Visibility.java @@ -0,0 +1,14 @@ +/** + * + */ +package de.bstly.we.model; + +/** + * @author _bastler@bstly.de + * + */ +public enum Visibility { + + PRIVATE, PROTECTED, PUBLIC + +} diff --git a/core/src/main/java/de/bstly/we/repository/PermissionMappingRepository.java b/core/src/main/java/de/bstly/we/repository/PermissionMappingRepository.java new file mode 100755 index 0000000..1587c6d --- /dev/null +++ b/core/src/main/java/de/bstly/we/repository/PermissionMappingRepository.java @@ -0,0 +1,21 @@ +/** + * + */ +package de.bstly.we.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.model.PermissionMapping; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface PermissionMappingRepository + extends JpaRepository, QuerydslPredicateExecutor { + +} diff --git a/core/src/main/java/de/bstly/we/repository/PermissionRepository.java b/core/src/main/java/de/bstly/we/repository/PermissionRepository.java new file mode 100755 index 0000000..832ee38 --- /dev/null +++ b/core/src/main/java/de/bstly/we/repository/PermissionRepository.java @@ -0,0 +1,21 @@ +/** + * + */ +package de.bstly.we.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.model.Permission; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface PermissionRepository + extends JpaRepository, QuerydslPredicateExecutor { + +} diff --git a/core/src/main/java/de/bstly/we/repository/QuotaMappingRepository.java b/core/src/main/java/de/bstly/we/repository/QuotaMappingRepository.java new file mode 100644 index 0000000..604fe85 --- /dev/null +++ b/core/src/main/java/de/bstly/we/repository/QuotaMappingRepository.java @@ -0,0 +1,21 @@ +/** + * + */ +package de.bstly.we.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.model.QuotaMapping; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface QuotaMappingRepository + extends JpaRepository, QuerydslPredicateExecutor { + +} diff --git a/core/src/main/java/de/bstly/we/repository/QuotaRepository.java b/core/src/main/java/de/bstly/we/repository/QuotaRepository.java new file mode 100644 index 0000000..c746e4e --- /dev/null +++ b/core/src/main/java/de/bstly/we/repository/QuotaRepository.java @@ -0,0 +1,20 @@ +/** + * + */ +package de.bstly.we.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.model.Quota; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface QuotaRepository extends JpaRepository, QuerydslPredicateExecutor { + +} diff --git a/core/src/main/java/de/bstly/we/repository/SystemProfileFieldRepository.java b/core/src/main/java/de/bstly/we/repository/SystemProfileFieldRepository.java new file mode 100644 index 0000000..01ec3c0 --- /dev/null +++ b/core/src/main/java/de/bstly/we/repository/SystemProfileFieldRepository.java @@ -0,0 +1,21 @@ +/** + * + */ +package de.bstly.we.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.model.SystemProfileField; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface SystemProfileFieldRepository extends JpaRepository, + QuerydslPredicateExecutor { + +} diff --git a/core/src/main/java/de/bstly/we/repository/SystemPropertyRepository.java b/core/src/main/java/de/bstly/we/repository/SystemPropertyRepository.java new file mode 100755 index 0000000..1ef6d99 --- /dev/null +++ b/core/src/main/java/de/bstly/we/repository/SystemPropertyRepository.java @@ -0,0 +1,21 @@ +/** + * + */ +package de.bstly.we.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.model.SystemProperty; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface SystemPropertyRepository + extends JpaRepository, QuerydslPredicateExecutor { + +} diff --git a/core/src/main/java/de/bstly/we/repository/UserAliasRepository.java b/core/src/main/java/de/bstly/we/repository/UserAliasRepository.java new file mode 100644 index 0000000..24e0596 --- /dev/null +++ b/core/src/main/java/de/bstly/we/repository/UserAliasRepository.java @@ -0,0 +1,21 @@ +/** + * + */ +package de.bstly.we.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.model.UserAlias; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface UserAliasRepository + extends JpaRepository, QuerydslPredicateExecutor { + +} diff --git a/core/src/main/java/de/bstly/we/repository/UserDomainRepository.java b/core/src/main/java/de/bstly/we/repository/UserDomainRepository.java new file mode 100644 index 0000000..8a78127 --- /dev/null +++ b/core/src/main/java/de/bstly/we/repository/UserDomainRepository.java @@ -0,0 +1,21 @@ +/** + * + */ +package de.bstly.we.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.model.UserDomain; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface UserDomainRepository + extends JpaRepository, QuerydslPredicateExecutor { + +} diff --git a/core/src/main/java/de/bstly/we/repository/UserProfileFieldRepository.java b/core/src/main/java/de/bstly/we/repository/UserProfileFieldRepository.java new file mode 100644 index 0000000..8208cb9 --- /dev/null +++ b/core/src/main/java/de/bstly/we/repository/UserProfileFieldRepository.java @@ -0,0 +1,23 @@ +/** + * + */ +package de.bstly.we.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.model.UserProfileField; +import de.bstly.we.model.UserProfileField.UserProfileFieldId; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface UserProfileFieldRepository + extends JpaRepository, + QuerydslPredicateExecutor { + +} diff --git a/core/src/main/java/de/bstly/we/repository/UserRepository.java b/core/src/main/java/de/bstly/we/repository/UserRepository.java new file mode 100755 index 0000000..0d59077 --- /dev/null +++ b/core/src/main/java/de/bstly/we/repository/UserRepository.java @@ -0,0 +1,20 @@ +/** + * + */ +package de.bstly.we.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.model.User; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface UserRepository extends JpaRepository, QuerydslPredicateExecutor { + +} diff --git a/core/src/main/java/de/bstly/we/repository/UserTotpRepository.java b/core/src/main/java/de/bstly/we/repository/UserTotpRepository.java new file mode 100644 index 0000000..e8c5d66 --- /dev/null +++ b/core/src/main/java/de/bstly/we/repository/UserTotpRepository.java @@ -0,0 +1,20 @@ +/** + * + */ +package de.bstly.we.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.model.UserTotp; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface UserTotpRepository extends JpaRepository, QuerydslPredicateExecutor { + +} diff --git a/core/src/main/java/de/bstly/we/security/LocalAuthenticationEntryPoint.java b/core/src/main/java/de/bstly/we/security/LocalAuthenticationEntryPoint.java new file mode 100644 index 0000000..b54e6f1 --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/LocalAuthenticationEntryPoint.java @@ -0,0 +1,60 @@ +/** + * + */ +package de.bstly.we.security; + +import java.io.IOException; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.util.matcher.RequestMatcher; + +import com.google.common.collect.Lists; + +/** + * + * @author _bastler@bstly.de + * + */ +public class LocalAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint { + + private List requestMatchers = Lists.newArrayList(); + + /** + * + * @param loginUrl + */ + public LocalAuthenticationEntryPoint(String loginFormUrl) { + super(loginFormUrl); + }; + + /* + * @see + * org.springframework.security.web.AuthenticationEntryPoint#commence(javax. + * servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, + * org.springframework.security.core.AuthenticationException) + */ + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + + for (RequestMatcher matcher : requestMatchers) { + if (matcher.matches(request)) { + super.commence(request, response, authException); + return; + } + } + + response.setStatus(HttpStatus.FORBIDDEN.value()); + } + + public void addRequestMatcher(RequestMatcher requestMatcher) { + this.requestMatchers.add(requestMatcher); + } +} diff --git a/core/src/main/java/de/bstly/we/security/LocalAuthenticationProvider.java b/core/src/main/java/de/bstly/we/security/LocalAuthenticationProvider.java new file mode 100755 index 0000000..77c580c --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/LocalAuthenticationProvider.java @@ -0,0 +1,133 @@ +/** + * + */ +package de.bstly.we.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +import de.bstly.we.security.businesslogic.SecondFactorProvider; +import de.bstly.we.security.businesslogic.SecondFactorProviderManager; +import de.bstly.we.security.model.LocalUserDetails; +import de.bstly.we.security.token.LocalAnonymousAuthenticationToken; +import de.bstly.we.security.token.LocalSecondFactorAuthenticationToken; + +/** + * + * @author _bastler@bstly.de + * + */ +public class LocalAuthenticationProvider extends DaoAuthenticationProvider { + + @Autowired + private SecondFactorProviderManager secondFactorProviderManager; + + /* + * + * @see org.springframework.security.authentication.dao. + * AbstractUserDetailsAuthenticationProvider#authenticate(org.springframework. + * security.core.Authentication) + */ + @Override + public Authentication authenticate(Authentication auth) throws AuthenticationException { + + if (auth instanceof UsernamePasswordAuthenticationToken) { + auth = super.authenticate(auth); + if (auth.getPrincipal() instanceof LocalUserDetails) { + LocalUserDetails details = (LocalUserDetails) auth.getPrincipal(); + if (!secondFactorProviderManager.getEnabled(details.getUserId()).isEmpty()) { + PreAuthenticatedAuthenticationToken newAuth = new PreAuthenticatedAuthenticationToken( + details, "", AuthorityUtils.createAuthorityList("ROLE_PRE_AUTH_USER")); + newAuth.setAuthenticated(false); + return newAuth; + } + + return new UsernamePasswordAuthenticationToken(details, auth.getCredentials(), + details.getAuthorities()); + } + + } else if (auth instanceof LocalSecondFactorAuthenticationToken) { + LocalSecondFactorAuthenticationToken secondFactorAuth = (LocalSecondFactorAuthenticationToken) auth; + if (auth.getPrincipal() instanceof LocalUserDetails) { + LocalUserDetails details = (LocalUserDetails) auth.getPrincipal(); + + SecondFactorProvider provider = secondFactorProviderManager + .getProvider(secondFactorAuth.getProvider()); + + if (provider == null) { + throw new SecondFactorAuthenticationException( + "invalid provider: " + secondFactorAuth.getProvider(), details); + } + + if (!provider.isEnabled(details.getUserId())) { + throw new SecondFactorAuthenticationException( + "provider is disabled: " + secondFactorAuth.getProvider(), details); + } + + if (provider.validate(details.getUserId(), secondFactorAuth.getCode())) { + return new UsernamePasswordAuthenticationToken(details, auth.getCredentials(), + details.getAuthorities()); + } else { + PreAuthenticatedAuthenticationToken newAuth = new PreAuthenticatedAuthenticationToken( + details, "", auth.getAuthorities()); + newAuth.setAuthenticated(false); + return newAuth; + } + } + } + + return auth; + } + + public boolean supports(Class authentication) { + return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)) + || (LocalSecondFactorAuthenticationToken.class.isAssignableFrom(authentication)) + || (LocalAnonymousAuthenticationToken.class.isAssignableFrom(authentication)); + } + + /** + * + * @author _bastler@bstly.de + * + */ + public static class SecondFactorAuthenticationException + extends InsufficientAuthenticationException { + + private LocalUserDetails principal; + + /** + * serialVersionUID + */ + private static final long serialVersionUID = 1L; + + /** + * @param msg + */ + public SecondFactorAuthenticationException(String message, LocalUserDetails principal) { + super(message); + this.principal = principal; + } + + /** + * @return the principal + */ + public LocalUserDetails getPrincipal() { + return principal; + } + + /** + * @param principal the principal to set + */ + public void setPrincipal(LocalUserDetails principal) { + this.principal = principal; + } + + } + +} diff --git a/core/src/main/java/de/bstly/we/security/LocalRememberMeServices.java b/core/src/main/java/de/bstly/we/security/LocalRememberMeServices.java new file mode 100644 index 0000000..ae779ec --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/LocalRememberMeServices.java @@ -0,0 +1,49 @@ +/** + * + */ +package de.bstly.we.security; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices; +import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; + +/** + * + * @author _bastler@bstly.de + * + */ +public class LocalRememberMeServices extends PersistentTokenBasedRememberMeServices { + + /** + * @param key + * @param userDetailsService + * @param tokenRepository + */ + public LocalRememberMeServices(String key, UserDetailsService userDetailsService, + PersistentTokenRepository tokenRepository) { + super(key, userDetailsService, tokenRepository); + } + + /* + * + * @see org.springframework.security.web.authentication.rememberme. + * AbstractRememberMeServices#rememberMeRequested(javax.servlet.http. + * HttpServletRequest, java.lang.String) + */ + @Override + protected boolean rememberMeRequested(HttpServletRequest request, String parameter) { + Object value = request.getAttribute(parameter); + if (value != null) { + String paramValue = value.toString(); + if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") + || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) { + return true; + } + } + + return super.rememberMeRequested(request, parameter); + } + +} diff --git a/core/src/main/java/de/bstly/we/security/LocalServletContextListener.java b/core/src/main/java/de/bstly/we/security/LocalServletContextListener.java new file mode 100644 index 0000000..5d70b90 --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/LocalServletContextListener.java @@ -0,0 +1,12 @@ +/** + * + */ +package de.bstly.we.security; + +/** + * @author _bastler@bstly.de + * + */ +public class LocalServletContextListener { + +} diff --git a/core/src/main/java/de/bstly/we/security/LocalUserDetailsService.java b/core/src/main/java/de/bstly/we/security/LocalUserDetailsService.java new file mode 100755 index 0000000..d0b4b86 --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/LocalUserDetailsService.java @@ -0,0 +1,106 @@ +package de.bstly.we.security; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.LockedException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.model.Permission; +import de.bstly.we.model.User; +import de.bstly.we.security.model.LocalUserDetails; + +/** + * + * @author _bastler@bstly.de + * + */ +@Service("users") +public class LocalUserDetailsService implements UserDetailsService { + + @Autowired + private UserManager userManager; + @Autowired + private PermissionManager permissionManager; + + /* + * (non-Javadoc) + * + * @see org.springframework.security.core.userdetails.UserDetailsService# + * loadUserByUsername(java.lang.String) + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userManager.getByUsername(username); + + if (user != null) { + String password = userManager.getPasswordHash(user.getId()); + + if (password == null) { + throw new AuthenticationCredentialsNotFoundException( + "No password found: " + username); + } + + if (user.isDisabled()) { + throw new DisabledException("User is disabled: " + username); + } + + if (user.isLocked()) { + throw new LockedException("User is locked: " + username); + } + + Set authorities = getAuthoritiesForUser(user.getId()); + + // ignore and use status +// boolean onlyAddons = true; +// for (Permission permission : permissionManager +// .getNotExpiresByTargetIgnoreStart(user.getId())) { +// if (!permission.isAddon()) { +// onlyAddons = false; +// break; +// } +// } +// +// if (authorities.isEmpty()) { +// throw new AccountExpiredException("User is expired: " + username); +// } +// +// if (onlyAddons) { +// throw new AccountExpiredException("User is expired: " + username); +// } + + // Create user details + LocalUserDetails userDetails = new LocalUserDetails(user.getId(), user.getUsername(), + password, authorities); + + return userDetails; + } + + // User was not found + throw new UsernameNotFoundException("User not found for: " + username); + } + + /** + * + * @param userId + * @return + */ + public Set getAuthoritiesForUser(Long userId) { + Set authorities = new HashSet(); + for (Permission permission : permissionManager.getNotExpiresByTarget(userId)) { + authorities.add(new SimpleGrantedAuthority(permission.getName())); + } + return authorities; + } + +} diff --git a/core/src/main/java/de/bstly/we/security/SecurityConfig.java b/core/src/main/java/de/bstly/we/security/SecurityConfig.java new file mode 100755 index 0000000..45fe180 --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/SecurityConfig.java @@ -0,0 +1,294 @@ +/** + * + */ +package de.bstly.we.security; + +import java.util.Collections; +import java.util.List; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; +import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; +import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices; +import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; +import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy; +import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import de.bstly.we.security.filter.FormSecondFactorAuthenticationFilter; +import de.bstly.we.security.filter.LocalAnonymousAuthenticationFilter; +import de.bstly.we.security.filter.RestAuthenticationFilter; +import de.bstly.we.security.filter.RestSecondFactorAuthenticationFilter; +import de.bstly.we.security.handler.FormAuthenticationSuccessHandler; +import de.bstly.we.security.handler.LocalAccessDeniedHandler; +import de.bstly.we.security.handler.RestAuthenticationFailureHandler; +import de.bstly.we.security.handler.RestAuthenticationSuccessHandler; +import dev.samstevens.totp.code.HashingAlgorithm; + +/** + * @author _bastler@bstly.de + * + */ +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private LocalUserDetailsService localUserDetailsService; + @Autowired + private DataSource dataSource; + @Autowired + private RestAuthenticationSuccessHandler restAuthenticationSuccessHandler; + @Autowired + private RestAuthenticationFailureHandler restAuthenticationFailureHandler; + @Autowired + private LocalAccessDeniedHandler localAccessDeniedHandler; + + @Value("${server.servlet.session.cookie.secure:false}") + private boolean secureCookie; + @Value("${loginUrl:/login}") + private String loginUrl; + @Value("${secondFactorUrl:/2fa}") + private String secondFactorUrl; + @Value("${loginTargetUrl:/}") + private String loginTargetUrl; + @Value("${cors.disable:false}") + private boolean disableCors; + @Value("${cors.allowedOriginPatterns:}") + private List allowedOriginPatterns; + + public static final String KEEP_PARAM = "keep"; + + /** + * @param auth + * @throws Exception + */ + @Autowired + public void configureAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth.authenticationProvider(daoAuthenticationProvider()); + } + + /* + * + * @see org.springframework.security.config.annotation.web.configuration. + * WebSecurityConfigurerAdapter#configure(org.springframework.security.config. + * annotation.web.builders.HttpSecurity) + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + http + // anonymous + .anonymous().authenticationFilter(localAnonymousAuthenticationFilter()).and() + // session + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS) + .sessionAuthenticationStrategy(new SessionFixationProtectionStrategy()).and() + // disable deprectated xss protection + .headers().xssProtection().disable().and() + // form login + .formLogin().loginPage(loginUrl).usernameParameter("username") + .passwordParameter("password").loginProcessingUrl("/auth/login") + .defaultSuccessUrl(loginTargetUrl) + .successHandler(formAuthenticationSuccessHandler()) + .failureHandler(new SimpleUrlAuthenticationFailureHandler(loginUrl + "?error")) + .and() + // remember me + .rememberMe().rememberMeServices(rememberMeServices()).and() + // form totp + .addFilterBefore(formSecondFactorAuthenticationFilter(), + LocalAnonymousAuthenticationFilter.class) + // rest login + .addFilterBefore(restAuthenticationFilter(), + UsernamePasswordAuthenticationFilter.class) + // rest totp + .addFilterAfter(restSecondFactorAuthenticationFilter(), + UsernamePasswordAuthenticationFilter.class) + // Logout + .logout().logoutUrl("/auth/logout") + .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()).and() + // exception + .exceptionHandling().accessDeniedHandler(localAccessDeniedHandler) + .authenticationEntryPoint(localAuthenticationEntryPoint()).and() + // crsf + .csrf().disable() + // x-frame + .headers().frameOptions().disable().referrerPolicy(ReferrerPolicy.UNSAFE_URL); + + if (disableCors) { + http.cors().disable(); + } else if (!allowedOriginPatterns.isEmpty()) { + // cors + http.cors().configurationSource(corsConfigurationSource()); + } + } + + /** + * + * @return + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(allowedOriginPatterns); + configuration.setAllowedMethods(Collections.singletonList("*")); + configuration.setAllowCredentials(true); + configuration.setAllowedHeaders(Collections.singletonList("*")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + /** + * + * @return + */ + @Bean + public DaoAuthenticationProvider daoAuthenticationProvider() { + LocalAuthenticationProvider provider = new LocalAuthenticationProvider(); + provider.setUserDetailsService(localUserDetailsService); + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + + /** + * + * @return + */ + @Bean(name = "passwordEncoder") + public Argon2PasswordEncoder passwordEncoder() { + return new Argon2PasswordEncoder(); + } + + /** + * + * @return + */ + @Bean + public LocalAnonymousAuthenticationFilter localAnonymousAuthenticationFilter() { + return new LocalAnonymousAuthenticationFilter(); + } + + /** + * + * @return + */ + @Bean + public LocalAuthenticationEntryPoint localAuthenticationEntryPoint() { + LocalAuthenticationEntryPoint localAuthenticationEntryPoint = new LocalAuthenticationEntryPoint( + loginUrl); + localAuthenticationEntryPoint + .addRequestMatcher(new AntPathRequestMatcher("/oidc/authorize")); + + return localAuthenticationEntryPoint; + } + + /** + * + * @return + */ + @Bean + public FormAuthenticationSuccessHandler formAuthenticationSuccessHandler() { + FormAuthenticationSuccessHandler formAuthenticationSuccessHandler = new FormAuthenticationSuccessHandler( + loginTargetUrl, SecurityConfig.KEEP_PARAM); + formAuthenticationSuccessHandler.setTotpRedirectUrl(secondFactorUrl); + formAuthenticationSuccessHandler.setTargetUrlParameter("forward"); + formAuthenticationSuccessHandler + .addRequestMatcher(new AntPathRequestMatcher("/oidc/authorize")); + return formAuthenticationSuccessHandler; + } + + /** + * + * @return + * @throws Exception + */ + @Bean + public FormSecondFactorAuthenticationFilter formSecondFactorAuthenticationFilter() + throws Exception { + FormSecondFactorAuthenticationFilter formSecondFactorAuthenticationFilter = new FormSecondFactorAuthenticationFilter( + "/auth/login/2fa"); + formSecondFactorAuthenticationFilter.setAuthenticationManager(authenticationManager()); + formSecondFactorAuthenticationFilter + .setAuthenticationSuccessHandler(formAuthenticationSuccessHandler()); + formSecondFactorAuthenticationFilter.setRememberMeServices(rememberMeServices()); + return formSecondFactorAuthenticationFilter; + } + + /** + * + * @return + * @throws Exception + */ + @Bean + public RestAuthenticationFilter restAuthenticationFilter() throws Exception { + RestAuthenticationFilter restAuthenticationFilter = new RestAuthenticationFilter( + "/auth/restlogin"); + restAuthenticationFilter.setAuthenticationManager(authenticationManager()); + restAuthenticationFilter.setAuthenticationSuccessHandler(restAuthenticationSuccessHandler); + restAuthenticationFilter.setAuthenticationFailureHandler(restAuthenticationFailureHandler); + return restAuthenticationFilter; + } + + /** + * + * @return + * @throws Exception + */ + @Bean + public RestSecondFactorAuthenticationFilter restSecondFactorAuthenticationFilter() + throws Exception { + RestSecondFactorAuthenticationFilter restSecondFactorAuthenticationFilter = new RestSecondFactorAuthenticationFilter( + "/auth/restlogin/2fa"); + restSecondFactorAuthenticationFilter.setAuthenticationManager(authenticationManager()); + restSecondFactorAuthenticationFilter + .setAuthenticationSuccessHandler(restAuthenticationSuccessHandler); + restSecondFactorAuthenticationFilter + .setAuthenticationFailureHandler(restAuthenticationFailureHandler); + restSecondFactorAuthenticationFilter.setRememberMeServices(rememberMeServices()); + return restSecondFactorAuthenticationFilter; + } + + /** + * + * @return + */ + @Bean + public HashingAlgorithm hashingAlgorithm() { + return HashingAlgorithm.SHA256; + } + + @Bean + public PersistentTokenRepository persistentTokenRepository() { + JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); + tokenRepository.setDataSource(dataSource); + return tokenRepository; + } + + @Bean + public RememberMeServices rememberMeServices() { + PersistentTokenBasedRememberMeServices rememberMeServices = new LocalRememberMeServices( + KEEP_PARAM, localUserDetailsService, persistentTokenRepository()); + rememberMeServices.setCookieName("SESSION_" + KEEP_PARAM.toUpperCase()); + rememberMeServices.setParameter(KEEP_PARAM); + rememberMeServices.setUseSecureCookie(secureCookie); + return rememberMeServices; + } + +} diff --git a/core/src/main/java/de/bstly/we/security/businesslogic/SecondFactorProvider.java b/core/src/main/java/de/bstly/we/security/businesslogic/SecondFactorProvider.java new file mode 100644 index 0000000..56cd3cb --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/businesslogic/SecondFactorProvider.java @@ -0,0 +1,70 @@ +/** + * + */ +package de.bstly.we.security.businesslogic; + +import de.bstly.we.businesslogic.UserDataProvider; +import de.bstly.we.model.SecondFactor; + +/** + * @author _bastler@bstly.de + * + */ +public interface SecondFactorProvider extends UserDataProvider { + + /** + * + * @return + */ + String getId(); + + /** + * + * @return + */ + boolean supports(String provider); + + /** + * + * @param userId + * @return + */ + boolean isEnabled(Long userId); + + /** + * + * @param userId + * @param code + * @return + */ + boolean validate(Long userId, String code); + + /** + * + * @param userId + * @return + */ + T get(Long userId); + + /** + * + * @param userId + * @return + */ + T create(Long userId); + + /** + * + * @param userId + * @param code + * @return + */ + boolean enable(Long userId, String code); + + /** + * + * @param userId + */ + void delete(Long userId); + +} diff --git a/core/src/main/java/de/bstly/we/security/businesslogic/SecondFactorProviderManager.java b/core/src/main/java/de/bstly/we/security/businesslogic/SecondFactorProviderManager.java new file mode 100644 index 0000000..ccab806 --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/businesslogic/SecondFactorProviderManager.java @@ -0,0 +1,84 @@ +/** + * + */ +package de.bstly.we.security.businesslogic; + +import java.util.List; + +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import com.google.common.collect.Lists; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class SecondFactorProviderManager implements SmartInitializingSingleton { + + @Autowired + private ApplicationContext context; + + /** + * Second Factor Provider + */ + private List> providers; + + /* + * @see org.springframework.beans.factory.SmartInitializingSingleton# + * afterSingletonsInstantiated() + */ + @Override + public void afterSingletonsInstantiated() { + providers = Lists.newArrayList(); + + for (SecondFactorProvider provider : context.getBeansOfType(SecondFactorProvider.class) + .values()) { + providers.add(provider); + } + } + + /** + * + * @return + */ + public List> getAll() { + return providers; + } + + /** + * + * @param userId + * @return + */ + public List> getEnabled(Long userId) { + List> result = Lists.newArrayList(); + + for (SecondFactorProvider provider : providers) { + if (provider.isEnabled(userId)) { + result.add(provider); + } + } + + return result; + } + + /** + * + * @param id + * @return + */ + public SecondFactorProvider getProvider(String id) { + for (SecondFactorProvider provider : providers) { + if (provider.supports(id)) { + return provider; + } + } + return null; + } + +} diff --git a/core/src/main/java/de/bstly/we/security/businesslogic/SecondFactorRequestProvider.java b/core/src/main/java/de/bstly/we/security/businesslogic/SecondFactorRequestProvider.java new file mode 100644 index 0000000..cb86706 --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/businesslogic/SecondFactorRequestProvider.java @@ -0,0 +1,21 @@ +/** + * + */ +package de.bstly.we.security.businesslogic; + +import de.bstly.we.model.SecondFactor; + +/** + * @author _bastler@bstly.de + * + */ +public interface SecondFactorRequestProvider + extends SecondFactorProvider { + + /** + * + * @param userId + */ + void request(Long userId); + +} diff --git a/core/src/main/java/de/bstly/we/security/filter/FormSecondFactorAuthenticationFilter.java b/core/src/main/java/de/bstly/we/security/filter/FormSecondFactorAuthenticationFilter.java new file mode 100644 index 0000000..dc5c53e --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/filter/FormSecondFactorAuthenticationFilter.java @@ -0,0 +1,110 @@ +/** + * + */ +package de.bstly.we.security.filter; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.util.StringUtils; + +import de.bstly.we.security.model.LocalUserDetails; +import de.bstly.we.security.token.LocalSecondFactorAuthenticationToken; + +/** + * + * @author _bastler@bstly.de + * + */ +public class FormSecondFactorAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + + public static final String SPRING_SECURITY_FORM_2FA_PROVIDER_KEY = "provider"; + public static final String SPRING_SECURITY_FORM_2FA_CODE_KEY = "code"; + + /** + * + */ + public FormSecondFactorAuthenticationFilter(String defaultFilterProcessesUrl) { + super(defaultFilterProcessesUrl); + } + + /* + * @see org.springframework.security.web.authentication. + * AbstractAuthenticationProcessingFilter#attemptAuthentication(javax.servlet. + * http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + @Override + public Authentication attemptAuthentication(HttpServletRequest request, + HttpServletResponse response) + throws AuthenticationException, IOException, ServletException { + + if (!request.getMethod().equals("POST")) { + throw new AuthenticationServiceException( + "Authentication method not supported: " + request.getMethod()); + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null + || !(authentication instanceof PreAuthenticatedAuthenticationToken) + || !(authentication.getPrincipal() instanceof LocalUserDetails)) { + throw new InsufficientAuthenticationException("login first!"); + } + + String provider = obtainProvider(request); + String code = obtainCode(request); + + if (!StringUtils.hasText(provider) || !StringUtils.hasText(code)) { + throw new AuthenticationCredentialsNotFoundException("Bad request"); + } + + LocalUserDetails details = (LocalUserDetails) authentication.getPrincipal(); + authentication = new LocalSecondFactorAuthenticationToken(details, provider, code); + return this.getAuthenticationManager().authenticate(authentication); + + } + + /* + * + * @see org.springframework.security.web.authentication. + * AbstractAuthenticationProcessingFilter#unsuccessfulAuthentication(javax. + * servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, + * org.springframework.security.core.AuthenticationException) + */ + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, + HttpServletResponse response, AuthenticationException failed) + throws IOException, ServletException { + if (logger.isDebugEnabled()) { + logger.debug("Authentication request failed: " + failed.toString(), failed); + logger.debug("Updated SecurityContextHolder to contain null Authentication"); + logger.debug("Delegating to authentication failure handler " + getFailureHandler()); + } + + getRememberMeServices().loginFail(request, response); + getFailureHandler().onAuthenticationFailure(request, response, failed); + } + + @Nullable + protected String obtainProvider(HttpServletRequest request) { + return request.getParameter(SPRING_SECURITY_FORM_2FA_PROVIDER_KEY); + } + + @Nullable + protected String obtainCode(HttpServletRequest request) { + return request.getParameter(SPRING_SECURITY_FORM_2FA_CODE_KEY); + } + +} diff --git a/core/src/main/java/de/bstly/we/security/filter/LocalAnonymousAuthenticationFilter.java b/core/src/main/java/de/bstly/we/security/filter/LocalAnonymousAuthenticationFilter.java new file mode 100755 index 0000000..f02a49d --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/filter/LocalAnonymousAuthenticationFilter.java @@ -0,0 +1,68 @@ +/** + * + */ +package de.bstly.we.security.filter; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; + +import de.bstly.we.security.model.LocalUserDetails; +import de.bstly.we.security.token.LocalAnonymousAuthenticationToken; + +/** + * + * @author _bastler@bstly.de + * + */ +public class LocalAnonymousAuthenticationFilter extends AnonymousAuthenticationFilter { + + public static final String KEY = "anonymous"; + + /** + * @param key + */ + public LocalAnonymousAuthenticationFilter() { + super(KEY); + } + + /* + * + * @see + * org.springframework.security.web.authentication.AnonymousAuthenticationFilter + * #doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, + * javax.servlet.FilterChain) + */ + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + if (SecurityContextHolder.getContext().getAuthentication() == null) { + SecurityContextHolder.getContext() + .setAuthentication(createAuthentication((HttpServletRequest) req)); + } + chain.doFilter(req, res); + } + + /* + * + * @see + * org.springframework.security.web.authentication.AnonymousAuthenticationFilter + * #createAuthentication(javax.servlet.http.HttpServletRequest) + */ + @Override + protected Authentication createAuthentication(HttpServletRequest request) { + Authentication authentication = new LocalAnonymousAuthenticationToken( + new LocalUserDetails(-1L, LocalAnonymousAuthenticationToken.ANONYMOUS_USERNAME, "", + LocalAnonymousAuthenticationToken.AUTHORITIES)); + authentication.setAuthenticated(false); + return authentication; + } +} diff --git a/core/src/main/java/de/bstly/we/security/filter/RestAuthenticationFilter.java b/core/src/main/java/de/bstly/we/security/filter/RestAuthenticationFilter.java new file mode 100644 index 0000000..a7cdb2d --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/filter/RestAuthenticationFilter.java @@ -0,0 +1,128 @@ +/** + * + */ +package de.bstly.we.security.filter; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import de.bstly.we.security.SecurityConfig; + +/** + * + * @author _bastler@bstly.de + * + */ +public class RestAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + + private String usernameKey = UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY; + private String passwordKey = UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY; + + /** + * + */ + public RestAuthenticationFilter(String defaultFilterProcessesUrl) { + super(defaultFilterProcessesUrl); + } + + /* + * @see org.springframework.security.web.authentication. + * AbstractAuthenticationProcessingFilter#attemptAuthentication(javax.servlet. + * http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + @Override + public Authentication attemptAuthentication(HttpServletRequest request, + HttpServletResponse response) + throws AuthenticationException, IOException, ServletException { + + if (!request.getMethod().equals("POST")) { + throw new AuthenticationServiceException( + "Authentication method not supported: " + request.getMethod()); + } + + try { + JsonElement jsonElement; + jsonElement = JsonParser.parseReader(request.getReader()); + + if (!jsonElement.isJsonObject()) { + throw new AuthenticationCredentialsNotFoundException("Bad request"); + } + + JsonObject loginModel = jsonElement.getAsJsonObject(); + + if (!loginModel.has(usernameKey) || !loginModel.has(passwordKey)) { + throw new AuthenticationCredentialsNotFoundException("Bad request"); + } + + if (loginModel.has(SecurityConfig.KEEP_PARAM) + && loginModel.get(SecurityConfig.KEEP_PARAM).isJsonPrimitive()) { + request.setAttribute(SecurityConfig.KEEP_PARAM, + loginModel.get(SecurityConfig.KEEP_PARAM).getAsString()); + } + + Authentication authRequest = new UsernamePasswordAuthenticationToken( + loginModel.get(usernameKey).getAsString(), + loginModel.get(passwordKey).getAsString()); + return this.getAuthenticationManager().authenticate(authRequest); + } catch (JsonMappingException | JsonParseException exception) { + throw new AuthenticationCredentialsNotFoundException("Bad request"); + } + } + + /** + * Sets the key name which will be used to obtain the username from the login + * request. + * + * @param usernameKey the key name. Defaults to "username". + */ + public void setUsernameKey(String usernameKey) { + Assert.hasText(usernameKey, "Username key must not be empty or null"); + this.usernameKey = usernameKey; + } + + /** + * Sets the key name which will be used to obtain the password from the login + * request.. + * + * @param passwordKey the key name. Defaults to "password". + */ + public void setPasswordParameter(String passwordKey) { + Assert.hasText(passwordKey, "Password key must not be empty or null"); + this.passwordKey = passwordKey; + } + + /** + * + * @return + */ + public final String getUsernameKey() { + return usernameKey; + } + + /** + * + * @return + */ + public final String getPasswordKey() { + return passwordKey; + } + +} \ No newline at end of file diff --git a/core/src/main/java/de/bstly/we/security/filter/RestSecondFactorAuthenticationFilter.java b/core/src/main/java/de/bstly/we/security/filter/RestSecondFactorAuthenticationFilter.java new file mode 100644 index 0000000..bc3bd7e --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/filter/RestSecondFactorAuthenticationFilter.java @@ -0,0 +1,117 @@ + +package de.bstly.we.security.filter; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import de.bstly.we.security.SecurityConfig; +import de.bstly.we.security.model.LocalUserDetails; +import de.bstly.we.security.token.LocalSecondFactorAuthenticationToken; + +/** + * + * @author _bastler@bstly.de + * + */ +public class RestSecondFactorAuthenticationFilter extends FormSecondFactorAuthenticationFilter { + + /** + * + */ + public RestSecondFactorAuthenticationFilter(String defaultFilterProcessesUrl) { + super(defaultFilterProcessesUrl); + } + + /* + * @see org.springframework.security.web.authentication. + * AbstractAuthenticationProcessingFilter#attemptAuthentication(javax.servlet. + * http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + @Override + public Authentication attemptAuthentication(HttpServletRequest request, + HttpServletResponse response) + throws AuthenticationException, IOException, ServletException { + + if (!request.getMethod().equals("POST")) { + throw new AuthenticationServiceException( + "Authentication method not supported: " + request.getMethod()); + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null + || !(authentication instanceof PreAuthenticatedAuthenticationToken) + || !(authentication.getPrincipal() instanceof LocalUserDetails)) { + throw new InsufficientAuthenticationException("login first!"); + } + + try { + JsonElement jsonElement; + jsonElement = JsonParser.parseReader(request.getReader()); + + if (!jsonElement.isJsonObject()) { + throw new AuthenticationCredentialsNotFoundException("Bad request"); + } + + JsonObject model2FA = jsonElement.getAsJsonObject(); + + if (!model2FA.has(SPRING_SECURITY_FORM_2FA_PROVIDER_KEY) + || !model2FA.has(SPRING_SECURITY_FORM_2FA_CODE_KEY)) { + throw new AuthenticationCredentialsNotFoundException("Bad request"); + } + + String provider = model2FA.get(SPRING_SECURITY_FORM_2FA_PROVIDER_KEY).getAsString(); + String code = model2FA.get(SPRING_SECURITY_FORM_2FA_CODE_KEY).getAsString(); + + if (model2FA.has(SecurityConfig.KEEP_PARAM) + && model2FA.get(SecurityConfig.KEEP_PARAM).isJsonPrimitive()) { + request.setAttribute(SecurityConfig.KEEP_PARAM, + model2FA.get(SecurityConfig.KEEP_PARAM).getAsString()); + } + + LocalUserDetails details = (LocalUserDetails) authentication.getPrincipal(); + authentication = new LocalSecondFactorAuthenticationToken(details, provider, code); + return this.getAuthenticationManager().authenticate(authentication); + } catch (JsonMappingException | JsonParseException exception) { + throw new AuthenticationCredentialsNotFoundException("Bad request"); + } + } + + /* + * + * @see org.springframework.security.web.authentication. + * AbstractAuthenticationProcessingFilter#unsuccessfulAuthentication(javax. + * servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, + * org.springframework.security.core.AuthenticationException) + */ + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, + HttpServletResponse response, AuthenticationException failed) + throws IOException, ServletException { + if (logger.isDebugEnabled()) { + logger.debug("Authentication request failed: " + failed.toString(), failed); + logger.debug("Updated SecurityContextHolder to contain null Authentication"); + logger.debug("Delegating to authentication failure handler " + getFailureHandler()); + } + + getRememberMeServices().loginFail(request, response); + getFailureHandler().onAuthenticationFailure(request, response, failed); + } +} \ No newline at end of file diff --git a/core/src/main/java/de/bstly/we/security/handler/FormAuthenticationSuccessHandler.java b/core/src/main/java/de/bstly/we/security/handler/FormAuthenticationSuccessHandler.java new file mode 100644 index 0000000..c0321d8 --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/handler/FormAuthenticationSuccessHandler.java @@ -0,0 +1,124 @@ +package de.bstly.we.security.handler; + +import java.io.IOException; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.security.web.util.UrlUtils; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +import com.google.common.collect.Lists; + +import de.bstly.we.security.token.LocalSecondFactorAuthenticationToken; + +/** + * + * @author _bastler@bstly.de + * + */ +public class FormAuthenticationSuccessHandler + extends SavedRequestAwareAuthenticationSuccessHandler { + + @Autowired + protected RememberMeServices rememberMeServices; + + private String totpRedirectUrl; + private String rememberMeParameter; + + private List requestMatchers = Lists.newArrayList(); + + /** + * + */ + public FormAuthenticationSuccessHandler(String rememberMeParameter) { + this.rememberMeParameter = rememberMeParameter; + } + + /** + * + * @param defaultTargetUrl + */ + public FormAuthenticationSuccessHandler(String defaultTargetUrl, String rememberMeParameter) { + setDefaultTargetUrl(defaultTargetUrl); + this.rememberMeParameter = rememberMeParameter; + } + + /* + * + * @see org.springframework.security.web.authentication. + * SavedRequestAwareAuthenticationSuccessHandler#onAuthenticationSuccess(javax. + * servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, + * org.springframework.security.core.Authentication) + */ + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws ServletException, IOException { + + if (!authentication.isAuthenticated() + && authentication instanceof PreAuthenticatedAuthenticationToken) { + rememberMeServices.loginFail(request, response); + boolean keep = false; + String paramValue = request.getParameter(rememberMeParameter); + if (paramValue != null) { + if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") + || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) { + keep = true; + } + } + + if (authentication.getAuthorities() != null && authentication.getAuthorities() + .containsAll(LocalSecondFactorAuthenticationToken.AUTHORITIES)) { + getRedirectStrategy().sendRedirect(request, response, + totpRedirectUrl + "?error" + (keep ? "&keep" : "")); + } else { + getRedirectStrategy().sendRedirect(request, response, + totpRedirectUrl + (keep ? "?keep" : "")); + } + } else { + rememberMeServices.loginSuccess(request, response, authentication); + + for (RequestMatcher matcher : requestMatchers) { + if (matcher.matches(request)) { + getRedirectStrategy().sendRedirect(request, response, + request.getRequestURI() + (request.getQueryString() != null + ? "?" + request.getQueryString() + : "")); + return; + } + } + + super.onAuthenticationSuccess(request, response, authentication); + } + } + + /** + * + * @param totpRedirectUrl + */ + public void setTotpRedirectUrl(String totpRedirectUrl) { + Assert.isTrue(UrlUtils.isValidRedirectUrl(totpRedirectUrl), + () -> "'" + totpRedirectUrl + "' is not a valid redirect URL"); + this.totpRedirectUrl = totpRedirectUrl; + } + + public void addRequestMatcher(RequestMatcher requestMatcher) { + this.requestMatchers.add(requestMatcher); + } + + /** + * @return the rememberMeParameter + */ + public String getRememberMeParameter() { + return rememberMeParameter; + } + +} diff --git a/core/src/main/java/de/bstly/we/security/handler/LocalAccessDeniedHandler.java b/core/src/main/java/de/bstly/we/security/handler/LocalAccessDeniedHandler.java new file mode 100644 index 0000000..53c0d31 --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/handler/LocalAccessDeniedHandler.java @@ -0,0 +1,16 @@ +/** + * + */ +package de.bstly.we.security.handler; + +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.stereotype.Component; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class LocalAccessDeniedHandler extends AccessDeniedHandlerImpl { + +} diff --git a/core/src/main/java/de/bstly/we/security/handler/RestAuthenticationFailureHandler.java b/core/src/main/java/de/bstly/we/security/handler/RestAuthenticationFailureHandler.java new file mode 100644 index 0000000..c77b2ec --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/handler/RestAuthenticationFailureHandler.java @@ -0,0 +1,35 @@ +package de.bstly.we.security.handler; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler { + + /* + * @see + * org.springframework.security.web.authentication.AuthenticationFailureHandler# + * onAuthenticationFailure(javax.servlet.http.HttpServletRequest, + * javax.servlet.http.HttpServletResponse, + * org.springframework.security.core.AuthenticationException) + */ + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); + } + +} \ No newline at end of file diff --git a/core/src/main/java/de/bstly/we/security/handler/RestAuthenticationSuccessHandler.java b/core/src/main/java/de/bstly/we/security/handler/RestAuthenticationSuccessHandler.java new file mode 100644 index 0000000..7cf878d --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/handler/RestAuthenticationSuccessHandler.java @@ -0,0 +1,54 @@ +package de.bstly.we.security.handler; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import de.bstly.we.security.SecurityConfig; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class RestAuthenticationSuccessHandler extends FormAuthenticationSuccessHandler { + + /** + * @param rememberMeParameter + */ + public RestAuthenticationSuccessHandler() { + super(SecurityConfig.KEEP_PARAM); + } + + /* + * + * @see org.springframework.security.web.authentication. + * SimpleUrlAuthenticationSuccessHandler#onAuthenticationSuccess(javax.servlet. + * http.HttpServletRequest, javax.servlet.http.HttpServletResponse, + * org.springframework.security.core.Authentication) + */ + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + if (!authentication.isAuthenticated()) { + rememberMeServices.loginFail(request, response); + response.sendError(HttpStatus.PRECONDITION_REQUIRED.value(), + HttpStatus.PRECONDITION_REQUIRED.getReasonPhrase()); + } else { + rememberMeServices.loginSuccess(request, response, authentication); + response.setStatus(HttpStatus.OK.value()); + response.getWriter().flush(); + } + + clearAuthenticationAttributes(request); + + } + +} \ No newline at end of file diff --git a/core/src/main/java/de/bstly/we/security/model/LocalUserDetails.java b/core/src/main/java/de/bstly/we/security/model/LocalUserDetails.java new file mode 100755 index 0000000..8920cac --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/model/LocalUserDetails.java @@ -0,0 +1,50 @@ +/** + * + */ +package de.bstly.we.security.model; + +import java.util.Collection; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; + +/** + * + * @author _bastler@bstly.de + * + */ +public class LocalUserDetails extends User { + + /** + * default serialVersionUID + */ + private static final long serialVersionUID = 1L; + private Long userId; + + /** + * @param username + * @param password + * @param authorities + */ + public LocalUserDetails(Long userId, String username, String password, + Collection authorities) { + // Super + super(username, password, authorities); + this.userId = userId; + } + + /** + * @return the userId + */ + public Long getUserId() { + return userId; + } + + /** + * @param userId the userId to set + */ + public void setUserId(Long userId) { + this.userId = userId; + } + +} diff --git a/core/src/main/java/de/bstly/we/security/token/LocalAnonymousAuthenticationToken.java b/core/src/main/java/de/bstly/we/security/token/LocalAnonymousAuthenticationToken.java new file mode 100644 index 0000000..a97ffc0 --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/token/LocalAnonymousAuthenticationToken.java @@ -0,0 +1,42 @@ +/** + * + */ +package de.bstly.we.security.token; + +import java.util.List; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; + +/** + * @author _bastler@bstly.de + * + */ +public class LocalAnonymousAuthenticationToken extends AnonymousAuthenticationToken { + + /** + * default serialVersionUID + */ + private static final long serialVersionUID = 1L; + public static final String ANONYMOUS_USERNAME = "anonymous"; + public static final List AUTHORITIES = AuthorityUtils + .createAuthorityList("ROLE_ANONYMOUS"); + + /** + * @param principal + */ + public LocalAnonymousAuthenticationToken(Object principal) { + this(principal, AUTHORITIES); + } + + /** + * + * @param principal + * @param authorities + */ + public LocalAnonymousAuthenticationToken(Object principal, List authorities) { + super(ANONYMOUS_USERNAME, principal, authorities); + } + +} diff --git a/core/src/main/java/de/bstly/we/security/token/LocalSecondFactorAuthenticationToken.java b/core/src/main/java/de/bstly/we/security/token/LocalSecondFactorAuthenticationToken.java new file mode 100644 index 0000000..04dc5ad --- /dev/null +++ b/core/src/main/java/de/bstly/we/security/token/LocalSecondFactorAuthenticationToken.java @@ -0,0 +1,77 @@ +/** + * + */ +package de.bstly.we.security.token; + +import java.util.List; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; + +import de.bstly.we.security.model.LocalUserDetails; + +/** + * @author _bastler@bstly.de + * + */ +public class LocalSecondFactorAuthenticationToken extends AbstractAuthenticationToken { + + /** + * default serialVersionUID + */ + private static final long serialVersionUID = 1L; + public static final String ROLE_REQUIRE_2FA = "ROLE_REQUIRE_2FA"; + public static final List AUTHORITIES = AuthorityUtils + .createAuthorityList(ROLE_REQUIRE_2FA); + /** + * + */ + private LocalUserDetails principal; + private String provider; + private final String code; + + /** + * @param principal + */ + public LocalSecondFactorAuthenticationToken(LocalUserDetails principal, String provider, + String code) { + super(AUTHORITIES); + this.principal = principal; + this.provider = provider; + this.code = code; + this.setAuthenticated(false); + } + + /* + * @see org.springframework.security.core.Authentication#getCredentials() + */ + @Override + public Object getCredentials() { + return code; + } + + /* + * @see org.springframework.security.core.Authentication#getPrincipal() + */ + @Override + public Object getPrincipal() { + return principal; + } + + /** + * + * @return the code + */ + public String getCode() { + return code; + } + + /** + * @return the provider + */ + public String getProvider() { + return provider; + } + +} diff --git a/email/pom.xml b/email/pom.xml new file mode 100755 index 0000000..16a979e --- /dev/null +++ b/email/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + de.bstly.we + webstly-main + ${revision} + + + email + webstly-email + + + + de.bstly.we + webstly-core + ${revision} + + + + org.springframework.boot + spring-boot-starter-mail + + + diff --git a/email/src/main/java/de/bstly/we/email/businesslogic/EmailManager.java b/email/src/main/java/de/bstly/we/email/businesslogic/EmailManager.java new file mode 100644 index 0000000..130ec29 --- /dev/null +++ b/email/src/main/java/de/bstly/we/email/businesslogic/EmailManager.java @@ -0,0 +1,43 @@ +/** + * + */ +package de.bstly.we.email.businesslogic; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.MailMessage; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Component; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class EmailManager { + + @Autowired + private JavaMailSender javaMailSender; + + public MailMessage sendText(String to, String from, String subject, String text) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(from); + message.setTo(to); + message.setSubject(subject); + message.setText(text); + javaMailSender.send(message); + + return message; + } + + public MailMessage sendBcc(String[] bcc, String from, String subject, String text) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(from); + message.setBcc(bcc); + message.setSubject(subject); + message.setText(text); + javaMailSender.send(message); + + return message; + } +} diff --git a/email/src/main/java/de/bstly/we/email/controller/EmailController.java b/email/src/main/java/de/bstly/we/email/controller/EmailController.java new file mode 100644 index 0000000..f4d57ab --- /dev/null +++ b/email/src/main/java/de/bstly/we/email/controller/EmailController.java @@ -0,0 +1,102 @@ +/** + * + */ +package de.bstly.we.email.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.MailMessage; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +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; + +import com.beust.jcommander.internal.Lists; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.Permissions; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.businesslogic.UserProfileFieldManager; +import de.bstly.we.businesslogic.UserProfileFields; +import de.bstly.we.controller.BaseController; +import de.bstly.we.email.businesslogic.EmailManager; +import de.bstly.we.email.controller.model.UserMailModel; +import de.bstly.we.model.Permission; +import de.bstly.we.model.User; +import de.bstly.we.model.UserProfileField; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/email") +public class EmailController extends BaseController { + + @Autowired + private EmailManager emailManager; + @Autowired + private UserManager userManager; + @Autowired + private PermissionManager permissionManager; + @Autowired + private UserProfileFieldManager userProfileFieldManager; + + /** + * + * @param to + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/test") + public MailMessage sendTest(@RequestBody String to) { + return emailManager.sendText(to, "no-reply@we.bstly.de", "Test Email", + "Test from we.bstly"); + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/list") + public List getUserMailList() { + List result = Lists.newArrayList(); + + for (Permission permission : permissionManager.getNotExpiresByName(Permissions.MAIL)) { + User user = userManager.get(permission.getTarget()); + if (user == null) { + continue; + } + + String email = userManager.getBstlyEmail(user.getUsername()); + + UserProfileField primaryEmailUserProfileField = userProfileFieldManager + .get(user.getId(), UserProfileFields.PROFILE_FIELD_EMAIL_PRIMARY); + if (primaryEmailUserProfileField != null + && "true".equals(primaryEmailUserProfileField.getValue())) { + UserProfileField emailUserProfileField = userProfileFieldManager.get(user.getId(), + UserProfileFields.PROFILE_FIELD_EMAIL); + if (emailUserProfileField != null + && StringUtils.hasText(emailUserProfileField.getValue())) { + email = emailUserProfileField.getValue(); + } + } + + UserMailModel userMailModel = new UserMailModel(user.getUsername(), email); + + UserProfileField localeUserProfileField = userProfileFieldManager.get(user.getId(), + UserProfileFields.PROFILE_FIELD_LOCALE); + if (localeUserProfileField != null + && StringUtils.hasText(localeUserProfileField.getValue())) { + userMailModel.setLocale(localeUserProfileField.getValue()); + + } + + result.add(userMailModel); + } + + return result; + } + +} diff --git a/email/src/main/java/de/bstly/we/email/controller/model/UserMailModel.java b/email/src/main/java/de/bstly/we/email/controller/model/UserMailModel.java new file mode 100644 index 0000000..890e0b3 --- /dev/null +++ b/email/src/main/java/de/bstly/we/email/controller/model/UserMailModel.java @@ -0,0 +1,68 @@ +/** + * + */ +package de.bstly.we.email.controller.model; + +/** + * @author _bastler@bstly.de + * + */ +public class UserMailModel { + + private String username; + private String email; + private String locale; + + /** + * @param username + * @param email + */ + public UserMailModel(String username, String email) { + super(); + this.username = username; + this.email = email; + } + + /** + * @return the username + */ + public String getUsername() { + return username; + } + + /** + * @param username the username to set + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * @return the email + */ + public String getEmail() { + return email; + } + + /** + * @param email the email to set + */ + public void setEmail(String email) { + this.email = email; + } + + /** + * @return the locale + */ + public String getLocale() { + return locale; + } + + /** + * @param locale the locale to set + */ + public void setLocale(String locale) { + this.locale = locale; + } + +} diff --git a/i18n/pom.xml b/i18n/pom.xml new file mode 100644 index 0000000..2386321 --- /dev/null +++ b/i18n/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + de.bstly.we + webstly-main + ${revision} + + + i18n + webstly-i18n + + + + de.bstly.we + webstly-core + ${revision} + + + diff --git a/i18n/src/main/java/de/bstly/we/i18n/businesslogic/I18nManager.java b/i18n/src/main/java/de/bstly/we/i18n/businesslogic/I18nManager.java new file mode 100644 index 0000000..30a8b9f --- /dev/null +++ b/i18n/src/main/java/de/bstly/we/i18n/businesslogic/I18nManager.java @@ -0,0 +1,135 @@ +/** + * + */ +package de.bstly.we.i18n.businesslogic; + +import java.util.List; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import de.bstly.we.i18n.model.I18n; +import de.bstly.we.i18n.repository.I18nRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class I18nManager { + + @Autowired + private I18nRepository i18nRepository; + private Gson gson = new Gson(); + + /** + * + * @param locale + * @return + */ + public I18n get(String locale) { + return i18nRepository.findById(locale).orElse(null); + } + + /** + * + * @param locale + * @return + */ + public JsonObject getLabel(String locale) { + I18n i18n = get(locale); + if (i18n != null && StringUtils.hasText(i18n.getLabel())) { + JsonElement element = JsonParser.parseString(i18n.getLabel()); + if (element != null && element.isJsonObject()) { + return element.getAsJsonObject(); + } + } + + return null; + } + + /** + * + * @return + */ + public List getLocales() { + return i18nRepository.findAll().stream().map(I18n::getLocale).collect(Collectors.toList()); + } + + /** + * + * @param dest + * @param src + */ + protected void extendJsonObject(JsonObject dest, JsonObject src) { + for (Entry srcEntry : src.entrySet()) { + String srcKey = srcEntry.getKey(); + JsonElement srcValue = srcEntry.getValue(); + if (dest.has(srcKey)) { + JsonElement destValue = dest.get(srcKey); + if (destValue.isJsonObject() && srcValue.isJsonObject()) { + extendJsonObject(destValue.getAsJsonObject(), srcValue.getAsJsonObject()); + } else { + dest.add(srcKey, srcValue); + } + } else { + dest.add(srcKey, srcValue); + } + } + } + + /** + * + * @param locale + * @param newLabel + * @return + */ + public I18n addLabel(String locale, JsonObject newLabel) { + JsonObject label = getLabel(locale); + + if (label == null || label.size() == 0 || label.entrySet().isEmpty()) { + label = newLabel; + } else { + extendJsonObject(label, newLabel); + } + + I18n i18n = new I18n(); + i18n.setLocale(locale); + i18n.setLabel(gson.toJson(label)); + + return i18nRepository.save(i18n); + } + + /** + * + * @param locale + * @param label + * @return + */ + public I18n setLabel(String locale, JsonObject label) { + I18n i18n = new I18n(); + i18n.setLocale(locale); + i18n.setLabel(gson.toJson(label)); + + return i18nRepository.save(i18n); + } + + /** + * + * @param locale + */ + public void deleteLabel(String locale) { + if (i18nRepository.existsById(locale)) { + i18nRepository.deleteById(locale); + } + } + +} diff --git a/i18n/src/main/java/de/bstly/we/i18n/controller/I18nController.java b/i18n/src/main/java/de/bstly/we/i18n/controller/I18nController.java new file mode 100644 index 0000000..d6ec69a --- /dev/null +++ b/i18n/src/main/java/de/bstly/we/i18n/controller/I18nController.java @@ -0,0 +1,108 @@ +/** + * + */ +package de.bstly.we.i18n.controller; + +import java.io.IOException; +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonObject; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.i18n.businesslogic.I18nManager; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/i18n") +public class I18nController extends BaseController { + + @Autowired + private I18nManager i18nManager; + private Gson gson = new Gson(); + + /** + * + * @return + */ + @GetMapping + public List getLocales() { + return i18nManager.getLocales(); + } + + /** + * + * @param locale + * @param response + * @throws JsonIOException + * @throws IOException + */ + @GetMapping("/{locale}") + public void getLabel(@PathVariable("locale") String locale, HttpServletResponse response) + throws JsonIOException, IOException { + JsonObject label = i18nManager.getLabel(locale); + if (label != null) { + response.setCharacterEncoding("utf-8"); + gson.toJson(label, response.getWriter()); + } + } + + /** + * + * @param locale + * @param label + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/{locale}") + public void setLabel(@PathVariable("locale") String locale, @RequestBody Object label) { + JsonElement element = gson.toJsonTree(label); + + if (element != null && element.isJsonObject()) { + i18nManager.setLabel(locale, element.getAsJsonObject()); + } + } + + /** + * + * @param locale + * @param label + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PutMapping("/{locale}") + public void addLabel(@PathVariable("locale") String locale, @RequestBody Object label) { + JsonElement element = gson.toJsonTree(label); + + if (element != null && element.isJsonObject()) { + i18nManager.addLabel(locale, element.getAsJsonObject()); + } + } + + /** + * + * @param locale + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/{locale}") + public void deleteLocale(@PathVariable("locale") String locale) { + i18nManager.deleteLabel(locale); + } + +} diff --git a/i18n/src/main/java/de/bstly/we/i18n/model/I18n.java b/i18n/src/main/java/de/bstly/we/i18n/model/I18n.java new file mode 100644 index 0000000..bedcf9d --- /dev/null +++ b/i18n/src/main/java/de/bstly/we/i18n/model/I18n.java @@ -0,0 +1,64 @@ +/** + * + */ +package de.bstly.we.i18n.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "i18n", uniqueConstraints = @UniqueConstraint(columnNames = { "locale" })) +public class I18n { + + /** + * + */ + @Id + @Column(name = "locale", unique = true, nullable = false) + private String locale; + + /** + * + */ + @Lob + @Column(name = "label") + private String label; + + /** + * @return the locale + */ + public String getLocale() { + return locale; + } + + /** + * @param locale the locale to set + */ + public void setLocale(String locale) { + this.locale = locale; + } + + /** + * @return the label + */ + public String getLabel() { + return label; + } + + /** + * @param label the label to set + */ + public void setLabel(String label) { + this.label = label; + } + +} diff --git a/i18n/src/main/java/de/bstly/we/i18n/repository/I18nRepository.java b/i18n/src/main/java/de/bstly/we/i18n/repository/I18nRepository.java new file mode 100644 index 0000000..94e2bfa --- /dev/null +++ b/i18n/src/main/java/de/bstly/we/i18n/repository/I18nRepository.java @@ -0,0 +1,21 @@ +/** + * + */ +package de.bstly.we.i18n.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.i18n.model.I18n; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface I18nRepository + extends JpaRepository, QuerydslPredicateExecutor { + +} diff --git a/invite/pom.xml b/invite/pom.xml new file mode 100755 index 0000000..55436da --- /dev/null +++ b/invite/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + de.bstly.we + webstly-main + ${revision} + + + invite + webstly-invite + + + + de.bstly.we + webstly-core + ${revision} + + + diff --git a/invite/src/main/java/de/bstly/we/invite/businesslogic/InviteManager.java b/invite/src/main/java/de/bstly/we/invite/businesslogic/InviteManager.java new file mode 100644 index 0000000..a67aa92 --- /dev/null +++ b/invite/src/main/java/de/bstly/we/invite/businesslogic/InviteManager.java @@ -0,0 +1,221 @@ +/** + * + */ +package de.bstly.we.invite.businesslogic; + +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.google.common.collect.Lists; +import com.querydsl.core.BooleanBuilder; + +import de.bstly.we.businesslogic.UserDataProvider; +import de.bstly.we.invite.model.Invite; +import de.bstly.we.invite.model.InviteMapping; +import de.bstly.we.invite.model.QInvite; +import de.bstly.we.invite.repository.InviteRepository; +import de.bstly.we.model.UserData; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class InviteManager implements UserDataProvider { + + @Autowired + private InviteRepository inviteRepository; + @Autowired + private InviteMappingManager inviteMappingManager; + private QInvite qInvite = QInvite.invite; + + @Value("${we.bstly.invite.codeLength:8}") + private int codeLength; + + /** + * @param id + * @return + */ + public Invite get(Long id) { + return inviteRepository.findById(id).orElse(null); + } + + /** + * + * @param owner + * @return + */ + public List getAllByOwner(Long owner) { + return Lists.newArrayList(inviteRepository.findAll(qInvite.owner.eq(owner))); + } + + /** + * + * @param page + * @param size + * @param search + * @return + */ + public Page get(int page, int size, String search) { + if (StringUtils.hasText(search)) { + return inviteRepository.findAll(qInvite.note.containsIgnoreCase(search), + PageRequest.of(page, size)); + } + return inviteRepository.findAll(PageRequest.of(page, size)); + } + + /** + * @param id + * @return + */ + public Invite getByCode(String code) { + return inviteRepository.findOne(qInvite.code.eq(code)).orElse(null); + } + + /** + * + * @param owner + * @param item + * @param page + * @param size + * @param sortBy + * @param descending + * @param search + * @return + */ + public Page getByOwner(Long owner, Integer item, int page, int size, String sortBy, + boolean descending, String search, String redeemed) { + PageRequest pageRequest = PageRequest.of(page, size, + descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending()); + + BooleanBuilder query = new BooleanBuilder(); + query.and(qInvite.owner.eq(owner)); + + if (item != null) { + query.and(qInvite.item.eq(item)); + } + + if (StringUtils.hasText(search)) { + query.and(qInvite.note.containsIgnoreCase(search)); + } + + if (StringUtils.hasText(redeemed)) { + if ("true".equals(redeemed)) { + query.and(qInvite.redeemed.isTrue()); + } else { + query.and(qInvite.redeemed.isFalse()); + } + } + + return inviteRepository.findAll(query.getValue(), pageRequest); + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page getOthers(Long owner, int item, int page, int size, String search, + String redeemed) { + + BooleanBuilder query = new BooleanBuilder(); + query.and(qInvite.owner.ne(owner)); + query.and(qInvite.item.eq(item)); + + if (StringUtils.hasText(search)) { + query.and(qInvite.note.containsIgnoreCase(search)); + } + + if (StringUtils.hasText(redeemed)) { + if ("true".equals(redeemed)) { + query.and(qInvite.redeemed.isTrue()); + } else { + query.and(qInvite.redeemed.isFalse()); + } + } + + return inviteRepository.findAll(query.getValue(), PageRequest.of(page, size)); + } + + /** + * + * @param invite + * @return + */ + public Invite save(Invite invite) { + if (!StringUtils.hasText(invite.getCode())) { + invite.setCode(RandomStringUtils.random(codeLength, true, true).toUpperCase()); + while (inviteRepository.exists(qInvite.code.eq(invite.getCode()))) { + invite.setCode(RandomStringUtils.random(codeLength, true, true).toUpperCase()); + } + } + + InviteMapping inviteMapping = inviteMappingManager.getByItem(invite.getItem()); + Assert.notNull(inviteMapping, "No mapping for item!"); + if (StringUtils.hasLength(inviteMapping.getCodeLink())) { + invite.setCodeLink(String.format(inviteMapping.getCodeLink(), invite.getCode())); + } else { + invite.setCodeLink(null); + } + + return inviteRepository.save(invite); + } + + /** + * + * @param id + */ + public void delete(Long id) { + inviteRepository.deleteById(id); + } + + /** + * + */ + public void deleteAll() { + inviteRepository.deleteAll(); + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getId() + */ + @Override + public String getId() { + return "invites"; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getUserData(java.lang.Long) + */ + @Override + public List getUserData(Long userId) { + List result = Lists.newArrayList(); + for (Invite invite : getAllByOwner(userId)) { + result.add(invite); + } + return result; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#purgeUserData(java.lang.Long) + */ + @Override + public void purgeUserData(Long userId) { + for (Invite invite : getAllByOwner(userId)) { + inviteRepository.delete(invite); + } + } + +} diff --git a/invite/src/main/java/de/bstly/we/invite/businesslogic/InviteMappingManager.java b/invite/src/main/java/de/bstly/we/invite/businesslogic/InviteMappingManager.java new file mode 100644 index 0000000..92170f5 --- /dev/null +++ b/invite/src/main/java/de/bstly/we/invite/businesslogic/InviteMappingManager.java @@ -0,0 +1,117 @@ +/** + * + */ +package de.bstly.we.invite.businesslogic; + +import java.time.Instant; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import de.bstly.we.invite.model.Invite; +import de.bstly.we.invite.model.InviteMapping; +import de.bstly.we.invite.model.QInvite; +import de.bstly.we.invite.model.QInviteMapping; +import de.bstly.we.invite.repository.InviteMappingRepository; +import de.bstly.we.invite.repository.InviteRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class InviteMappingManager { + + @Autowired + private InviteMappingRepository inviteMappingRepository; + @Autowired + private InviteRepository inviteRepository; + private QInviteMapping qInviteMapping = QInviteMapping.inviteMapping; + private QInvite qInvite = QInvite.invite; + + /** + * @param id + * @return + */ + public InviteMapping get(String quota) { + return inviteMappingRepository.findById(quota).orElse(null); + } + + /** + * + * @param item + * @return + */ + public InviteMapping getByItem(int item) { + return inviteMappingRepository.findOne(qInviteMapping.item.eq(item)).orElse(null); + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + Sort sort = descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending(); + return inviteMappingRepository.findAll(PageRequest.of(page, size, sort)); + } + + /** + * + * @param owner + * @param room + * @param starts + * @param expires + * @return + */ + public InviteMapping create(String quota, int item, Instant starts, Instant expires) { + Assert.isNull(get(quota), "ivite mapping for quota already exists"); + + InviteMapping inviteMapping = new InviteMapping(); + inviteMapping.setQuota(quota); + inviteMapping.setStarts(starts); + inviteMapping.setExpires(expires); + + return save(inviteMapping); + } + + /** + * + * @param inviteMapping + * @return + */ + public InviteMapping save(InviteMapping inviteMapping) { + + for (Invite invite : inviteRepository.findAll(qInvite.item.eq(inviteMapping.getItem()))) { + if (StringUtils.hasText(inviteMapping.getCodeLink())) { + String codeLink = String.format(inviteMapping.getCodeLink(), invite.getCode()); + if (!codeLink.equals(invite.getCodeLink())) { + invite.setCodeLink(codeLink); + inviteRepository.save(invite); + } + } else if (StringUtils.hasText(invite.getCodeLink())) { + invite.setCodeLink(null); + inviteRepository.save(invite); + } + } + + return inviteMappingRepository.save(inviteMapping); + } + + /** + * + * @param quota + */ + public void delete(String quota) { + inviteMappingRepository.deleteById(quota); + } + +} diff --git a/invite/src/main/java/de/bstly/we/invite/controller/InviteController.java b/invite/src/main/java/de/bstly/we/invite/controller/InviteController.java new file mode 100644 index 0000000..49f4b35 --- /dev/null +++ b/invite/src/main/java/de/bstly/we/invite/controller/InviteController.java @@ -0,0 +1,328 @@ +/** + * + */ +package de.bstly.we.invite.controller; + +import java.time.Instant; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.beust.jcommander.internal.Lists; +import com.google.gson.JsonArray; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.Permissions; +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.businesslogic.UserProfileFieldManager; +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.model.UserModel; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.controller.validation.PasswordModelValidator; +import de.bstly.we.controller.validation.UserModelValidator; +import de.bstly.we.controller.validation.UserProfileFieldValidator; +import de.bstly.we.invite.businesslogic.InviteManager; +import de.bstly.we.invite.businesslogic.InviteMappingManager; +import de.bstly.we.invite.model.Invite; +import de.bstly.we.invite.model.InviteMapping; +import de.bstly.we.model.Permission; +import de.bstly.we.model.ProfileFieldType; +import de.bstly.we.model.Quota; +import de.bstly.we.model.User; +import de.bstly.we.model.UserProfileField; +import de.bstly.we.model.UserStatus; +import de.bstly.we.model.Visibility; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/invites") +public class InviteController extends BaseController { + + @Autowired + private InviteManager inviteManager; + @Autowired + private InviteMappingManager inviteMappingManager; + @Autowired + private UserManager userManager; + @Autowired + private UserProfileFieldManager userProfileFieldManager; + @Autowired + private PermissionManager permissionManager; + @Autowired + private QuotaManager quotaManager; + @Autowired + private PasswordModelValidator passwordModelValidator; + @Autowired + private UserModelValidator userModelValidator; + @Autowired + private UserProfileFieldValidator userProfileFieldValidator; + + /** + * + * @param code + * @return + */ + @GetMapping("/{code}") + public Invite get(@PathVariable("code") String code) { + Invite invite = inviteManager.getByCode(code); + if (invite == null) { + throw new EntityResponseStatusException(HttpStatus.NOT_ACCEPTABLE); + } + + if (getCurrentUserId() == null) { + invite.setId(null); + invite.setItem(null); + invite.setNote(null); + invite.setOwner(null); + } else if (!getCurrentUserId().equals(invite.getOwner())) { + if (permissionManager.hasPermission(getCurrentUserId(), Permissions.ROLE_MEMBER)) { + invite.setId(null); + invite.setItem(null); + if (!StringUtils.hasText(invite.getNote())) { + invite.setNote("..."); + } + invite.setOwner(null); + } else { + invite.setId(null); + invite.setItem(null); + invite.setNote(null); + invite.setOwner(null); + } + } + + return invite; + } + + /** + * + * @param userModel + */ + @PostMapping + public void register(@RequestBody UserModel userModel) { + Errors errors = new RequestBodyErrors(userModel); + Invite invite = inviteManager.getByCode(userModel.getToken()); + + if (invite == null) { + throw new EntityResponseStatusException(HttpStatus.NOT_ACCEPTABLE); + } + + if (invite.isRedeemed() + || invite.getExpires() != null && invite.getExpires().isBefore(Instant.now())) { + throw new EntityResponseStatusException(HttpStatus.GONE); + } + + boolean register = false; + + for (Permission permission : permissionManager.getForItem(null, invite.getItem(), + new JsonArray(), invite.getStarts())) { + if (permission.getExpires().isAfter(Instant.now()) && !permission.isAddon()) { + register = true; + break; + } + } + + if (!register) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + userModelValidator.validate(userModel, errors); + passwordModelValidator.validate(userModel, errors); + + if (userModel.getProfileFields() == null) { + userModel.setProfileFields(Lists.newArrayList()); + } + + if (userModel.getStatus() == null) { + userModel.setStatus(UserStatus.NORMAL); + } + + for (UserProfileField userProfileField : userModel.getProfileFields()) { + userProfileFieldValidator.validate(userProfileField, errors); + } + + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + User user = userManager.create(userModel.getUsername(), userModel.getPassword(), + userModel.getStatus()); + + for (UserProfileField userProfileField : userModel.getProfileFields()) { + userProfileField.setTarget(user.getId()); + if (userProfileField.getType() == null) { + userProfileField.setType(ProfileFieldType.TEXT); + } + + if (userProfileField.getVisibility() == null) { + userProfileField.setVisibility(Visibility.PRIVATE); + } + + userProfileField = userProfileFieldManager.save(userProfileField); + } + + permissionManager.applyItem(user.getId(), invite.getItem(), new JsonArray(), + invite.getStarts()); + quotaManager.applyItem(user.getId(), invite.getItem()); + + invite.setRedeemed(true); + inviteManager.save(invite); + } + + /** + * + * @param quotaParameter + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping + public Page getInvites(@RequestParam("quota") Optional quotaParameter, + @RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter, + @RequestParam("sort") Optional sortParamater, + @RequestParam("desc") Optional descParameter, + @RequestParam("search") Optional searchParameter, + @RequestParam("redeemed") Optional redeemedParameter) { + Integer item = null; + if (quotaParameter.isPresent() && StringUtils.hasText(quotaParameter.get())) { + InviteMapping inviteMapping = inviteMappingManager.get(quotaParameter.get()); + if (inviteMapping == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + item = inviteMapping.getItem(); + } + + return inviteManager.getByOwner(getCurrentUserId(), item, pageParameter.orElse(0), + sizeParameter.orElse(10), sortParamater.orElse("id"), descParameter.orElse(false), + searchParameter.orElse(null), redeemedParameter.orElse(null)); + } + + /** + * + * @param quota + * @param page + * @param sizeParameter + * @param searchParameter + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping("/{quota}/others") + public Page getOtherInvites(@PathVariable("quota") String quota, + @RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter, + @RequestParam("search") Optional searchParameter, + @RequestParam("redeemed") Optional redeemedParameter) { + + InviteMapping inviteMapping = inviteMappingManager.get(quota); + Quota inviteQuota = quotaManager.get(getCurrentUserId(), + InviteMapping.QUOTA_PREFIX + quota); + if (inviteMapping == null || inviteQuota == null || inviteQuota.getValue() < 1) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + Page page = inviteManager.getOthers(getCurrentUserId(), inviteMapping.getItem(), + pageParameter.orElse(0), sizeParameter.orElse(10), searchParameter.orElse(null), + redeemedParameter.orElse(null)); + for (Invite invite : page.getContent()) { + invite.setId(null); + invite.setCode(null); + invite.setCodeLink(null); + invite.setMessage(null); + invite.setOwner(null); + } + + return page; + } + + /** + * + * @param quota + * @param inviteModel + * @return + */ + @PreAuthorize("isAuthenticated()") + @PostMapping("/{quota}") + public Invite createInvite(@PathVariable("quota") String quota, + @RequestBody Invite inviteModel) { + InviteMapping inviteMapping = inviteMappingManager.get(quota); + Quota inviteQuota = quotaManager.get(getCurrentUserId(), + InviteMapping.QUOTA_PREFIX + quota); + if (inviteMapping == null || inviteQuota == null || inviteQuota.getValue() < 1) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + if (!permissionManager.isFullUser(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + Invite invite = new Invite(); + invite.setOwner(getCurrentUserId()); + invite.setStarts(inviteMapping.getStarts() != null ? inviteMapping.getStarts() + : inviteModel.getStarts()); + invite.setExpires(inviteMapping.getExpires() != null ? inviteMapping.getExpires() + : inviteModel.getExpires()); + invite.setItem(inviteMapping.getItem()); + + if (inviteMapping.getMessageLimit() != null && inviteMapping.getMessageLimit() > 0 + && StringUtils.hasText(inviteModel.getMessage()) + && inviteModel.getMessage().length() > inviteMapping.getMessageLimit()) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + invite.setMessage(inviteModel.getMessage()); + + if (!StringUtils.hasText(invite.getMessage())) { + invite.setMessage(inviteMapping.getDefaultMessage()); + } + + invite.setNote(inviteModel.getNote()); + invite.setRedeemed(false); + + inviteQuota.setValue(inviteQuota.getValue() - 1); + inviteQuota = quotaManager.update(inviteQuota); + + return inviteManager.save(invite); + } + + /** + * + * @param inviteModel + * @return + */ + @PreAuthorize("isAuthenticated()") + @PatchMapping + public Invite updateInvite(@RequestBody Invite inviteModel) { + if (inviteModel.getId() == null) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + Invite invite = inviteManager.get(inviteModel.getId()); + + if (invite == null || !invite.getOwner().equals(getCurrentUserId()) + || !permissionManager.isFullUser(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + invite.setMessage(inviteModel.getMessage()); + invite.setNote(inviteModel.getNote()); + + return inviteManager.save(invite); + } +} diff --git a/invite/src/main/java/de/bstly/we/invite/controller/InviteManagingController.java b/invite/src/main/java/de/bstly/we/invite/controller/InviteManagingController.java new file mode 100644 index 0000000..59746e9 --- /dev/null +++ b/invite/src/main/java/de/bstly/we/invite/controller/InviteManagingController.java @@ -0,0 +1,117 @@ +/** + * + */ +package de.bstly.we.invite.controller; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.google.common.collect.Lists; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.invite.businesslogic.InviteManager; +import de.bstly.we.invite.businesslogic.InviteMappingManager; +import de.bstly.we.invite.model.Invite; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/invites/manage") +public class InviteManagingController extends BaseController { + + @Autowired + private InviteManager inviteManager; + @Autowired + private InviteMappingManager inviteMappingManager; + + /** + * + * @param page + * @param size + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getInvites(@RequestParam("page") Optional page, + @RequestParam("size") Optional size, + @RequestParam("search") Optional search) { + return inviteManager.get(page.orElse(0), size.orElse(10), search.orElse(null)); + } + + /** + * + * @param userModel + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public Invite createOrUpdate(@RequestBody Invite invite) { + if (invite.getItem() == null || inviteMappingManager.getByItem(invite.getItem()) == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + return inviteManager.save(invite); + } + + /** + * + * @param invites + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/list") + public List createOrUpdateList(@RequestBody List invites) { + List result = Lists.newArrayList(); + for (Invite invite : invites) { + Errors errors = new RequestBodyErrors(invite); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + result.add(inviteManager.save(invite)); + } + return result; + } + + /** + * + * @param id + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/{id}") + public void delete(@PathVariable("id") Long id) { + if (inviteManager.get(id) == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + inviteManager.delete(id); + } + + /** + * + * @param id + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping + public void deleteAll() { + inviteManager.deleteAll(); + } + +} diff --git a/invite/src/main/java/de/bstly/we/invite/controller/InviteMappingController.java b/invite/src/main/java/de/bstly/we/invite/controller/InviteMappingController.java new file mode 100644 index 0000000..3e59e7e --- /dev/null +++ b/invite/src/main/java/de/bstly/we/invite/controller/InviteMappingController.java @@ -0,0 +1,101 @@ +/** + * + */ +package de.bstly.we.invite.controller; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.google.common.collect.Lists; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.invite.businesslogic.InviteMappingManager; +import de.bstly.we.invite.model.InviteMapping; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/invites/mappings") +public class InviteMappingController extends BaseController { + + @Autowired + private InviteMappingManager inviteMappingManager; + + /** + * + * @param pageParameter + * @param sizeParameter + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getInviteMappings( + @RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter) { + return inviteMappingManager.get(pageParameter.orElse(0), sizeParameter.orElse(10), "quota", + true); + } + + /** + * + * @param userModel + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public InviteMapping createOrUpdate(@RequestBody InviteMapping inviteMapping) { + return inviteMappingManager.save(inviteMapping); + } + + /** + * + * @param inviteMappings + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/list") + public List createOrUpdateList(@RequestBody List inviteMappings) { + List result = Lists.newArrayList(); + for (InviteMapping inviteMapping : inviteMappings) { + Errors errors = new RequestBodyErrors(inviteMapping); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + result.add(inviteMappingManager.save(inviteMapping)); + } + return result; + } + + /** + * + * @param quota + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/{quota}") + public void delete(@PathVariable("quota") String quota) { + if (inviteMappingManager.get(quota) == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + inviteMappingManager.delete(quota); + } + +} diff --git a/invite/src/main/java/de/bstly/we/invite/model/Invite.java b/invite/src/main/java/de/bstly/we/invite/model/Invite.java new file mode 100644 index 0000000..d8e2180 --- /dev/null +++ b/invite/src/main/java/de/bstly/we/invite/model/Invite.java @@ -0,0 +1,180 @@ +/** + * + */ +package de.bstly.we.invite.model; + +import java.time.Instant; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; + +import de.bstly.we.model.UserData; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "invites") +public class Invite implements UserData { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + private Long owner; + private String code; + private Integer item; + private Instant starts; + private Instant expires; + @Lob + private String message; + private String note; + private boolean redeemed; + private String codeLink; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the owner + */ + public Long getOwner() { + return owner; + } + + /** + * @param owner the owner to set + */ + public void setOwner(Long owner) { + this.owner = owner; + } + + /** + * @return the code + */ + public String getCode() { + return code; + } + + /** + * @param code the code to set + */ + public void setCode(String code) { + this.code = code; + } + + /** + * @return the item + */ + public Integer getItem() { + return item; + } + + /** + * @param item the item to set + */ + public void setItem(Integer item) { + this.item = item; + } + + /** + * @return the starts + */ + public Instant getStarts() { + return starts; + } + + /** + * @param starts the starts to set + */ + public void setStarts(Instant starts) { + this.starts = starts; + } + + /** + * @return the expires + */ + public Instant getExpires() { + return expires; + } + + /** + * @param expires the expires to set + */ + public void setExpires(Instant expires) { + this.expires = expires; + } + + /** + * @return the message + */ + public String getMessage() { + return message; + } + + /** + * @param message the message to set + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * @return the note + */ + public String getNote() { + return note; + } + + /** + * @param note the note to set + */ + public void setNote(String note) { + this.note = note; + } + + /** + * @return the redeemed + */ + public boolean isRedeemed() { + return redeemed; + } + + /** + * @param redeemed the redeemed to set + */ + public void setRedeemed(boolean redeemed) { + this.redeemed = redeemed; + } + + /** + * @return the codeLink + */ + public String getCodeLink() { + return codeLink; + } + + /** + * @param codeLink the codeLink to set + */ + public void setCodeLink(String codeLink) { + this.codeLink = codeLink; + } +} diff --git a/invite/src/main/java/de/bstly/we/invite/model/InviteMapping.java b/invite/src/main/java/de/bstly/we/invite/model/InviteMapping.java new file mode 100644 index 0000000..3ce14ca --- /dev/null +++ b/invite/src/main/java/de/bstly/we/invite/model/InviteMapping.java @@ -0,0 +1,131 @@ +/** + * + */ +package de.bstly.we.invite.model; + +import java.time.Instant; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "invite_mapping") +public class InviteMapping { + + public static final String QUOTA_PREFIX = "invite_"; + + @Id + private String quota; + private Integer item; + private Instant starts; + private Instant expires; + @Column(name = "message_limit") + private Integer messageLimit; + private String codeLink; + private String defaultMessage; + + /** + * @return the quota + */ + public String getQuota() { + return quota; + } + + /** + * @param quota the quota to set + */ + public void setQuota(String quota) { + this.quota = quota; + } + + /** + * @return the item + */ + public Integer getItem() { + return item; + } + + /** + * @param item the item to set + */ + public void setItem(Integer item) { + this.item = item; + } + + /** + * @return the starts + */ + public Instant getStarts() { + return starts; + } + + /** + * @param starts the starts to set + */ + public void setStarts(Instant starts) { + this.starts = starts; + } + + /** + * @return the expires + */ + public Instant getExpires() { + return expires; + } + + /** + * @param expires the expires to set + */ + public void setExpires(Instant expires) { + this.expires = expires; + } + + /** + * @return the messageLimit + */ + public Integer getMessageLimit() { + return messageLimit; + } + + /** + * @param messageLimit the messageLimit to set + */ + public void setMessageLimit(Integer messageLimit) { + this.messageLimit = messageLimit; + } + + /** + * @return the codeLink + */ + public String getCodeLink() { + return codeLink; + } + + /** + * @param codeLink the codeLink to set + */ + public void setCodeLink(String codeLink) { + this.codeLink = codeLink; + } + + /** + * @return the defaultMessage + */ + public String getDefaultMessage() { + return defaultMessage; + } + + /** + * @param defaultMessage the defaultMessage to set + */ + public void setDefaultMessage(String defaultMessage) { + this.defaultMessage = defaultMessage; + } + +} diff --git a/invite/src/main/java/de/bstly/we/invite/repository/InviteMappingRepository.java b/invite/src/main/java/de/bstly/we/invite/repository/InviteMappingRepository.java new file mode 100755 index 0000000..4db58b9 --- /dev/null +++ b/invite/src/main/java/de/bstly/we/invite/repository/InviteMappingRepository.java @@ -0,0 +1,20 @@ +/** + * + */ +package de.bstly.we.invite.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.invite.model.InviteMapping; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface InviteMappingRepository + extends JpaRepository, QuerydslPredicateExecutor { +} diff --git a/invite/src/main/java/de/bstly/we/invite/repository/InviteRepository.java b/invite/src/main/java/de/bstly/we/invite/repository/InviteRepository.java new file mode 100644 index 0000000..8341b5f --- /dev/null +++ b/invite/src/main/java/de/bstly/we/invite/repository/InviteRepository.java @@ -0,0 +1,20 @@ +/** + * + */ +package de.bstly.we.invite.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.invite.model.Invite; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface InviteRepository + extends JpaRepository, QuerydslPredicateExecutor { +} diff --git a/jitsi/pom.xml b/jitsi/pom.xml new file mode 100755 index 0000000..0077d6c --- /dev/null +++ b/jitsi/pom.xml @@ -0,0 +1,31 @@ + + 4.0.0 + + de.bstly.we + webstly-main + ${revision} + + + jitsi + webstly-jitsi + + + + de.bstly.we + webstly-core + ${revision} + + + de.bstly.we + webstly-urlshortener + ${revision} + + + + com.nimbusds + nimbus-jose-jwt + + + diff --git a/jitsi/src/main/java/de/bstly/we/jitsi/businesslogic/JitsiPermissions.java b/jitsi/src/main/java/de/bstly/we/jitsi/businesslogic/JitsiPermissions.java new file mode 100644 index 0000000..2041825 --- /dev/null +++ b/jitsi/src/main/java/de/bstly/we/jitsi/businesslogic/JitsiPermissions.java @@ -0,0 +1,13 @@ +/** + * + */ +package de.bstly.we.jitsi.businesslogic; + +/** + * @author _bastler@bstly.de + * + */ +public interface JitsiPermissions { + + public static final String JITSI_MEET = "jitsi"; +} diff --git a/jitsi/src/main/java/de/bstly/we/jitsi/businesslogic/JitsiQuotas.java b/jitsi/src/main/java/de/bstly/we/jitsi/businesslogic/JitsiQuotas.java new file mode 100644 index 0000000..136dd2c --- /dev/null +++ b/jitsi/src/main/java/de/bstly/we/jitsi/businesslogic/JitsiQuotas.java @@ -0,0 +1,13 @@ +/** + * + */ +package de.bstly.we.jitsi.businesslogic; + +/** + * @author _bastler@bstly.de + * + */ +public interface JitsiQuotas { + + public static final String JITSI_MEET = "jitsi"; +} diff --git a/jitsi/src/main/java/de/bstly/we/jitsi/businesslogic/JitsiRoomManager.java b/jitsi/src/main/java/de/bstly/we/jitsi/businesslogic/JitsiRoomManager.java new file mode 100644 index 0000000..e0d0fe3 --- /dev/null +++ b/jitsi/src/main/java/de/bstly/we/jitsi/businesslogic/JitsiRoomManager.java @@ -0,0 +1,325 @@ +/** + * + */ +package de.bstly.we.jitsi.businesslogic; + +import java.time.Instant; +import java.util.Date; +import java.util.List; + +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.google.common.collect.Lists; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.KeyLengthException; +import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTClaimsSet.Builder; +import com.nimbusds.jwt.SignedJWT; + +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.businesslogic.UserDataProvider; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.jitsi.model.JitsiRoom; +import de.bstly.we.jitsi.model.QJitsiRoom; +import de.bstly.we.jitsi.repository.JitsiRoomRepository; +import de.bstly.we.model.Quota; +import de.bstly.we.model.UserData; +import de.bstly.we.urlshortener.businesslogic.ShortenedUrlManager; +import de.bstly.we.urlshortener.businesslogic.ShortenedUrlQuotas; +import de.bstly.we.urlshortener.model.QShortenedUrl; +import de.bstly.we.urlshortener.model.ShortenedUrl; +import de.bstly.we.urlshortener.repository.ShortenedUrlRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class JitsiRoomManager implements SmartInitializingSingleton, UserDataProvider { + + @Autowired + private UserManager userManager; + @Autowired + private JitsiRoomRepository jitsiRoomRepository; + @Autowired + private ShortenedUrlRepository shortenedUrlRepository; + @Autowired + private QuotaManager quotaManager; + @Autowired + private ShortenedUrlManager shortenedUrlManager; + private QShortenedUrl qShortenedUrl = QShortenedUrl.shortenedUrl; + private QJitsiRoom qJitsiRoom = QJitsiRoom.jitsiRoom; + + private JWSSigner signer; + + @Value("${jitsi.iss:}") + private String iss; + @Value("${jitsi.sub:}") + private String sub; + @Value("${jitsi.secret:}") + private String secret; + @Value("${jitsi.urlFormat:%s/jwt?%s}") + private String urlFormat; + + /* + * @see org.springframework.beans.factory.SmartInitializingSingleton# + * afterSingletonsInstantiated() + */ + @Override + public void afterSingletonsInstantiated() { + try { + signer = new MACSigner(secret); + } catch (KeyLengthException e) { + e.printStackTrace(); + } + } + + /** + * @param id + * @return + */ + public JitsiRoom get(Long id) { + return jitsiRoomRepository.findById(id).orElse(null); + } + + /** + * + * @param owner + * @return + */ + public List getAllByOwner(Long owner) { + return Lists.newArrayList(jitsiRoomRepository.findAll(qJitsiRoom.owner.eq(owner))); + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + return jitsiRoomRepository.findAll(PageRequest.of(page, size, + descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending())); + } + + /** + * + * @param userId + * @return + */ + public Page getForUserId(Long userId, int page, int size, String sortBy, + boolean descending) { + // delete expired rooms + jitsiRoomRepository.deleteAll(jitsiRoomRepository.findAll( + qJitsiRoom.owner.eq(userId).and(qJitsiRoom.expires.before(Instant.now())))); + + return jitsiRoomRepository.findAll(qJitsiRoom.owner.eq(userId), PageRequest.of(page, size, + descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending())); + } + + /** + * + * @param owner + * @param room + * @param starts + * @param expires + * @return + * @throws JOSEException + */ + public JitsiRoom create(Long owner, String room, Instant starts, Instant moderationStarts, + Instant expires) throws JOSEException { + JitsiRoom jitsiRoom = new JitsiRoom(); + jitsiRoom.setOwner(owner); + jitsiRoom.setRoom(room); + jitsiRoom.setStarts(starts); + jitsiRoom.setModerationStarts(moderationStarts); + jitsiRoom.setExpires(expires); + + return save(jitsiRoom); + } + + /** + * + * @param jitsiRoom + * @return + * @throws JOSEException + */ + public JitsiRoom save(JitsiRoom jitsiRoom) throws JOSEException { + Assert.notNull(jitsiRoom.getRoom(), "No domain defined!"); + Assert.notNull(jitsiRoom.getOwner(), "No owner defined!"); + Assert.notNull(userManager.get(jitsiRoom.getOwner()), "Invalid owner defined!"); + + Builder bodyBuilder = new JWTClaimsSet.Builder(); + + bodyBuilder.audience("jitsi"); + bodyBuilder.subject(sub); + bodyBuilder.issuer(iss); + bodyBuilder.claim("room", jitsiRoom.getRoom()); + bodyBuilder.claim("moderator", false); + if (jitsiRoom.getExpires() != null) { + bodyBuilder.expirationTime(Date.from(jitsiRoom.getExpires())); + } + if (jitsiRoom.getStarts() != null) { + bodyBuilder.notBeforeTime(Date.from(jitsiRoom.getStarts())); + } + + JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256).type(JOSEObjectType.JWT) + .build(); + + SignedJWT token = new SignedJWT(jwsHeader, bodyBuilder.build()); + token.sign(signer); + jitsiRoom.setToken(token.serialize()); + bodyBuilder.claim("moderator", true); + if (jitsiRoom.getModerationStarts() != null) { + bodyBuilder.notBeforeTime(Date.from(jitsiRoom.getModerationStarts())); + } + + SignedJWT moderationToken = new SignedJWT(jwsHeader, bodyBuilder.build()); + moderationToken.sign(signer); + jitsiRoom.setModerationToken(moderationToken.serialize()); + + jitsiRoom.setUrl(String.format(urlFormat, jitsiRoom.getRoom(), jitsiRoom.getToken())); + jitsiRoom.setModerationUrl( + String.format(urlFormat, jitsiRoom.getRoom(), jitsiRoom.getModerationToken())); + + if (StringUtils.hasText(jitsiRoom.getCode())) { + ShortenedUrl shortenedUrl = shortenedUrlManager.get(jitsiRoom.getCode()); + if (shortenedUrl != null) { + shortenedUrl.setUrl(jitsiRoom.getUrl()); + shortenedUrl.setNote("JitsiRoom: " + jitsiRoom.getRoom()); + shortenedUrl.setExpires(jitsiRoom.getExpires()); + shortenedUrlManager.save(shortenedUrl); + jitsiRoom.setOrgUrl(jitsiRoom.getUrl()); + jitsiRoom.setUrl(shortenedUrl.getLink()); + } else { + jitsiRoom.setCode(null); + } + } + + return jitsiRoomRepository.save(jitsiRoom); + } + + /** + * + * @param jitsiRoom + * @return + */ + public JitsiRoom createShortenedUrl(JitsiRoom jitsiRoom) { + ShortenedUrl shortenedUrl = shortenedUrlManager.create(jitsiRoom.getOwner(), + jitsiRoom.getUrl(), "JitsiRoom: " + jitsiRoom.getRoom(), null, + jitsiRoom.getExpires(), null, false, true); + jitsiRoom.setOrgUrl(jitsiRoom.getUrl()); + jitsiRoom.setCode(shortenedUrl.getCode()); + jitsiRoom.setUrl(shortenedUrl.getLink()); + return jitsiRoomRepository.save(jitsiRoom); + } + + /** + * + * @param id + */ + public void delete(JitsiRoom jitsiRoom, boolean quota) { + if (quota) { + Quota jitsiRoomsQuota = quotaManager.get(jitsiRoom.getOwner(), JitsiQuotas.JITSI_MEET); + if (jitsiRoomsQuota == null) { + jitsiRoomsQuota = quotaManager.create(jitsiRoom.getOwner(), JitsiQuotas.JITSI_MEET, + 0, "#", true); + } + + jitsiRoomsQuota.setValue(jitsiRoomsQuota.getValue() + 1); + quotaManager.update(jitsiRoomsQuota); + } + + if (StringUtils.hasText(jitsiRoom.getCode())) { + ShortenedUrl shortenedUrl = shortenedUrlRepository + .findOne(qShortenedUrl.code.eq(jitsiRoom.getCode())).orElse(null); + if (shortenedUrl != null) { + if (quota) { + Quota shortenedUrlsQuota = quotaManager.get(shortenedUrl.getOwner(), + ShortenedUrlQuotas.URL_SHORTENER); + if (shortenedUrlsQuota == null) { + shortenedUrlsQuota = quotaManager.create(shortenedUrl.getOwner(), + ShortenedUrlQuotas.URL_SHORTENER, 0, "#", true); + } + + shortenedUrlsQuota.setValue(shortenedUrlsQuota.getValue() + 1); + quotaManager.update(shortenedUrlsQuota); + } + + shortenedUrlRepository.delete(shortenedUrl); + } + } + + jitsiRoomRepository.delete(jitsiRoom); + + } + + /** + * + * @param owner + * @param quota + */ + public void deleteAll(Long owner, boolean quota) { + List jitsiRooms = Lists + .newArrayList(jitsiRoomRepository.findAll(qJitsiRoom.owner.eq(owner))); + + for (JitsiRoom jitsiRoom : jitsiRooms) { + delete(jitsiRoom, quota); + } + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getId() + */ + @Override + public String getId() { + return "jitsiRooms"; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getUserData(java.lang.Long) + */ + @Override + public List getUserData(Long userId) { + List result = Lists.newArrayList(); + for (JitsiRoom jitsiRoom : getAllByOwner(userId)) { + result.add(jitsiRoom); + } + return result; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#purgeUserData(java.lang.Long) + */ + @Override + public void purgeUserData(Long userId) { + for (JitsiRoom jitsiRoom : getAllByOwner(userId)) { + jitsiRoomRepository.delete(jitsiRoom); + } + } + + /** + * + */ + @Scheduled(cron = "0 */5 * * * *") + protected void clearExpiredRooms() { + jitsiRoomRepository + .deleteAll(jitsiRoomRepository.findAll(qJitsiRoom.expires.before(Instant.now()))); + } +} diff --git a/jitsi/src/main/java/de/bstly/we/jitsi/businesslogic/support/UrlShortenerSync.java b/jitsi/src/main/java/de/bstly/we/jitsi/businesslogic/support/UrlShortenerSync.java new file mode 100644 index 0000000..0527fc1 --- /dev/null +++ b/jitsi/src/main/java/de/bstly/we/jitsi/businesslogic/support/UrlShortenerSync.java @@ -0,0 +1,62 @@ +/** + * + */ +package de.bstly.we.jitsi.businesslogic.support; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import de.bstly.we.event.AbstractModelEvent; +import de.bstly.we.event.AbstractModelEventType; +import de.bstly.we.jitsi.model.JitsiRoom; +import de.bstly.we.jitsi.model.QJitsiRoom; +import de.bstly.we.jitsi.repository.JitsiRoomRepository; +import de.bstly.we.urlshortener.model.ShortenedUrl; + +/** + * @author Lurkars + * + */ +@Component +public class UrlShortenerSync implements ApplicationListener { + + @Autowired + private JitsiRoomRepository jitsiRoomRepository; + private QJitsiRoom qJitsiRoom = QJitsiRoom.jitsiRoom; + + /* + * @see org.springframework.context.ApplicationListener#onApplicationEvent(org. + * springframework.context.ApplicationEvent) + */ + @Override + public void onApplicationEvent(AbstractModelEvent event) { + if (event.getModel() instanceof ShortenedUrl) { + if (AbstractModelEventType.PRE_REMOVE.equals(event.getType())) { + ShortenedUrl shortenedUrl = (ShortenedUrl) event.getModel(); + + JitsiRoom jitsiRoom = jitsiRoomRepository + .findOne(qJitsiRoom.code.eq(shortenedUrl.getCode())).orElse(null); + if (jitsiRoom != null) { + jitsiRoom.setUrl(jitsiRoom.getOrgUrl()); + jitsiRoom.setCode(null); + jitsiRoom.setOrgUrl(null); + jitsiRoomRepository.save(jitsiRoom); + } + } else if (AbstractModelEventType.POST_UPDATE.equals(event.getType())) { + ShortenedUrl shortenedUrl = (ShortenedUrl) event.getModel(); + JitsiRoom jitsiRoom = jitsiRoomRepository + .findOne(qJitsiRoom.code.eq(shortenedUrl.getCode())).orElse(null); + if (jitsiRoom != null) { + if (!StringUtils.hasText(jitsiRoom.getOrgUrl())) { + jitsiRoom.setOrgUrl(jitsiRoom.getUrl()); + } + jitsiRoom.setUrl(shortenedUrl.getLink()); + jitsiRoomRepository.save(jitsiRoom); + } + } + } + } + +} diff --git a/jitsi/src/main/java/de/bstly/we/jitsi/controller/JitsiRoomController.java b/jitsi/src/main/java/de/bstly/we/jitsi/controller/JitsiRoomController.java new file mode 100644 index 0000000..4877597 --- /dev/null +++ b/jitsi/src/main/java/de/bstly/we/jitsi/controller/JitsiRoomController.java @@ -0,0 +1,212 @@ +/** + * + */ +package de.bstly.we.jitsi.controller; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.nimbusds.jose.JOSEException; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.Permissions; +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.jitsi.businesslogic.JitsiPermissions; +import de.bstly.we.jitsi.businesslogic.JitsiQuotas; +import de.bstly.we.jitsi.businesslogic.JitsiRoomManager; +import de.bstly.we.jitsi.controller.validation.JitsiRoomValidator; +import de.bstly.we.jitsi.model.JitsiRoom; +import de.bstly.we.model.Quota; +import de.bstly.we.urlshortener.businesslogic.ShortenedUrlPermissions; +import de.bstly.we.urlshortener.businesslogic.ShortenedUrlQuotas; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/jitsi/rooms") +public class JitsiRoomController extends BaseController { + + @Autowired + private JitsiRoomManager jitsiRoomManager; + @Autowired + private JitsiRoomValidator jitsiRoomValidator; + @Autowired + private PermissionManager permissionManager; + @Autowired + private QuotaManager quotaManager; + + /** + * + * @param pageParameter + * @param sizeParameter + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping + public Page getJitsiRooms(@RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter, + @RequestParam("sort") Optional sortParameter, + @RequestParam("desc") Optional descParameter) { + if (!permissionManager.hasPermission(getCurrentUserId(), JitsiPermissions.JITSI_MEET)) { + jitsiRoomManager.deleteAll(getCurrentUserId(), false); + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + return jitsiRoomManager.getForUserId(getCurrentUserId(), pageParameter.orElse(0), + sizeParameter.orElse(10), sortParameter.orElse("id"), descParameter.orElse(false)); + } + + /** + * + * @param jitsiRoom + * @return + */ + @PreAuthorize("isAuthenticated()") + @PostMapping + public JitsiRoom createJitsiRoom(@RequestBody JitsiRoom jitsiRoom) { + + if (!permissionManager.hasPermission(getCurrentUserId(), JitsiPermissions.JITSI_MEET) + || !permissionManager.isFullUser(getCurrentUserId())) { + jitsiRoomManager.deleteAll(getCurrentUserId(), false); + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + Quota jitsiRoomsQuota = quotaManager.get(getCurrentUserId(), JitsiQuotas.JITSI_MEET); + if (jitsiRoomsQuota == null || jitsiRoomsQuota.getValue() < 1) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + jitsiRoom.setOwner(getCurrentUserId()); + Errors errors = new RequestBodyErrors(jitsiRoom); + + if (permissionManager.hasPermission(getCurrentUserId(), Permissions.ROLE_ADMIN)) { + jitsiRoomValidator.validate(jitsiRoom.getOwner(), jitsiRoom.getRoom(), errors); + } else { + jitsiRoomValidator.validate(jitsiRoom, errors); + } + + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + jitsiRoomsQuota.setValue(jitsiRoomsQuota.getValue() - 1); + jitsiRoomsQuota = quotaManager.update(jitsiRoomsQuota); + + try { + return jitsiRoomManager.create(jitsiRoom.getOwner(), jitsiRoom.getRoom(), + jitsiRoom.getStarts(), jitsiRoom.getModerationStarts(), jitsiRoom.getExpires()); + } catch (JOSEException e) { + jitsiRoomsQuota.setValue(jitsiRoomsQuota.getValue() - 1); + jitsiRoomsQuota = quotaManager.update(jitsiRoomsQuota); + throw new EntityResponseStatusException(HttpStatus.EXPECTATION_FAILED); + } + } + + /** + * + * @param jitsiRoom + * @return + */ + @PreAuthorize("isAuthenticated()") + @PatchMapping + public JitsiRoom updateJitsiRoom(@RequestBody JitsiRoom jitsiRoom) { + + if (!permissionManager.hasPermission(getCurrentUserId(), JitsiPermissions.JITSI_MEET) + || !permissionManager.isFullUser(getCurrentUserId())) { + jitsiRoomManager.deleteAll(getCurrentUserId(), false); + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + if (jitsiRoom.getId() == null) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + JitsiRoom origJitsiRoom = jitsiRoomManager.get(jitsiRoom.getId()); + if (origJitsiRoom == null || !origJitsiRoom.getOwner().equals(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + jitsiRoom.setOwner(getCurrentUserId()); + Errors errors = new RequestBodyErrors(jitsiRoom); + + if (permissionManager.hasPermission(getCurrentUserId(), Permissions.ROLE_ADMIN)) { + jitsiRoomValidator.validate(jitsiRoom.getOwner(), jitsiRoom.getRoom(), errors); + } else { + jitsiRoomValidator.validate(jitsiRoom, errors); + } + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + try { + return jitsiRoomManager.save(jitsiRoom); + } catch (JOSEException e) { + throw new EntityResponseStatusException(HttpStatus.EXPECTATION_FAILED); + } + } + + @PreAuthorize("isAuthenticated()") + @PatchMapping("/{id}") + public JitsiRoom createShortenedUrl(@PathVariable("id") Long id) { + JitsiRoom jitsiRoom = jitsiRoomManager.get(id); + if (jitsiRoom == null || !(jitsiRoom.getOwner().equals(getCurrentUserId()))) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + if (!permissionManager.hasPermission(getCurrentUserId(), + ShortenedUrlPermissions.URL_SHORTENER) + || !permissionManager.isFullUser(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + Quota shortenedUrlsQuota = quotaManager.get(getCurrentUserId(), + ShortenedUrlQuotas.URL_SHORTENER); + if (shortenedUrlsQuota == null || shortenedUrlsQuota.getValue() < 1) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + // create shortened Url + return jitsiRoomManager.createShortenedUrl(jitsiRoom); + } + + /** + * + * @param id + */ + @PreAuthorize("isAuthenticated()") + @DeleteMapping("/{id}") + public void deleteJitsiRoom(@PathVariable("id") Long id) { + + if (!permissionManager.hasPermission(getCurrentUserId(), JitsiPermissions.JITSI_MEET)) { + jitsiRoomManager.deleteAll(getCurrentUserId(), false); + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + JitsiRoom jitsiRoom = jitsiRoomManager.get(id); + if (jitsiRoom == null || !jitsiRoom.getOwner().equals(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + jitsiRoomManager.delete(jitsiRoom, true); + } + +} diff --git a/jitsi/src/main/java/de/bstly/we/jitsi/controller/JitsiRoomManagementController.java b/jitsi/src/main/java/de/bstly/we/jitsi/controller/JitsiRoomManagementController.java new file mode 100644 index 0000000..313923a --- /dev/null +++ b/jitsi/src/main/java/de/bstly/we/jitsi/controller/JitsiRoomManagementController.java @@ -0,0 +1,105 @@ +/** + * + */ +package de.bstly.we.jitsi.controller; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.nimbusds.jose.JOSEException; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.jitsi.businesslogic.JitsiRoomManager; +import de.bstly.we.jitsi.controller.validation.JitsiRoomValidator; +import de.bstly.we.jitsi.model.JitsiRoom; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/jitsi/rooms/manage") +public class JitsiRoomManagementController extends BaseController { + + @Autowired + private JitsiRoomManager jitsiRoomManager; + @Autowired + private JitsiRoomValidator jitsiRoomValidator; + + /** + * + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getJitsiRooms(@RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter) { + return jitsiRoomManager.get(pageParameter.orElse(0), sizeParameter.orElse(10), "id", true); + } + + /** + * + * @param jitsiRoom + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public JitsiRoom createOrUpdateJitsiRoom(@RequestBody JitsiRoom jitsiRoom) { + Errors errors = new RequestBodyErrors(jitsiRoom); + jitsiRoomValidator.validate(jitsiRoom.getOwner(), jitsiRoom.getRoom(), errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + try { + return jitsiRoomManager.save(jitsiRoom); + } catch (JOSEException e) { + e.printStackTrace(); + return null; + } + } + + /** + * + * @param id + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/{id}") + public void deleteJitsiRoom(@PathVariable("id") Long id, + @RequestParam("quota") Optional quota) { + + JitsiRoom jitsiRoom = jitsiRoomManager.get(id); + if (jitsiRoom == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + jitsiRoomManager.delete(jitsiRoom, quota.isPresent() && quota.get().booleanValue()); + } + + /** + * + * @param owner + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/all/{owner}") + public void deleteAll(@PathVariable("owner") Long owner, + @RequestParam("quota") Optional quota) { + jitsiRoomManager.deleteAll(owner, quota.isPresent() && quota.get().booleanValue()); + } + +} diff --git a/jitsi/src/main/java/de/bstly/we/jitsi/controller/validation/JitsiRoomValidator.java b/jitsi/src/main/java/de/bstly/we/jitsi/controller/validation/JitsiRoomValidator.java new file mode 100644 index 0000000..fb4fbd7 --- /dev/null +++ b/jitsi/src/main/java/de/bstly/we/jitsi/controller/validation/JitsiRoomValidator.java @@ -0,0 +1,189 @@ +/** + * + */ +package de.bstly.we.jitsi.controller.validation; + +import java.time.Instant; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import de.bstly.we.businesslogic.SystemPropertyManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.jitsi.model.JitsiRoom; +import de.bstly.we.jitsi.model.QJitsiRoom; +import de.bstly.we.jitsi.repository.JitsiRoomRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class JitsiRoomValidator implements Validator { + + public static final String RESERVED_JITSI_ROOMS = "jitsi.rooms.reserved"; + protected static final String ROOM_NAME_REGEX = "^([a-zA-Z0-9]+)$"; + + @Autowired + private UserManager userManager; + @Autowired + private SystemPropertyManager systemPropertyManager; + @Autowired + private JitsiRoomRepository jitsiRoomRepository; + private QJitsiRoom qJitsiRoom = QJitsiRoom.jitsiRoom; + + /* + * @see org.springframework.validation.Validator#supports(java.lang.Class) + */ + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(JitsiRoom.class); + } + + /* + * @see org.springframework.validation.Validator#validate(java.lang.Object, + * org.springframework.validation.Errors) + */ + @Override + public void validate(Object target, Errors errors) { + JitsiRoom jitsiRoom = (JitsiRoom) target; + + validate(jitsiRoom.getOwner(), jitsiRoom.getRoom(), errors); + + if (errors.hasErrors()) { + return; + } + + validateRoom(jitsiRoom.getRoom(), errors); + + validateExpiry(jitsiRoom, errors); + + } + + /** + * + * @param roomName + * @param field + * @param errors + */ + public void validate(Long owner, String roomName, Errors errors) { + if (owner == null) { + errors.rejectValue("owner", "REQUIRED"); + return; + } + + if (userManager.get(owner) == null) { + errors.rejectValue("owner", "NOT_VALID"); + return; + } + + if (!StringUtils.hasText(roomName)) { + errors.rejectValue("room", "REQUIRED"); + return; + } + + if (!roomName.matches(ROOM_NAME_REGEX)) { + errors.rejectValue("room", "NOT_VALID"); + return; + } + } + + /** + * + * @param owner + * @param roomName + * @param errors + */ + public void validateRoom(String roomName, Errors errors) { + for (String systemRoomName : systemPropertyManager.get(RESERVED_JITSI_ROOMS, "") + .split(",")) { + if (StringUtils.hasText(systemRoomName) + && (roomName.toLowerCase().equals(systemRoomName) + || roomName.toLowerCase().matches(systemRoomName))) { + errors.rejectValue("room", "NOT_VALID"); + return; + } + } + } + + /** + * + * @param jitsiRoom + * @param errors + */ + public void validateExpiry(JitsiRoom jitsiRoom, Errors errors) { + + // no moderation start without start + if (jitsiRoom.getStarts() == null && jitsiRoom.getModerationStarts() != null) { + errors.rejectValue("moderationStarts", "NOT_VALID"); + return; + } else + // no moderation before start + if (jitsiRoom.getStarts() != null && jitsiRoom.getModerationStarts() != null + && jitsiRoom.getStarts().isBefore(jitsiRoom.getModerationStarts())) { + errors.rejectValue("moderationStarts", "NOT_VALID"); + return; + } + + // no expiry + no start + if (jitsiRoom.getExpires() == null && jitsiRoom.getStarts() == null) { + if (jitsiRoomRepository.exists(qJitsiRoom.room.eq(jitsiRoom.getRoom()) + // exlude self + .and(qJitsiRoom.id.ne(jitsiRoom.getId() == null ? -1L : jitsiRoom.getId())) + // expires null or after now + .and(qJitsiRoom.expires.isNull() + .or(qJitsiRoom.expires.after(Instant.now()))))) { + errors.rejectValue("room", "NOT_VALID"); + } + } else + // expiry + no start + if (jitsiRoom.getExpires() != null && jitsiRoom.getStarts() == null) { + if (jitsiRoomRepository.exists(qJitsiRoom.room.eq(jitsiRoom.getRoom()) + // exlude self + .and(qJitsiRoom.id.ne(jitsiRoom.getId() == null ? -1L : jitsiRoom.getId())) + // expires null or after now + .and(qJitsiRoom.expires.isNull().or(qJitsiRoom.expires.after(Instant.now()))) + // start null or before expires + .and(qJitsiRoom.starts.isNull() + .or(qJitsiRoom.starts.before(jitsiRoom.getExpires()))))) { + errors.rejectValue("expires", "NOT_VALID"); + } + } + // no expiry + start + if (jitsiRoom.getExpires() == null && jitsiRoom.getStarts() != null) { + if (jitsiRoomRepository.exists(qJitsiRoom.room.eq(jitsiRoom.getRoom()) + // exlude self + .and(qJitsiRoom.id.ne(jitsiRoom.getId() == null ? -1L : jitsiRoom.getId())) + // expires null or after room start + .and(qJitsiRoom.expires.isNull() + .or(qJitsiRoom.expires.after(jitsiRoom.getModerationStarts() != null + ? jitsiRoom.getModerationStarts() + : jitsiRoom.getStarts()))))) { + errors.rejectValue("starts", "NOT_VALID"); + } + } else + // expiry + start + if (jitsiRoom.getExpires() != null && jitsiRoom.getStarts() != null) { + if (jitsiRoom.getStarts().isAfter(jitsiRoom.getExpires()) + || jitsiRoomRepository.exists(qJitsiRoom.room.eq(jitsiRoom.getRoom()) + // exlude self + .and(qJitsiRoom.id + .ne(jitsiRoom.getId() == null ? -1L : jitsiRoom.getId())) + // expires null or after room start + .and(qJitsiRoom.expires.isNull().or( + qJitsiRoom.expires.after(jitsiRoom.getModerationStarts() != null + ? jitsiRoom.getModerationStarts() + : jitsiRoom.getStarts()))) + // start null or before expires + .and(qJitsiRoom.starts.isNull() + .or(qJitsiRoom.starts.before(jitsiRoom.getExpires()))))) { + errors.rejectValue("starts", "NOT_VALID"); + errors.rejectValue("expires", "NOT_VALID"); + } + } + } + +} diff --git a/jitsi/src/main/java/de/bstly/we/jitsi/model/JitsiRoom.java b/jitsi/src/main/java/de/bstly/we/jitsi/model/JitsiRoom.java new file mode 100644 index 0000000..5406ea0 --- /dev/null +++ b/jitsi/src/main/java/de/bstly/we/jitsi/model/JitsiRoom.java @@ -0,0 +1,221 @@ +/** + * + */ +package de.bstly.we.jitsi.model; + +import java.time.Instant; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; + +import org.springframework.data.annotation.Reference; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import de.bstly.we.model.UserData; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "jitsi_rooms") +public class JitsiRoom implements UserData { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + private Long owner; + private String room; + private Instant starts; + private Instant moderationStarts; + private Instant expires; + @Lob + @JsonIgnore + private String token; + @Lob + @JsonIgnore + private String moderationToken; + @Lob + private String url; + @Lob + private String moderationUrl; + @Reference + private String code; + @Lob + private String orgUrl; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the owner + */ + public Long getOwner() { + return owner; + } + + /** + * @param owner the owner to set + */ + public void setOwner(Long owner) { + this.owner = owner; + } + + /** + * @return the room + */ + public String getRoom() { + return room; + } + + /** + * @param room the room to set + */ + public void setRoom(String room) { + this.room = room; + } + + /** + * @return the starts + */ + public Instant getStarts() { + return starts; + } + + /** + * @param starts the starts to set + */ + public void setStarts(Instant starts) { + this.starts = starts; + } + + /** + * @return the moderationStarts + */ + public Instant getModerationStarts() { + return moderationStarts; + } + + /** + * @param moderationStarts the moderationStarts to set + */ + public void setModerationStarts(Instant moderationStarts) { + this.moderationStarts = moderationStarts; + } + + /** + * @return the expires + */ + public Instant getExpires() { + return expires; + } + + /** + * @param expires the expires to set + */ + public void setExpires(Instant expires) { + this.expires = expires; + } + + /** + * @return the token + */ + public String getToken() { + return token; + } + + /** + * @param token the token to set + */ + public void setToken(String token) { + this.token = token; + } + + /** + * @return the moderationToken + */ + public String getModerationToken() { + return moderationToken; + } + + /** + * @param moderationToken the moderationToken to set + */ + public void setModerationToken(String moderationToken) { + this.moderationToken = moderationToken; + } + + /** + * @return the url + */ + public String getUrl() { + return url; + } + + /** + * @param url the url to set + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * @return the moderationUrl + */ + public String getModerationUrl() { + return moderationUrl; + } + + /** + * @param moderationUrl the moderationUrl to set + */ + public void setModerationUrl(String moderationUrl) { + this.moderationUrl = moderationUrl; + } + + /** + * @return the code + */ + public String getCode() { + return code; + } + + /** + * @param code the code to set + */ + public void setCode(String code) { + this.code = code; + } + + /** + * @return the orgUrl + */ + public String getOrgUrl() { + return orgUrl; + } + + /** + * @param orgUrl the orgUrl to set + */ + public void setOrgUrl(String orgUrl) { + this.orgUrl = orgUrl; + } +} diff --git a/jitsi/src/main/java/de/bstly/we/jitsi/repository/JitsiRoomRepository.java b/jitsi/src/main/java/de/bstly/we/jitsi/repository/JitsiRoomRepository.java new file mode 100755 index 0000000..b746e8b --- /dev/null +++ b/jitsi/src/main/java/de/bstly/we/jitsi/repository/JitsiRoomRepository.java @@ -0,0 +1,20 @@ +/** + * + */ +package de.bstly.we.jitsi.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.jitsi.model.JitsiRoom; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface JitsiRoomRepository + extends JpaRepository, QuerydslPredicateExecutor { +} diff --git a/membership/pom.xml b/membership/pom.xml new file mode 100644 index 0000000..4033354 --- /dev/null +++ b/membership/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + de.bstly.we + webstly-main + ${revision} + + + membership + webstly-membership + + + + de.bstly.we + webstly-core + ${revision} + + + diff --git a/membership/src/main/java/de/bstly/we/membership/businesslogic/MembershipManager.java b/membership/src/main/java/de/bstly/we/membership/businesslogic/MembershipManager.java new file mode 100644 index 0000000..c8fb46e --- /dev/null +++ b/membership/src/main/java/de/bstly/we/membership/businesslogic/MembershipManager.java @@ -0,0 +1,816 @@ +/** + * + */ +package de.bstly.we.membership.businesslogic; + +import java.io.IOException; +import java.io.Writer; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.Period; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +import com.beust.jcommander.internal.Lists; +import com.beust.jcommander.internal.Maps; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import de.bstly.we.businesslogic.PretixManager; +import de.bstly.we.businesslogic.SystemPropertyManager; +import de.bstly.we.businesslogic.support.InstantHelper; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class MembershipManager { + + @Autowired + private PretixManager pretixManager; + @Autowired + private SystemPropertyManager systemPropertyManager; + + private Logger logger = LoggerFactory.getLogger(MembershipManager.class); + private DateFormat pretixDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + + /** + * + */ + public void generateMembershipIds() { + String membershipItemId = systemPropertyManager + .get(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ITEM, ""); + + int membershipInternalIdQuestion = systemPropertyManager.getInteger( + PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_INTERNAL_ID_QUESTION, 0); + + int membershipIdQuestion = systemPropertyManager + .getInteger(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ID_QUESTION, 0); + + if (!StringUtils.hasText(membershipItemId) || membershipInternalIdQuestion == 0 + || membershipIdQuestion == 0) { + logger.warn("Required system properties for 'generateMembershipIds' not set!"); + return; + } + + MultiValueMap queryParams = new LinkedMultiValueMap(); + queryParams.add("ordering", "order__datetime"); + queryParams.add("item", membershipItemId); + queryParams.add("order__status", "p"); + + List orderPositions = fetchAllOrderPositions( + String.format("/api/v1/organizers/%s/events/%s/orderpositions/", + pretixManager.getOrganizer(), pretixManager.getEvent()), + queryParams, Lists.newArrayList()); + + Map newMembershipPositions = new TreeMap(); + + int nextMembershipId = 1; + for (JsonObject orderPosition : orderPositions) { + JsonArray answers = new JsonArray(); + if (orderPosition.has("answers")) { + answers = orderPosition.getAsJsonArray("answers"); + } + + boolean hasInternalMembershipId = false; + + int internalMembershipId = getNumberAnswer(orderPosition, membershipInternalIdQuestion); + + if (internalMembershipId > 0) { + if (internalMembershipId >= nextMembershipId) { + nextMembershipId = internalMembershipId + 1; + } + hasInternalMembershipId = true; + } + + if (!hasInternalMembershipId) { + newMembershipPositions.put(pretixManager.getLastPaymentDateForOrder( + orderPosition.get("order").getAsString()), orderPosition); + } else { + boolean writeMembershipId = true; + + JsonObject membershipIdAnswer = getAnswerObject(orderPosition, + membershipIdQuestion); + + if (membershipIdAnswer != null) { + int membershipId = Integer.valueOf(membershipIdAnswer.get("answer").getAsInt()); + if (internalMembershipId == membershipId) { + writeMembershipId = false; + logger.trace("Order '" + orderPosition.get("order").getAsString() + + "' already processed, membership id: '" + membershipId + "'"); + } + } + + if (writeMembershipId) { + if (membershipIdAnswer == null) { + membershipIdAnswer = new JsonObject(); + membershipIdAnswer.addProperty("question", membershipIdQuestion); + membershipIdAnswer.add("options", new JsonArray()); + membershipIdAnswer.add("option_identifiers", new JsonArray()); + answers.add(membershipIdAnswer); + } + + membershipIdAnswer.addProperty("answer", internalMembershipId); + + JsonObject payload = new JsonObject(); + payload.add("answers", answers); + + logger.debug( + "Write membershipId for '" + orderPosition.get("order").getAsString() + + "': " + internalMembershipId); + + pretixManager.request( + String.format("/api/v1/organizers/%s/events/%s/orderpositions/%s/", + pretixManager.getOrganizer(), pretixManager.getEvent(), + orderPosition.get("id").getAsString()), + HttpMethod.PATCH, payload); + } + } + } + + Iterator> iterator = newMembershipPositions.entrySet() + .iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + JsonObject orderPosition = entry.getValue(); + JsonArray answers = new JsonArray(); + if (orderPosition.has("answers")) { + answers = orderPosition.getAsJsonArray("answers"); + } + + JsonObject membershipInternalIdAnswer = new JsonObject(); + membershipInternalIdAnswer.addProperty("question", membershipInternalIdQuestion); + membershipInternalIdAnswer.add("options", new JsonArray()); + membershipInternalIdAnswer.add("option_identifiers", new JsonArray()); + membershipInternalIdAnswer.addProperty("answer", nextMembershipId); + answers.add(membershipInternalIdAnswer); + + JsonObject membershipIdAnswer = new JsonObject(); + membershipIdAnswer.addProperty("question", membershipIdQuestion); + membershipIdAnswer.add("options", new JsonArray()); + membershipIdAnswer.add("option_identifiers", new JsonArray()); + membershipIdAnswer.addProperty("answer", nextMembershipId); + answers.add(membershipIdAnswer); + + JsonObject payload = new JsonObject(); + + payload.add("answers", answers); + + logger.debug("Create membershipId for '" + orderPosition.get("order").getAsString() + + "': " + nextMembershipId); + + pretixManager + .request( + String.format("/api/v1/organizers/%s/events/%s/orderpositions/%s/", + pretixManager.getOrganizer(), pretixManager.getEvent(), + orderPosition.get("id").getAsString()), + HttpMethod.PATCH, payload); + + nextMembershipId++; + } + } + + /** + * + */ + public void activeMemberships() { + + String membershipItemId = systemPropertyManager + .get(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ITEM, ""); + + String membershipFeeItemId = systemPropertyManager + .get(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_ITEM, ""); + + int membershipInternalIdQuestion = systemPropertyManager.getInteger( + PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_INTERNAL_ID_QUESTION, 0); + + int membershipFeeReferenceQuestion = systemPropertyManager.getInteger( + PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_REFERENCE_QUESTION, 0); + + int membershipActiveQuestion = systemPropertyManager + .getInteger(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ACTIVE_QUESTION, 0); + + if (!StringUtils.hasText(membershipItemId) || !StringUtils.hasText(membershipFeeItemId) + || membershipInternalIdQuestion == 0 || membershipFeeReferenceQuestion == 0 + || membershipActiveQuestion == 0) { + logger.warn("Required system properties for 'activeMemberships' not set!"); + return; + } + + MultiValueMap queryParams = new LinkedMultiValueMap(); + queryParams.add("ordering", "order__datetime"); + queryParams.add("item", membershipItemId); + queryParams.add("order__status", "p"); + + List membershipOrderPositions = fetchAllOrderPositions( + String.format("/api/v1/organizers/%s/events/%s/orderpositions/", + pretixManager.getOrganizer(), pretixManager.getEvent()), + queryParams, Lists.newArrayList()); + + queryParams.remove("item"); + queryParams.add("item", membershipFeeItemId); + + List membershipfeeOrderPositions = fetchAllOrderPositions( + String.format("/api/v1/organizers/%s/events/%s/orderpositions/", + pretixManager.getOrganizer(), pretixManager.getEvent()), + queryParams, Lists.newArrayList()); + + Map> membershipfeeOrderPositionMap = Maps.newHashMap(); + + for (JsonObject membershipFeeOrderPosition : membershipfeeOrderPositions) { + int membershipReference = getNumberAnswer(membershipFeeOrderPosition, + membershipFeeReferenceQuestion); + if (!membershipfeeOrderPositionMap.containsKey(membershipReference)) { + membershipfeeOrderPositionMap.put(membershipReference, Lists.newArrayList()); + } + membershipfeeOrderPositionMap.get(membershipReference).add(membershipFeeOrderPosition); + } + + for (JsonObject membershipOrderPosition : membershipOrderPositions) { + JsonArray answers = new JsonArray(); + if (membershipOrderPosition.has("answers")) { + answers = membershipOrderPosition.getAsJsonArray("answers"); + } + + JsonObject membershipActiveAnswer = getAnswerObject(membershipOrderPosition, + membershipActiveQuestion); + int membershipId = getNumberAnswer(membershipOrderPosition, + membershipInternalIdQuestion); + + if (membershipId == 0) { + logger.warn("No membership number found for '" + + membershipOrderPosition.get("order").getAsString() + "'!"); + continue; + } + + Instant membershipStart = pretixManager + .getLastPaymentDateForOrder(membershipOrderPosition.get("order").getAsString()); + + if (membershipStart == null) { + logger.warn("No last payment found for '" + + membershipOrderPosition.get("order").getAsString() + "'!"); + continue; + } + + int years = 1; + + if (membershipfeeOrderPositionMap.containsKey(membershipId)) { + years += membershipfeeOrderPositionMap.get(membershipId).size(); + } + + Instant active = InstantHelper.truncate( + InstantHelper.plus(membershipStart, years, ChronoUnit.YEARS), ChronoUnit.YEARS); + + String activeAnswer = pretixDateFormat.format(Date.from(active)); + + if (membershipActiveAnswer == null) { + membershipActiveAnswer = new JsonObject(); + membershipActiveAnswer.addProperty("question", membershipActiveQuestion); + membershipActiveAnswer.add("options", new JsonArray()); + membershipActiveAnswer.add("option_identifiers", new JsonArray()); + answers.add(membershipActiveAnswer); + } + + if (!membershipActiveAnswer.has("answer") + || !activeAnswer.equals(membershipActiveAnswer.get("answer").getAsString())) { + membershipActiveAnswer.addProperty("answer", activeAnswer); + + JsonObject payload = new JsonObject(); + + payload.add("answers", answers); + pretixManager.request( + String.format("/api/v1/organizers/%s/events/%s/orderpositions/%s/", + pretixManager.getOrganizer(), pretixManager.getEvent(), + membershipOrderPosition.get("id").getAsString()), + HttpMethod.PATCH, payload); + + logger.debug("Write active state for '" + + membershipOrderPosition.get("order").getAsString() + "': " + + activeAnswer); + } else { + logger.trace("Order active state for '" + + membershipOrderPosition.get("order").getAsString() + "' already set: " + + activeAnswer); + } + } + } + + /** + * + */ + public void createMembershipExtension() { + + String membershipItemId = systemPropertyManager + .get(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ITEM, ""); + + String membershipFeeItemId = systemPropertyManager + .get(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_ITEM, ""); + + int membershipInternalIdQuestion = systemPropertyManager.getInteger( + PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_INTERNAL_ID_QUESTION, 0); + + int membershipFeeReferenceQuestion = systemPropertyManager.getInteger( + PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_REFERENCE_QUESTION, 0); + + int membershipFeeCodeQuestion = systemPropertyManager + .getInteger(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_CODE_QUESTION, 0); + + int membershipActiveQuestion = systemPropertyManager + .getInteger(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ACTIVE_QUESTION, 0); + + int membershipfeeOrderDays = systemPropertyManager + .getInteger(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_ORDER_DAYS, 0); + + int membershipfeeReminderDays = systemPropertyManager + .getInteger(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_REMINDER_DAYS, 0); + + int membershipfeeReminderQuestion = systemPropertyManager.getInteger( + PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIPFEE_REMINDER_QUESTION, 0); + + boolean orderSendmail = systemPropertyManager + .getBoolean(PretixManager.SYSTEM_PROPERTY_PRETIX_ORDER_SENDMAIL, false); + boolean orderTestmode = systemPropertyManager + .getBoolean(PretixManager.SYSTEM_PROPERTY_PRETIX_ORDER_TESTMODE, true); + boolean orderSimulate = systemPropertyManager + .getBoolean(PretixManager.SYSTEM_PROPERTY_PRETIX_ORDER_SIMULATE, false); + + if (!StringUtils.hasText(membershipItemId) || !StringUtils.hasText(membershipFeeItemId) + || membershipInternalIdQuestion == 0 || membershipFeeReferenceQuestion == 0 + || membershipFeeCodeQuestion == 0 || membershipActiveQuestion == 0 + || membershipfeeOrderDays == 0) { + logger.warn("Required system properties for 'createMembershipExtension' not set!"); + return; + } + + MultiValueMap queryMembershipParams = new LinkedMultiValueMap(); + queryMembershipParams.add("ordering", "order__datetime"); + queryMembershipParams.add("item", membershipItemId); + queryMembershipParams.add("order__status", "p"); + + List membershipOrderPositions = fetchAllOrderPositions( + String.format("/api/v1/organizers/%s/events/%s/orderpositions/", + pretixManager.getOrganizer(), pretixManager.getEvent()), + queryMembershipParams, Lists.newArrayList()); + + List expireMemberships = Lists.newArrayList(); + + for (JsonObject membershipOrderPosition : membershipOrderPositions) { + Instant active = getDateAnswer(membershipOrderPosition, membershipActiveQuestion); + if (active.isAfter(InstantHelper.truncate(Instant.now(), ChronoUnit.YEARS)) + && InstantHelper.minus(active, membershipfeeOrderDays, ChronoUnit.DAYS) + .isBefore(Instant.now())) { + expireMemberships.add(membershipOrderPosition); + } else { + logger.trace("Membership '" + membershipOrderPosition.get("order").getAsString() + + "' not expiring in next " + membershipfeeOrderDays + " days."); + } + } + + MultiValueMap queryMembershipFeeParams = new LinkedMultiValueMap(); + queryMembershipFeeParams.add("ordering", "order__datetime"); + queryMembershipFeeParams.add("item", membershipFeeItemId); + queryMembershipFeeParams.add("order__status__in", "n,e,c"); + + List membershipfeeOrderPositions = fetchAllOrderPositions( + String.format("/api/v1/organizers/%s/events/%s/orderpositions/", + pretixManager.getOrganizer(), pretixManager.getEvent()), + queryMembershipFeeParams, Lists.newArrayList()); + + Map> membershipfeeOrderPositionMap = Maps.newHashMap(); + + for (JsonObject membershipfeeOrderPosition : membershipfeeOrderPositions) { + int membershipReference = getNumberAnswer(membershipfeeOrderPosition, + membershipFeeReferenceQuestion); + + JsonObject membershipFeeOrder = pretixManager + .getOrder(membershipfeeOrderPosition.get("order").getAsString()); + + Instant membershipFeeOrderDatetime = OffsetDateTime + .parse(membershipFeeOrder.get("datetime").getAsString()).toInstant(); + + if (membershipFeeOrderDatetime + .isAfter(InstantHelper.truncate(Instant.now(), ChronoUnit.YEARS))) { + if (!membershipfeeOrderPositionMap.containsKey(membershipReference)) { + membershipfeeOrderPositionMap.put(membershipReference, Lists.newArrayList()); + } + + membershipfeeOrderPositionMap.get(membershipReference) + .add(membershipfeeOrderPosition); + } + } + + for (JsonObject membershipOrderPosition : expireMemberships) { + int membershipId = getNumberAnswer(membershipOrderPosition, + membershipInternalIdQuestion); + Instant active = getDateAnswer(membershipOrderPosition, membershipActiveQuestion); + + if (membershipId == 0) { + logger.warn("No membership number found for '" + + membershipOrderPosition.get("order").getAsString() + "'!"); + continue; + } + + if (!membershipfeeOrderPositionMap.containsKey(membershipId) + || membershipfeeOrderPositionMap.get(membershipId).isEmpty()) { + JsonObject membershipOrder = pretixManager + .getOrder(membershipOrderPosition.get("order").getAsString()); + + JsonObject membershipFeeOrder = new JsonObject(); + membershipFeeOrder.add("email", membershipOrder.get("email")); + membershipFeeOrder.add("locale", membershipOrder.get("locale")); + + JsonArray membershipFeeOrderPositions = new JsonArray(); + JsonObject membershipFeeOrderPosition = new JsonObject(); + + membershipFeeOrderPosition.addProperty("item", membershipFeeItemId); + + JsonArray membershipFeeOrderPositionAnswers = new JsonArray(); + JsonObject membershipFeeReferenceAnswer = new JsonObject(); + membershipFeeReferenceAnswer.addProperty("question", + membershipFeeReferenceQuestion); + membershipFeeReferenceAnswer.add("options", new JsonArray()); + membershipFeeReferenceAnswer.add("option_identifiers", new JsonArray()); + membershipFeeReferenceAnswer.addProperty("answer", membershipId); + membershipFeeOrderPositionAnswers.add(membershipFeeReferenceAnswer); + + JsonObject membershipFeeCodeAnswer = new JsonObject(); + membershipFeeCodeAnswer.addProperty("question", + membershipFeeCodeQuestion); + membershipFeeCodeAnswer.add("options", new JsonArray()); + membershipFeeCodeAnswer.add("option_identifiers", new JsonArray()); + membershipFeeCodeAnswer.addProperty("answer", membershipOrderPosition.get("order").getAsString()); + membershipFeeOrderPositionAnswers.add(membershipFeeCodeAnswer); + + membershipFeeOrderPosition.add("answers", membershipFeeOrderPositionAnswers); + + membershipFeeOrderPositions.add(membershipFeeOrderPosition); + + membershipFeeOrder.add("locale", membershipOrder.get("locale")); + membershipFeeOrder.add("positions", membershipFeeOrderPositions); + + if (membershipOrder.has("payments") + && membershipOrder.getAsJsonArray("payments").size() > 0) { + JsonObject payment = membershipOrder.getAsJsonArray("payments").get(0) + .getAsJsonObject(); + membershipFeeOrder.add("payment_provider", payment.get("provider")); + } + + membershipFeeOrder.addProperty("send_email", orderSendmail); + membershipFeeOrder.addProperty("testmode", orderTestmode); + membershipFeeOrder.addProperty("simulate", orderSimulate); + + membershipFeeOrder = pretixManager.createOrder(membershipFeeOrder); + + JsonObject expire = new JsonObject(); + Instant expires = InstantHelper.minus(InstantHelper.truncate( + InstantHelper.plus(Instant.now(), 1, ChronoUnit.YEARS), ChronoUnit.YEARS), + 1, ChronoUnit.DAYS); + expire.addProperty("expires", pretixDateFormat.format(Date.from(expires))); + membershipFeeOrder = pretixManager + .extendOrder(membershipFeeOrder.get("code").getAsString(), expire); + + logger.debug("Create membership fee order: " + membershipFeeOrder.toString()); + } else + // send reminder mail for membership fee order + if (membershipfeeReminderDays > 0 && membershipfeeReminderQuestion > 0 + && active.isAfter(InstantHelper.truncate(Instant.now(), ChronoUnit.YEARS)) + && InstantHelper.minus(active, membershipfeeReminderDays, ChronoUnit.DAYS) + .isBefore(Instant.now())) { + + for (JsonObject membershipfeeOrderPosition : membershipfeeOrderPositionMap + .get(membershipId)) { + + JsonObject membershipFeeOrder = pretixManager + .getOrder(membershipfeeOrderPosition.get("order").getAsString()); + + if (!"c".equals(membershipFeeOrder.get("status").getAsString())) { + JsonArray membershipfeeAnswers = new JsonArray(); + if (membershipfeeOrderPosition.has("answers")) { + membershipfeeAnswers = membershipfeeOrderPosition + .getAsJsonArray("answers"); + } + + JsonObject membershipReminderAnswer = getAnswerObject( + membershipfeeOrderPosition, membershipfeeReminderQuestion); + + if (membershipReminderAnswer == null) { + membershipReminderAnswer = new JsonObject(); + membershipReminderAnswer.addProperty("question", + membershipfeeReminderQuestion); + membershipReminderAnswer.add("options", new JsonArray()); + membershipReminderAnswer.add("option_identifiers", new JsonArray()); + membershipReminderAnswer.addProperty("answer", "false"); + membershipfeeAnswers.add(membershipReminderAnswer); + } + + if (!membershipReminderAnswer.get("answer").getAsBoolean()) { + membershipReminderAnswer.addProperty("answer", "true"); + + pretixManager.sendEmail( + membershipfeeOrderPosition.get("order").getAsString()); + + JsonObject payload = new JsonObject(); + + payload.add("answers", membershipfeeAnswers); + pretixManager.request( + String.format( + "/api/v1/organizers/%s/events/%s/orderpositions/%s/", + pretixManager.getOrganizer(), pretixManager.getEvent(), + membershipfeeOrderPosition.get("id").getAsString()), + HttpMethod.PATCH, payload); + + logger.debug("Send reminder for membership fee order: " + + membershipfeeOrderPosition.get("order").getAsString()); + } else { + logger.debug("Reminder for Membership fee order of'" + + membershipfeeOrderPosition.get("order").getAsString() + + "' already created."); + } + } else { + logger.debug("Membership fee order for'" + + membershipfeeOrderPosition.get("order").getAsString() + + "' is canceled, no reminder."); + } + } + } else { + logger.debug("Membership fee order for '" + + membershipOrderPosition.get("order").getAsString() + + "' already created, no reminder."); + } + + } + } + + /** + * + */ + public void calculatePrice() { + int membershipItemId = systemPropertyManager + .getInteger(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ITEM, 0); + String discountPeriod = systemPropertyManager + .get(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_DISCOUNT_PERIOD, ""); + + if (!StringUtils.hasText(discountPeriod) || membershipItemId == 0) { + logger.warn("Required system properties for 'calculatePrice' not set!"); + return; + } + + try { + int position = -1; + Period period = Period.parse(discountPeriod); + + Period oneYear = Period.ofYears(1); + + long variations = InstantHelper.plus(Instant.ofEpochMilli(0), oneYear).getEpochSecond() + / InstantHelper.plus(Instant.ofEpochMilli(0), period).getEpochSecond(); + + Instant compare = InstantHelper.truncate(Instant.now(), ChronoUnit.YEARS); + + while (compare.isBefore(Instant.now())) { + compare = InstantHelper.plus(compare, period); + position++; + } + + JsonObject defaultVariation = new JsonObject(); + defaultVariation.addProperty("active", true); + int defaultVariationId = 0; + boolean applyDefault = true; + + for (JsonElement variationElement : pretixManager.getVariations(membershipItemId)) { + JsonObject variation = variationElement.getAsJsonObject(); + JsonObject variationUpdate = new JsonObject(); + int variationId = variation.get("id").getAsInt(); + int variationPosition = variation.get("position").getAsInt(); + + if (variationPosition < variations) { + + variationUpdate.addProperty("active", false); + if (variationPosition == 0) { + defaultVariationId = variationId; + } + + if (variationPosition == position) { + variationUpdate.addProperty("active", true); + applyDefault = false; + } + pretixManager.updateVariation(membershipItemId, variationId, variationUpdate); + } + } + + if (applyDefault && defaultVariationId > 0) { + pretixManager.updateVariation(membershipItemId, defaultVariationId, + defaultVariation); + } + + } catch (DateTimeParseException e) { + logger.error("Error in parsing 'pretix.membership.discount.period'!", e); + } + } + + /** + * + * @param writer + * @param includeInactive + * @return + */ + public JsonArray membershipCsvExport(Writer writer, boolean includeInactive) { + String membershipItemId = systemPropertyManager + .get(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ITEM, "0"); + + int membershipActiveQuestion = systemPropertyManager + .getInteger(PretixManager.SYSTEM_PROPERTY_PRETIX_MEMBERSHIP_ACTIVE_QUESTION, 0); + + MultiValueMap queryParams = new LinkedMultiValueMap(); + queryParams.add("item", membershipItemId); + queryParams.add("order__status", "p"); + + JsonArray members = new JsonArray(); + + List answerHeaders = Lists.newArrayList(); + + List orderPositions = fetchAllOrderPositions( + String.format("/api/v1/organizers/%s/events/%s/orderpositions/", + pretixManager.getOrganizer(), pretixManager.getEvent()), + queryParams, Lists.newArrayList()); + + for (JsonObject orderPosition : orderPositions) { + JsonObject order = pretixManager + .request( + String.format("/api/v1/organizers/%s/events/%s/orders/%s/", + pretixManager.getOrganizer(), pretixManager.getEvent(), + orderPosition.get("order").getAsString()), + HttpMethod.GET, queryParams) + .getAsJsonObject(); + + JsonObject member = new JsonObject(); + + member.addProperty("email", order.get("email").getAsString()); + JsonObject answers = new JsonObject(); + Instant active = null; + if (orderPosition.has("answers")) { + for (JsonElement answerElement : orderPosition.getAsJsonArray("answers")) { + JsonObject answer = answerElement.getAsJsonObject(); + String question_identifier = answer.get("question_identifier").getAsString(); + if (!answerHeaders.contains(question_identifier)) { + answerHeaders.add(question_identifier); + } + answers.addProperty(question_identifier, answer.get("answer").getAsString()); + if (membershipActiveQuestion == answer.get("question").getAsInt() + && answer.has("answer")) { + + Date date; + try { + date = pretixDateFormat.parse(answer.get("answer").getAsString()); + active = date.toInstant(); + } catch (ParseException e) { + logger.error("Error on parsing active date for '" + + orderPosition.get("order").getAsString() + "'!", e); + } + } + } + } + member.add("answers", answers); + if (includeInactive || active != null + && active.isAfter(InstantHelper.truncate(Instant.now(), ChronoUnit.YEARS))) { + members.add(member); + } + } + + try (CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT)) { + List csvHeaders = Lists.newArrayList(); + csvHeaders.add("email"); + csvHeaders.addAll(answerHeaders); + csvPrinter.printRecord(csvHeaders); + + for (JsonElement memberElement : members) { + JsonObject member = memberElement.getAsJsonObject(); + JsonObject answers = member.getAsJsonObject("answers"); + List records = Lists.newArrayList(); + records.add(member.get("email").getAsString()); + for (String answer : answerHeaders) { + if (answers.has(answer)) { + records.add(answers.get(answer).getAsString()); + } else { + records.add(null); + } + } + csvPrinter.printRecord(records); + } + } catch (IOException e) { + e.printStackTrace(); + } + return members; + } + + /** + * + */ + @Scheduled(cron = "${we.bstly.membership.cron:0 0 0 * * * }") + public void cron() { + generateMembershipIds(); + activeMemberships(); + createMembershipExtension(); + calculatePrice(); + } + + /** + * + * @param path + * @param queryParams + * @param result + * @return + */ + protected List fetchAllOrderPositions(String path, + MultiValueMap queryParams, List result) { + JsonObject orderPositions = pretixManager.request(path, HttpMethod.GET, queryParams) + .getAsJsonObject(); + + for (JsonElement element : orderPositions.getAsJsonArray("results")) { + JsonObject orderPosition = element.getAsJsonObject(); + result.add(orderPosition); + } + + if (orderPositions.has("next") && !orderPositions.get("next").isJsonNull()) { + return fetchAllOrderPositions(orderPositions.get("next").getAsString(), queryParams, + result); + } + + return result; + } + + /** + * + * @param orderPosition + * @param question + * @return + */ + protected JsonObject getAnswerObject(JsonObject orderPosition, int question) { + if (orderPosition.has("answers")) { + for (JsonElement answerElement : orderPosition.getAsJsonArray("answers")) { + JsonObject answer = answerElement.getAsJsonObject(); + if (question == answer.get("question").getAsInt()) { + return answer; + } + } + } + return null; + } + + /** + * + * @param orderPosition + * @param question + * @return + */ + protected int getNumberAnswer(JsonObject orderPosition, int question) { + JsonObject answer = getAnswerObject(orderPosition, question); + if (answer != null && answer.has("answer")) { + return answer.get("answer").getAsInt(); + } + return 0; + } + + /** + * + * @param orderPosition + * @param question + * @return + */ + protected Instant getDateAnswer(JsonObject orderPosition, int question) { + JsonObject answer = getAnswerObject(orderPosition, question); + if (answer != null && answer.has("answer")) { + try { + return pretixDateFormat.parse(answer.get("answer").getAsString()).toInstant(); + } catch (ParseException e) { + logger.error("Error on parsing active date for '" + + orderPosition.get("order").getAsString() + "' in question '" + question + + "'!", e); + } + } + return null; + } + +} diff --git a/membership/src/main/java/de/bstly/we/membership/controller/MembershipController.java b/membership/src/main/java/de/bstly/we/membership/controller/MembershipController.java new file mode 100644 index 0000000..40e1b03 --- /dev/null +++ b/membership/src/main/java/de/bstly/we/membership/controller/MembershipController.java @@ -0,0 +1,129 @@ +/** + * + */ +package de.bstly.we.membership.controller; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.membership.businesslogic.MembershipManager; + +/** + * @author _bastler@bstly.de + * + */ + +@RestController +@RequestMapping("/membership") +public class MembershipController extends BaseController { + + @Autowired + private MembershipManager membershipManager; + + @Value("${we.bstly.pretix.webhook.key:}") + private String WEBHOOK_KEY; + + /** + * + * @param response + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/cron") + public void cron(HttpServletResponse response) { + membershipManager.cron(); + } + + /** + * + * @param response + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/ids") + public void generateMembershipIds(HttpServletResponse response) { + membershipManager.generateMembershipIds(); + } + + /** + * + * @param response + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/active") + public void activeMemberships(HttpServletResponse response) { + membershipManager.activeMemberships(); + } + + /** + * + * @param response + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/extension") + public void createMembershipExtension(HttpServletResponse response) { + membershipManager.createMembershipExtension(); + } + + /** + * + * @param response + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/calculateprice") + public void calculatePrice(HttpServletResponse response) { + membershipManager.calculatePrice(); + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/export") + public void getMemberMailList( + @RequestParam("includeInactive") Optional includeInactive, + HttpServletResponse response) throws IOException { + response.setContentType("text/csv"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + membershipManager.membershipCsvExport(response.getWriter(), + includeInactive.isPresent() && includeInactive.get()); + } + + /** + * + * @param webhookKey + * @param payload + */ + @PostMapping("/pretix/webhook") + public void webhook(@RequestParam("key") String webhookKey, @RequestBody String payload) { + if (!StringUtils.hasText(webhookKey) || !webhookKey.equals(WEBHOOK_KEY)) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + JsonObject event = JsonParser.parseString(payload).getAsJsonObject(); + + if (event.has("action") + && "pretix.event.order.paid".equals(event.get("action").getAsString())) { + membershipManager.generateMembershipIds(); + membershipManager.activeMemberships(); + } else { + throw new EntityResponseStatusException(HttpStatus.NO_CONTENT); + } + + } +} diff --git a/minetest/pom.xml b/minetest/pom.xml new file mode 100755 index 0000000..ccbf1f5 --- /dev/null +++ b/minetest/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + de.bstly.we + webstly-main + ${revision} + + + minetest + webstly-minetest + + + + de.bstly.we + webstly-core + ${revision} + + + diff --git a/minetest/src/main/java/de/bstly/we/minetest/businesslogic/MinetestAccountManager.java b/minetest/src/main/java/de/bstly/we/minetest/businesslogic/MinetestAccountManager.java new file mode 100644 index 0000000..ccc4259 --- /dev/null +++ b/minetest/src/main/java/de/bstly/we/minetest/businesslogic/MinetestAccountManager.java @@ -0,0 +1,173 @@ +/** + * + */ +package de.bstly.we.minetest.businesslogic; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import com.google.common.collect.Lists; + +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.businesslogic.UserDataProvider; +import de.bstly.we.minetest.model.MinetestAccount; +import de.bstly.we.minetest.model.QMinetestAccount; +import de.bstly.we.minetest.repository.MinetestAccountRepository; +import de.bstly.we.model.Quota; +import de.bstly.we.model.UserData; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class MinetestAccountManager implements UserDataProvider { + + @Autowired + private QuotaManager quotaManager; + @Autowired + private MinetestAccountRepository minetestAccountRepository; + private QMinetestAccount qMinetestAccount = QMinetestAccount.minetestAccount; + + /** + * + * @param name + * @return + */ + public MinetestAccount get(String name) { + return minetestAccountRepository.findById(name).orElse(null); + } + + /** + * + * @param userId + * @return + */ + public List getAllByOwner(Long userId) { + return Lists + .newArrayList(minetestAccountRepository.findAll(qMinetestAccount.owner.eq(userId))); + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + return minetestAccountRepository.findAll(PageRequest.of(page, size, + descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending())); + } + + /** + * + * @param owner + * @param name + * @param quota + * @return + */ + public MinetestAccount create(Long owner, String name, boolean quota) { + MinetestAccount minetestAccount = new MinetestAccount(); + minetestAccount.setOwner(owner); + + Assert.isTrue(!minetestAccountRepository.existsById(name), "Given name already exists!"); + + minetestAccount.setName(name); + + minetestAccount = minetestAccountRepository.save(minetestAccount); + + if (quota) { + Quota minetestAccountsQuota = quotaManager.get(minetestAccount.getOwner(), + MinetestQuotas.MINETEST_ACCOUNTS); + if (minetestAccountsQuota != null) { + minetestAccountsQuota.setValue(minetestAccountsQuota.getValue() - 1); + quotaManager.update(minetestAccountsQuota); + } + } + + return minetestAccount; + } + + /** + * + * @param minetestAccount + * @return + */ + public MinetestAccount save(MinetestAccount minetestAccount) { + return minetestAccountRepository.save(minetestAccount); + } + + /** + * + * @param minetestAccount + * @param quota + */ + public void delete(MinetestAccount minetestAccount, boolean quota) { + if (quota) { + Quota minetestAccountsQuota = quotaManager.get(minetestAccount.getOwner(), + MinetestQuotas.MINETEST_ACCOUNTS); + if (minetestAccountsQuota == null) { + minetestAccountsQuota = quotaManager.create(minetestAccount.getOwner(), + MinetestQuotas.MINETEST_ACCOUNTS, 0, "#", true); + } + + minetestAccountsQuota.setValue(minetestAccountsQuota.getValue() + 1); + quotaManager.update(minetestAccountsQuota); + } + + minetestAccountRepository.delete(minetestAccount); + } + + /** + * + * @param owner + * @param quota + */ + public void deleteAll(Long owner, boolean quota) { + List minetestAccounts = Lists + .newArrayList(minetestAccountRepository.findAll(qMinetestAccount.owner.eq(owner))); + + for (MinetestAccount minetestAccount : minetestAccounts) { + delete(minetestAccount, quota); + } + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getId() + */ + @Override + public String getId() { + return "minetest-accounts"; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getUserData(java.lang.Long) + */ + @Override + public List getUserData(Long userId) { + List result = Lists.newArrayList(); + for (MinetestAccount minetestAccount : getAllByOwner(userId)) { + result.add(minetestAccount); + } + return result; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#purgeUserData(java.lang.Long) + */ + @Override + public void purgeUserData(Long userId) { + for (MinetestAccount minetestAccount : getAllByOwner(userId)) { + minetestAccountRepository.delete(minetestAccount); + } + } + +} diff --git a/minetest/src/main/java/de/bstly/we/minetest/businesslogic/MinetestPermissions.java b/minetest/src/main/java/de/bstly/we/minetest/businesslogic/MinetestPermissions.java new file mode 100644 index 0000000..6274b26 --- /dev/null +++ b/minetest/src/main/java/de/bstly/we/minetest/businesslogic/MinetestPermissions.java @@ -0,0 +1,13 @@ +/** + * + */ +package de.bstly.we.minetest.businesslogic; + +/** + * @author _bastler@bstly.de + * + */ +public interface MinetestPermissions { + + public static final String MINETEST = "minetest"; +} diff --git a/minetest/src/main/java/de/bstly/we/minetest/businesslogic/MinetestQuotas.java b/minetest/src/main/java/de/bstly/we/minetest/businesslogic/MinetestQuotas.java new file mode 100644 index 0000000..d81da20 --- /dev/null +++ b/minetest/src/main/java/de/bstly/we/minetest/businesslogic/MinetestQuotas.java @@ -0,0 +1,13 @@ +/** + * + */ +package de.bstly.we.minetest.businesslogic; + +/** + * @author _bastler@bstly.de + * + */ +public interface MinetestQuotas { + + public static final String MINETEST_ACCOUNTS = "minetest_accounts"; +} diff --git a/minetest/src/main/java/de/bstly/we/minetest/controller/MinetestAccountController.java b/minetest/src/main/java/de/bstly/we/minetest/controller/MinetestAccountController.java new file mode 100644 index 0000000..17a303a --- /dev/null +++ b/minetest/src/main/java/de/bstly/we/minetest/controller/MinetestAccountController.java @@ -0,0 +1,113 @@ +/** + * + */ +package de.bstly.we.minetest.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.minetest.businesslogic.MinetestAccountManager; +import de.bstly.we.minetest.businesslogic.MinetestPermissions; +import de.bstly.we.minetest.businesslogic.MinetestQuotas; +import de.bstly.we.minetest.controller.validation.MinetestAccountValidator; +import de.bstly.we.minetest.model.MinetestAccount; +import de.bstly.we.model.Quota; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/minetest/accounts") +public class MinetestAccountController extends BaseController { + + @Autowired + private MinetestAccountManager minetestAccountManager; + @Autowired + private PermissionManager permissionManager; + @Autowired + private QuotaManager quotaManager; + @Autowired + private MinetestAccountValidator minetestAccountModelValidator; + + /** + * + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping + public List getMinetestAccounts() { + if (!permissionManager.hasPermission(getCurrentUserId(), MinetestPermissions.MINETEST)) { + minetestAccountManager.deleteAll(getCurrentUserId(), true); + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + return minetestAccountManager.getAllByOwner(getCurrentUserId()); + } + + /** + * + * @param minetestAccountModel + * @return + */ + @PreAuthorize("isAuthenticated()") + @PostMapping + public MinetestAccount createMinetestAccount(@RequestBody MinetestAccount minetestAccount) { + if (!permissionManager.hasPermission(getCurrentUserId(), MinetestPermissions.MINETEST) + || !permissionManager.isFullUser(getCurrentUserId())) { + minetestAccountManager.deleteAll(getCurrentUserId(), true); + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + Quota minetestAccountsQuota = quotaManager.get(getCurrentUserId(), + MinetestQuotas.MINETEST_ACCOUNTS); + if (minetestAccountsQuota == null || minetestAccountsQuota.getValue() < 1) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + Errors errors = new RequestBodyErrors(minetestAccount); + + minetestAccountModelValidator.validate(minetestAccount, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + return minetestAccountManager.create(getCurrentUserId(), minetestAccount.getName(), true); + } + + /** + * + * @param id + */ + @PreAuthorize("isAuthenticated()") + @DeleteMapping("/{name}") + public void deleteMinetestAccount(@PathVariable("name") String name) { + if (!permissionManager.hasPermission(getCurrentUserId(), MinetestPermissions.MINETEST)) { + minetestAccountManager.deleteAll(getCurrentUserId(), true); + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + MinetestAccount minetestAccount = minetestAccountManager.get(name); + if (minetestAccount == null || !minetestAccount.getOwner().equals(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + minetestAccountManager.delete(minetestAccount, true); + } +} diff --git a/minetest/src/main/java/de/bstly/we/minetest/controller/MinetestAccountManagementController.java b/minetest/src/main/java/de/bstly/we/minetest/controller/MinetestAccountManagementController.java new file mode 100644 index 0000000..8335ae8 --- /dev/null +++ b/minetest/src/main/java/de/bstly/we/minetest/controller/MinetestAccountManagementController.java @@ -0,0 +1,104 @@ +/** + * + */ +package de.bstly.we.minetest.controller; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.minetest.businesslogic.MinetestAccountManager; +import de.bstly.we.minetest.controller.validation.MinetestAccountValidator; +import de.bstly.we.minetest.model.MinetestAccount; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/minetest/accounts/manage") +public class MinetestAccountManagementController extends BaseController { + + @Autowired + private MinetestAccountManager minetestAccountManager; + @Autowired + private MinetestAccountValidator minetestAccountModelValidator; + + /** + * + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getMinetestAccounts( + @RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter) { + return minetestAccountManager.get(pageParameter.orElse(0), sizeParameter.orElse(10), "name", + true); + } + + /** + * + * @param minetestAccount + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public MinetestAccount createOrUpdateMinetestAccount( + @RequestBody MinetestAccount minetestAccount) { + + Errors errors = new RequestBodyErrors(minetestAccount); + + minetestAccountModelValidator.validateMinetestName(minetestAccount, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + return minetestAccountManager.save(minetestAccount); + } + + /** + * + * @param id + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/{name}") + public void deleteMinetestAccount(@PathVariable("name") String name, + @RequestParam("quota") Optional quota) { + + MinetestAccount minetestAccount = minetestAccountManager.get(name); + if (minetestAccount == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + minetestAccountManager.delete(minetestAccount, + quota.isPresent() && quota.get().booleanValue()); + } + + /** + * + * @param owner + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/all/{owner}") + public void deleteAll(@PathVariable("owner") Long owner, + @RequestParam("quota") Optional quota) { + minetestAccountManager.deleteAll(owner, quota.isPresent() && quota.get().booleanValue()); + } + +} diff --git a/minetest/src/main/java/de/bstly/we/minetest/controller/validation/MinetestAccountValidator.java b/minetest/src/main/java/de/bstly/we/minetest/controller/validation/MinetestAccountValidator.java new file mode 100644 index 0000000..4d0c63d --- /dev/null +++ b/minetest/src/main/java/de/bstly/we/minetest/controller/validation/MinetestAccountValidator.java @@ -0,0 +1,94 @@ +/** + * + */ +package de.bstly.we.minetest.controller.validation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import de.bstly.we.businesslogic.SystemPropertyManager; +import de.bstly.we.controller.validation.UserModelValidator; +import de.bstly.we.minetest.businesslogic.MinetestAccountManager; +import de.bstly.we.minetest.model.MinetestAccount; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class MinetestAccountValidator implements Validator { + + @Autowired + private MinetestAccountManager minetestAccountManager; + @Autowired + private SystemPropertyManager systemPropertyManager; + protected static final String NAME_REGEX = "^([-_a-zA-Z0-9]+)$"; + + /* + * @see org.springframework.validation.Validator#supports(java.lang.Class) + */ + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(MinetestAccount.class); + } + + /* + * @see org.springframework.validation.Validator#validate(java.lang.Object, + * org.springframework.validation.Errors) + */ + @Override + public void validate(Object target, Errors errors) { + validateMinetestName(target, errors); + if (!errors.hasErrors()) { + validateReservedNames(target, errors); + } + } + + /** + * + * @param target + * @param errors + */ + public void validateMinetestName(Object target, Errors errors) { + MinetestAccount minetestAccount = (MinetestAccount) target; + if (!StringUtils.hasText(minetestAccount.getName())) { + errors.rejectValue("name", "REQUIRED"); + return; + } + + MinetestAccount existing = minetestAccountManager.get(minetestAccount.getName()); + if (existing != null && !existing.getOwner().equals(minetestAccount.getOwner())) { + errors.rejectValue("name", "NOT_VALID"); + return; + } + + if (!minetestAccount.getName().matches(NAME_REGEX)) { + errors.rejectValue("name", "NOT_VALID"); + return; + } + } + + /** + * + * @param target + * @param errors + */ + public void validateReservedNames(Object target, Errors errors) { + MinetestAccount minetestAccount = (MinetestAccount) target; + if (StringUtils.hasText(minetestAccount.getName())) { + for (String systemUsername : systemPropertyManager + .get(UserModelValidator.RESERVED_USERNAMES, "").split(",")) { + if (StringUtils.hasText(systemUsername) && (minetestAccount.getName().toLowerCase() + .equals(systemUsername) + || minetestAccount.getName().toLowerCase().matches(systemUsername))) { + errors.rejectValue("name", "NOT_VALID"); + break; + } + } + } + } + +} diff --git a/minetest/src/main/java/de/bstly/we/minetest/model/MinetestAccount.java b/minetest/src/main/java/de/bstly/we/minetest/model/MinetestAccount.java new file mode 100644 index 0000000..2f9c0a6 --- /dev/null +++ b/minetest/src/main/java/de/bstly/we/minetest/model/MinetestAccount.java @@ -0,0 +1,73 @@ +/** + * + */ +package de.bstly.we.minetest.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; + +import de.bstly.we.model.UserData; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "minetest_accounts") +public class MinetestAccount implements UserData { + + @Id + @Column(name = "name") + private String name; + @Column(name = "owner") + private Long owner; + @Lob + @Column(name = "skin") + private String skin; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the owner + */ + public Long getOwner() { + return owner; + } + + /** + * @param owner the owner to set + */ + public void setOwner(Long owner) { + this.owner = owner; + } + + /** + * @return the skin + */ + public String getSkin() { + return skin; + } + + /** + * @param skin the skin to set + */ + public void setSkin(String skin) { + this.skin = skin; + } + +} diff --git a/minetest/src/main/java/de/bstly/we/minetest/repository/MinetestAccountRepository.java b/minetest/src/main/java/de/bstly/we/minetest/repository/MinetestAccountRepository.java new file mode 100755 index 0000000..7bbddbf --- /dev/null +++ b/minetest/src/main/java/de/bstly/we/minetest/repository/MinetestAccountRepository.java @@ -0,0 +1,20 @@ +/** + * + */ +package de.bstly.we.minetest.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.minetest.model.MinetestAccount; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface MinetestAccountRepository + extends JpaRepository, QuerydslPredicateExecutor { +} diff --git a/oidc/pom.xml b/oidc/pom.xml new file mode 100755 index 0000000..15a3549 --- /dev/null +++ b/oidc/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + de.bstly.we + webstly-main + ${revision} + + + oidc + webstly-oidc + + + + de.bstly.we + webstly-core + ${revision} + + + + com.nimbusds + nimbus-jose-jwt + + + diff --git a/oidc/src/main/java/de/bstly/we/oidc/businesslogic/OidcAuthorizationCodeManager.java b/oidc/src/main/java/de/bstly/we/oidc/businesslogic/OidcAuthorizationCodeManager.java new file mode 100644 index 0000000..75cb1d6 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/businesslogic/OidcAuthorizationCodeManager.java @@ -0,0 +1,61 @@ +/** + * + */ +package de.bstly.we.oidc.businesslogic; + +import java.net.URI; +import java.util.Map; +import java.util.Set; + +import org.springframework.stereotype.Service; + +import com.google.common.collect.Maps; + +import de.bstly.we.oidc.model.OidcAuthorizationCode; + +/** + * + * @author _bastler@bstly.de + * + */ +@Service +public class OidcAuthorizationCodeManager { + + /** + * hold codes in memory + */ + private final Map oidcAuthorizationCodes = Maps.newHashMap(); + + /** + * + * @param clientId + * @param redirectUri + * @param scopes + * @param subject + * @return + */ + public OidcAuthorizationCode create(String clientId, URI redirectUri, Set scopes, Long userId, + String nonce) { + OidcAuthorizationCode oidcAuthorizationCode = new OidcAuthorizationCode(clientId, redirectUri, scopes, userId, + nonce); + oidcAuthorizationCodes.put(oidcAuthorizationCode.getCode(), oidcAuthorizationCode); + return oidcAuthorizationCode; + } + + /** + * + * @param code + * @return + */ + public OidcAuthorizationCode getByCode(String code) { + return oidcAuthorizationCodes.get(code); + } + + /** + * + * @param code + */ + public void removeByCode(String code) { + oidcAuthorizationCodes.remove(code); + } +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/businesslogic/OidcClientManager.java b/oidc/src/main/java/de/bstly/we/oidc/businesslogic/OidcClientManager.java new file mode 100644 index 0000000..d8b47f1 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/businesslogic/OidcClientManager.java @@ -0,0 +1,179 @@ +/** + * + */ +package de.bstly.we.oidc.businesslogic; + +import java.util.Set; + +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import com.google.common.collect.Sets; + +import de.bstly.we.oidc.model.OidcAuthorizationGrantType; +import de.bstly.we.oidc.model.OidcClient; +import de.bstly.we.oidc.model.OidcClientAuthenticationMethod; +import de.bstly.we.oidc.model.QOidcClient; +import de.bstly.we.oidc.repository.OidcClientRepository; + +/** + * + * @author _bastler@bstly.de + * + */ +@Service +public class OidcClientManager { + + public static final int OIDC_CLIENT_CLIENT_ID_LENGTH = 24; + public static final int OIDC_CLIENT_CLIENT_SECRET_LENGTH = 64; + public static final long OIDC_CLIENT_TOKEN_LIFETIME = 120; + + @Autowired + private OidcClientRepository oidcClientRepository; + private QOidcClient qOidcClient = QOidcClient.oidcClient; + + /** + * + * @param clientName + * @param redirectUris + * @return + */ + public OidcClient createClient(String clientName, Set redirectUris) { + return createClient(clientName, + Sets.newHashSet(OidcClientAuthenticationMethod.basic, + OidcClientAuthenticationMethod.basic), + Sets.newHashSet(OidcAuthorizationGrantType.authorization_code), redirectUris, + Sets.newHashSet("openid"), null, false); + } + + /** + * + * @param clientName + * @param clientAuthenticationMethods + * @param authorizationGrantTypes + * @param redirectUris + * @param scopes + * @return + */ + public OidcClient createClient(String clientName, + Set clientAuthenticationMethods, + Set authorizationGrantTypes, Set redirectUris, + Set scopes, String loginUrl, boolean alwaysPermitted) { + + OidcClient oidcClient = new OidcClient(); + + oidcClient.setClientName(clientName); + oidcClient.setRedirectUris(redirectUris); + oidcClient.setClientAuthenticationMethods(clientAuthenticationMethods); + oidcClient.setAuthorizationGrantTypes(authorizationGrantTypes); + oidcClient.setScopes(scopes); + oidcClient.setLoginUrl(loginUrl); + oidcClient.setAlwaysPermitted(alwaysPermitted); + oidcClient.setTokenLifetime(OIDC_CLIENT_TOKEN_LIFETIME); + + String clientId = RandomStringUtils.random(OIDC_CLIENT_CLIENT_ID_LENGTH, true, true); + + while (oidcClientRepository.findOne(qOidcClient.clientId.eq(clientId)).isPresent()) { + clientId = RandomStringUtils.random(OIDC_CLIENT_CLIENT_ID_LENGTH, true, true); + } + + oidcClient.setClientId(clientId); + oidcClient.setClientSecret( + RandomStringUtils.random(OIDC_CLIENT_CLIENT_SECRET_LENGTH, true, true)); + + return oidcClientRepository.save(oidcClient); + } + + /** + * + * @param oidcClient + * @return + */ + public OidcClient updateClient(OidcClient oidcClient) { + return oidcClientRepository.save(oidcClient); + } + + /** + * + * @param clientName + * @return + */ + public OidcClient createNewSecretByClientName(String clientName) { + OidcClient oidcClient = getByClientName(clientName); + Assert.notNull(oidcClient, "No client found for name '" + clientName + "'"); + oidcClient.setClientSecret( + RandomStringUtils.random(OIDC_CLIENT_CLIENT_SECRET_LENGTH, true, true)); + + return oidcClientRepository.save(oidcClient); + } + + /** + * + * @param clientId + * @return + */ + public OidcClient getByClientId(String clientId) { + return oidcClientRepository.findOne(qOidcClient.clientId.eq(clientId)).orElse(null); + } + + /** + * + * @param clientName + * @return + */ + public OidcClient getByClientName(String clientName) { + return oidcClientRepository.findOne(qOidcClient.clientName.eq(clientName)).orElse(null); + } + + /** + * + * @param clientId + * @param clientSecret + * @return + */ + public OidcClient getByClientIdAndSecret(String clientId, String clientSecret) { + return oidcClientRepository.findOne( + qOidcClient.clientId.eq(clientId).and(qOidcClient.clientSecret.eq(clientSecret))) + .orElse(null); + } + + /** + * + * @param clientId + * @return + */ + public void deleteByClientId(String clientId) { + OidcClient oidcClient = getByClientId(clientId); + if (oidcClient != null) { + oidcClientRepository.delete(oidcClient); + } + } + + /** + * + * @param name + */ + public void deleteByClientName(String clientName) { + OidcClient oidcClient = getByClientName(clientName); + Assert.notNull(oidcClient, "No client found for clientName '" + clientName + "'"); + oidcClientRepository.delete(oidcClient); + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + Sort sort = descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending(); + return oidcClientRepository.findAll(PageRequest.of(page, size, sort)); + } +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/businesslogic/OidcTokenManager.java b/oidc/src/main/java/de/bstly/we/oidc/businesslogic/OidcTokenManager.java new file mode 100644 index 0000000..6ba3c47 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/businesslogic/OidcTokenManager.java @@ -0,0 +1,261 @@ +/** + * + */ +package de.bstly.we.oidc.businesslogic; + +import java.text.ParseException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.Map; +import java.util.Set; + +import javax.annotation.PostConstruct; + +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.beust.jcommander.internal.Maps; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; +import com.nimbusds.jwt.JWTClaimsSet.Builder; +import com.nimbusds.jwt.SignedJWT; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.businesslogic.SystemPropertyManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.businesslogic.UserProfileFieldManager; +import de.bstly.we.businesslogic.UserProfileFields; +import de.bstly.we.model.Permission; +import de.bstly.we.model.Quota; +import de.bstly.we.model.User; +import de.bstly.we.model.UserProfileField; +import de.bstly.we.oidc.model.OidcClient; +import de.bstly.we.oidc.model.OidcToken; +import de.bstly.we.oidc.model.QOidcToken; +import de.bstly.we.oidc.repository.OidcTokenRepository; + +/** + * + * @author _bastler@bstly.de + * + */ +@Service +public class OidcTokenManager { + + public static final int ACCESS_TOKEN_LENGTH = 64; + public static final String OIDC_JWK_PUBLIC_KEY = "oidc-jwk-public-key"; + public static final String BEARER_TOKEN_TYPE = "Bearer"; + + private RSAKey publicKey; + private JWKSet jwkSet; + private JWSSigner signer; + private JWSVerifier verifier; + + @Autowired + private OidcTokenRepository tokenRepository; + @Autowired + private UserManager userManager; + @Autowired + private UserProfileFieldManager userProfileFieldManager; + @Autowired + private PermissionManager permissionManager; + @Autowired + private QuotaManager quotaManager; + @Autowired + private SystemPropertyManager systemPropertyManager; + private QOidcToken qOidcToken = QOidcToken.oidcToken; + + @PostConstruct + public void initOidcTokenManager() throws JOSEException { + RSAKey rsaJWK = null; + if (systemPropertyManager.has(OIDC_JWK_PUBLIC_KEY)) { + try { + rsaJWK = RSAKey.parse(systemPropertyManager.get(OIDC_JWK_PUBLIC_KEY)); + } catch (ParseException e) { + + } + } else { + rsaJWK = new RSAKeyGenerator(2048).keyID("1").generate(); + systemPropertyManager.add(OIDC_JWK_PUBLIC_KEY, rsaJWK.toJSONString()); + } + + this.publicKey = rsaJWK.toPublicJWK(); + this.signer = new RSASSASigner(rsaJWK); + this.jwkSet = new JWKSet(this.publicKey); + this.verifier = new RSASSAVerifier(this.publicKey); + } + + /** + * + * @param client + * @param target + * @return + */ + public OidcToken createToken(OidcClient client, Long userId) { + OidcToken token = new OidcToken(); + token.setUserId(userId); + token.setAccessToken(RandomStringUtils.random(ACCESS_TOKEN_LENGTH, true, true)); + token.setExpiresIn(client.getTokenLifetime()); + return tokenRepository.save(token); + } + + /** + * + * @param client + * @param userId + * @param nonce + * @param scopes + * @param issuer + * @return + * @throws JOSEException + */ + public OidcToken createTokenWithIdToken(OidcClient client, Long userId, String nonce, + Set scopes, String issuer) throws JOSEException { + OidcToken token = new OidcToken(); + token.setClient(client.getId()); + + User user = userManager.get(userId); + + Assert.notNull(user, "User does not exist!"); + + token.setUserId(user.getId()); + token.setAccessToken(RandomStringUtils.random(ACCESS_TOKEN_LENGTH, true, true)); + token.setExpiresIn(client.getTokenLifetime()); + + Builder claimsSetBuilder = createUserClaims(user); + + claimsSetBuilder.issuer(issuer); + claimsSetBuilder.audience(client.getClientId()); + claimsSetBuilder.expirationTime( + Date.from(Instant.now().plus(client.getTokenLifetime(), ChronoUnit.SECONDS) + .atZone(ZoneId.systemDefault()).toInstant())); + claimsSetBuilder.issueTime(new Date()); + + if (StringUtils.hasText(nonce)) { + claimsSetBuilder.claim("nonce", nonce); + } + + JWSHeader.Builder headerBuilder = new JWSHeader.Builder(JWSAlgorithm.RS256); + headerBuilder.keyID(getPublicKey().getKeyID()); + + SignedJWT jwt = new SignedJWT(headerBuilder.build(), claimsSetBuilder.build()); + jwt.sign(getSigner()); + + token.setIdToken(jwt.serialize()); + + return tokenRepository.save(token); + } + + /** + * + * @param user + * @return + */ + public Builder createUserClaims(User user) { + + Builder claimsSetBuilder = new Builder(); + claimsSetBuilder.subject(String.valueOf(user.getId())); + claimsSetBuilder.claim("name", user.getUsername()); + claimsSetBuilder.claim("preferred_username", user.getUsername()); + + UserProfileField emailProfileField = userProfileFieldManager.get(user.getId(), + UserProfileFields.PROFILE_FIELD_EMAIL); + UserProfileField emailPrimaryProfileField = userProfileFieldManager.get(user.getId(), + UserProfileFields.PROFILE_FIELD_EMAIL_PRIMARY); + if (emailProfileField != null && emailPrimaryProfileField != null + && StringUtils.hasText(emailProfileField.getValue()) + && Boolean.getBoolean(emailPrimaryProfileField.getValue())) { + claimsSetBuilder.claim("email", emailProfileField.getValue()); + } else { + claimsSetBuilder.claim("email_verified", true); + claimsSetBuilder.claim("email", userManager.getBstlyEmail(user.getUsername())); + } + + UserProfileField localeProfileField = userProfileFieldManager.get(user.getId(), + UserProfileFields.PROFILE_FIELD_LOCALE); + + if (localeProfileField != null) { + if ("de-informal".equals(localeProfileField.getValue())) { + claimsSetBuilder.claim(UserProfileFields.PROFILE_FIELD_LOCALE, "de"); + } else if (StringUtils.hasText(localeProfileField.getValue())) { + claimsSetBuilder.claim(UserProfileFields.PROFILE_FIELD_LOCALE, + localeProfileField.getValue()); + } + } + + Map permissions = Maps.newHashMap(); + for (Permission permission : permissionManager.getNotExpiresByTarget(user.getId())) { + permissions.put(permission.getName(), permission.getExpires().toString()); + } + + if (!permissions.isEmpty()) { + claimsSetBuilder.claim("permissions", permissions); + } + + Map quotas = Maps.newHashMap(); + for (Quota quota : quotaManager.getNotExpiresByTarget(user.getId())) { + quotas.put(quota.getName(), String.valueOf(quota.getValue()) + quota.getUnit()); + } + + if (!quotas.isEmpty()) { + claimsSetBuilder.claim("quotas", quotas); + } + + return claimsSetBuilder; + } + + /** + * + * @param accessToken + * @return + */ + public OidcToken getByAccessToken(String accessToken) { + return tokenRepository.findOne(qOidcToken.accessToken.eq(accessToken)).orElse(null); + } + + /** + * + * @return + */ + public JWSSigner getSigner() { + return signer; + } + + /** + * + * @return + */ + public JWSVerifier getVerifier() { + return verifier; + } + + /** + * + * @return + */ + public RSAKey getPublicKey() { + return publicKey; + } + + /** + * + * @return + */ + public JWKSet getJwkSet() { + return jwkSet; + } +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/controller/OIDCClientController.java b/oidc/src/main/java/de/bstly/we/oidc/controller/OIDCClientController.java new file mode 100755 index 0000000..4f6e4f6 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/controller/OIDCClientController.java @@ -0,0 +1,176 @@ +/** + * + */ +package de.bstly.we.oidc.controller; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.google.common.collect.Sets; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.oidc.businesslogic.OidcClientManager; +import de.bstly.we.oidc.controller.model.OidcClientModel; +import de.bstly.we.oidc.model.OidcAuthorizationGrantType; +import de.bstly.we.oidc.model.OidcClient; +import de.bstly.we.oidc.model.OidcClientAuthenticationMethod; +import de.bstly.we.oidc.repository.OidcClientRepository; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/oidc/clients") +public class OIDCClientController extends BaseController { + + @Autowired + private OidcClientManager registeredClientService; + @Autowired + private OidcClientRepository registeredClientRepository; + + /** + * + * @param pageParameter + * @param sizeParameter + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getClients(@RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter) { + return registeredClientService.get(pageParameter.orElse(0), sizeParameter.orElse(10), + "clientName", true); + } + + /** + * + * @param name + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/{name}") + public OidcClient get(@PathVariable("name") String name) { + OidcClient client = registeredClientService.getByClientName(name); + if (client == null) { + throw new EntityResponseStatusException(HttpStatus.NO_CONTENT); + } + + return client; + } + + /** + * + * @param clientId + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/id/{clientId}") + public OidcClient getByClientId(@PathVariable("clientId") String clientId) { + OidcClient client = registeredClientService.getByClientId(clientId); + if (client == null) { + throw new EntityResponseStatusException(HttpStatus.NO_CONTENT); + } + + return client; + } + + /** + * + * @param oidcClientModel + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public OidcClient create(@RequestBody OidcClientModel oidcClientModel) { + if (registeredClientService.getByClientName(oidcClientModel.getName()) != null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + if (oidcClientModel.getClientAuthenticationMethods() == null + || oidcClientModel.getClientAuthenticationMethods().isEmpty()) { + oidcClientModel.setClientAuthenticationMethods(Sets.newHashSet( + OidcClientAuthenticationMethod.basic, OidcClientAuthenticationMethod.post)); + } + + if (oidcClientModel.getAuthorizationGrantTypes() == null + || oidcClientModel.getAuthorizationGrantTypes().isEmpty()) { + oidcClientModel.setAuthorizationGrantTypes( + Sets.newHashSet(OidcAuthorizationGrantType.authorization_code)); + } + + return registeredClientService.createClient(oidcClientModel.getName(), + oidcClientModel.getClientAuthenticationMethods(), + oidcClientModel.getAuthorizationGrantTypes(), + oidcClientModel.getRegisteredRedirectUris(), oidcClientModel.getScopes(), + oidcClientModel.getLoginUrl(), oidcClientModel.isAlwaysPermitted()); + } + + /** + * + * @param oAuth2ClientModel + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PatchMapping + public OidcClient update(@RequestBody OidcClient client) { + + OidcClient oldClient = registeredClientRepository.findById(client.getId()).orElse(null); + + if (oldClient == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + OidcClient otherClient = registeredClientService.getByClientName(client.getClientName()); + + if (otherClient != null && !otherClient.getId().equals(client.getId())) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + return registeredClientService.updateClient(client); + } + + /** + * + * @param oAuth2ClientModel + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/{name}") + public void deleteClient(@PathVariable("name") String name) { + if (registeredClientService.getByClientName(name) == null) { + throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED); + } + registeredClientService.deleteByClientName(name); + } + + /** + * + * @param oAuth2ClientModel + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/{name}/secret") + public OidcClient createNewSecret(@PathVariable("name") String name) { + if (registeredClientService.getByClientName(name) == null) { + throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED); + } + return registeredClientService.createNewSecretByClientName(name); + } + +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/controller/OidcAuthorizationController.java b/oidc/src/main/java/de/bstly/we/oidc/controller/OidcAuthorizationController.java new file mode 100644 index 0000000..cfb32fc --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/controller/OidcAuthorizationController.java @@ -0,0 +1,284 @@ +/** + * + */ +package de.bstly.we.oidc.controller; + +import java.io.IOException; +import java.net.URI; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; +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 com.google.common.collect.Sets; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.Permissions; +import de.bstly.we.oidc.businesslogic.OidcAuthorizationCodeManager; +import de.bstly.we.oidc.businesslogic.OidcClientManager; +import de.bstly.we.oidc.model.OidcAuthorizationCode; +import de.bstly.we.oidc.model.OidcAuthorizationErrorCode; +import de.bstly.we.oidc.model.OidcAuthorizationGrantType; +import de.bstly.we.oidc.model.OidcAuthorizationResponseType; +import de.bstly.we.oidc.model.OidcClient; +import de.bstly.we.security.model.LocalUserDetails; + +/** + * + * @author _bastler@bstly.de + * + */ +@RequestMapping("/oidc/authorize") +@RestController +public class OidcAuthorizationController { + + private Logger logger = LoggerFactory.getLogger(OidcAuthorizationController.class); + + @Autowired + private PermissionManager permissionManager; + @Autowired + private OidcClientManager oidcClientManager; + @Autowired + private OidcAuthorizationCodeManager oidcAuthorizationCodeManager; + + /** + * + * @param scope + * @param responseType + * @param clientId + * @param redirectUri + * @param state + * @param request + * @param response + * @throws IOException + */ + @PreAuthorize("isAuthenticated()") + @GetMapping + void authorizationRequest( + // for OIDC scope must contain "openid" + @RequestParam(name = "scope") String scope, + // for now only "code" is available + @RequestParam(name = "response_type") String responseType, + // client id + @RequestParam(name = "client_id") String clientId, + // redirect url + @RequestParam(name = "redirect_uri") URI redirectUri, + // optional state + @RequestParam(name = "state", required = false) String state, + // optional state + @RequestParam(name = "nonce", required = false) String nonce, + // authentication details + @AuthenticationPrincipal LocalUserDetails principal, + // the request + HttpServletRequest request, + // the response + HttpServletResponse response) throws IOException { + + if (!StringUtils.hasText(clientId)) { + logger.debug("missing client_id"); + throw new InvalidAuthorizationRequestError(redirectUri, + OidcAuthorizationErrorCode.INVALID_REQUEST, "missing client_id", state); + } + + OidcClient client = oidcClientManager.getByClientId(clientId); + + if (client == null) { + logger.debug("invalid client_id: " + clientId); + throw new InvalidAuthorizationRequestError(redirectUri, + OidcAuthorizationErrorCode.INVALID_REQUEST, "invalid client_id", state); + + } + + if (!client.getRedirectUris().contains(redirectUri.toString())) { + logger.debug("invalid redirect_uri: " + redirectUri + " allowed: " + + client.getRedirectUris()); + throw new InvalidAuthorizationRequestError(redirectUri, + OidcAuthorizationErrorCode.INVALID_REQUEST, "invalid redirect_uri", state); + } + + if (!client.isAlwaysPermitted() + && !permissionManager.hasPermission(principal.getUserId(), client.getClientName()) + && !permissionManager.hasPermission(principal.getUserId(), + Permissions.ROLE_ADMIN)) { + logger.debug( + "user not allowed: " + principal.getUserId() + " - " + client.getClientName()); + throw new InvalidAuthorizationRequestError(redirectUri, + OidcAuthorizationErrorCode.ACCESS_DENIED, "user not allowed", state); + } + + if (!client.getAuthorizationGrantTypes() + .contains(OidcAuthorizationGrantType.authorization_code)) { + logger.debug("authorization grant type not allowed: " + + OidcAuthorizationGrantType.authorization_code + " - " + + client.getClientName()); + throw new InvalidAuthorizationRequestError(redirectUri, + OidcAuthorizationErrorCode.UNAUTHORIZED_CLIENT, + "authorization grant type not allowed", state); + + } + + if (!responseType.equals(OidcAuthorizationResponseType.code.toString())) { + logger.debug("response type not allowed: " + OidcAuthorizationResponseType.code + + + " - " + client.getClientName()); + throw new InvalidAuthorizationRequestError(redirectUri, + OidcAuthorizationErrorCode.UNSUPPORTED_RESPONSE_TYPE, + "response type not allowed", state); + + } + + Set scopes = Sets.newHashSet(scope.split(" ")); + + if (!scopes.contains("openid")) { + logger.debug("missing openid scope: " + scopes + " - " + client.getClientName()); + throw new InvalidAuthorizationRequestError(redirectUri, + OidcAuthorizationErrorCode.INVALID_SCOPE, "missing openid scope", state); + + } + + OidcAuthorizationCode authorizationCode = oidcAuthorizationCodeManager.create(clientId, + redirectUri, scopes, principal.getUserId(), nonce); + + String uri = redirectUri.toString(); + + if (StringUtils.hasText(redirectUri.getQuery())) { + uri += "&code=" + authorizationCode.getCode(); + } else { + uri += "?code=" + authorizationCode.getCode(); + } + + if (StringUtils.hasText(state)) { + uri += "&state=" + state; + } + + response.sendRedirect(uri); + } + + /** + * + * @param exception + * @param httpServletResponse + * @return + * @throws IOException + */ + @ExceptionHandler(InvalidAuthorizationRequestError.class) + public void handle(InvalidAuthorizationRequestError exception, HttpServletResponse response) + throws IOException { + String uri = exception.getRedirectUri().toString(); + + uri += "?error=" + exception.getErrorCode().getAuthorizationErrorCode(); + + if (StringUtils.hasText(exception.getErrorDescription())) { + uri += "&error_description=" + exception.getErrorDescription(); + } + + if (StringUtils.hasText(exception.getState())) { + uri += "&state=" + exception.getState(); + } + + response.sendRedirect(uri); + } + + /** + * + * @author _bastler@bstly.de + * + */ + static class InvalidAuthorizationRequestError extends RuntimeException { + /** + * default serialVersionUID + */ + private static final long serialVersionUID = 1L; + + private URI redirectUri; + private OidcAuthorizationErrorCode errorCode; + private String errorDescription; + private String state; + + /** + * + * @param redirectUri + * @param errorCode + * @param errorDescription + * @param state + */ + InvalidAuthorizationRequestError(URI redirectUri, OidcAuthorizationErrorCode errorCode, + String errorDescription, String state) { + super(errorDescription); + this.redirectUri = redirectUri; + this.errorCode = errorCode; + this.errorDescription = errorDescription; + this.state = state; + } + + /** + * @return the redirectUri + */ + public URI getRedirectUri() { + return redirectUri; + } + + /** + * @param redirectUri the redirectUri to set + */ + public void setRedirectUri(URI redirectUri) { + this.redirectUri = redirectUri; + } + + /** + * @return the errorCode + */ + public OidcAuthorizationErrorCode getErrorCode() { + return errorCode; + } + + /** + * @param errorCode the errorCode to set + */ + public void setErrorCode(OidcAuthorizationErrorCode errorCode) { + this.errorCode = errorCode; + } + + /** + * @return the errorDescription + */ + public String getErrorDescription() { + return errorDescription; + } + + /** + * @param errorDescription the errorDescription to set + */ + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + /** + * @return the state + */ + public String getState() { + return state; + } + + /** + * @param state the state to set + */ + public void setState(String state) { + this.state = state; + } + + } + +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/controller/OidcDiscoveryController.java b/oidc/src/main/java/de/bstly/we/oidc/controller/OidcDiscoveryController.java new file mode 100644 index 0000000..bfffce6 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/controller/OidcDiscoveryController.java @@ -0,0 +1,69 @@ +/** + * + */ +package de.bstly.we.oidc.controller; + +import java.net.URI; +import java.net.URISyntaxException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.google.common.collect.Sets; + +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.oidc.model.OidcConfiguration; + +/** + * @author _bastler@bstly.de + * + */ +@RequestMapping("/.well-known/openid-configuration") +@RestController +public class OidcDiscoveryController { + + @Value("${oidc.provider.issuer:}") + private String oidcIssuer; + + @GetMapping + public OidcConfiguration getConfiguration(HttpServletRequest request, + HttpServletResponse response) { + OidcConfiguration config = new OidcConfiguration(); + + String issuer = oidcIssuer; + + if (!StringUtils.hasText(issuer)) { + issuer = request.getScheme() + "://" + request.getServerName(); + if (request.getServerPort() != 443 && request.getServerPort() != 80) { + issuer += ":" + request.getServerPort(); + } + } + + config.setIssuer(issuer); + config.setScopes_supported(Sets.newHashSet("openid")); + config.setResponse_types_supported(Sets.newHashSet("code")); + config.setGrant_types_supported(Sets.newHashSet("authorization_code")); + config.setToken_endpoint_auth_methods_supported( + Sets.newHashSet("client_secret_post", "client_secret_basic")); + config.setSubject_types_supported(Sets.newHashSet("public")); + + try { + config.setAuthorization_endpoint(new URI(issuer + "/oidc/authorize")); + config.setToken_endpoint(new URI(issuer + "/oidc/token")); + config.setUserinfo_endpoint(new URI(issuer + "/oidc/userinfo")); + config.setJwks_uri(new URI(issuer + "/oidc/jwks")); + } catch (URISyntaxException e) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + return config; + } + +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/controller/OidcJwksController.java b/oidc/src/main/java/de/bstly/we/oidc/controller/OidcJwksController.java new file mode 100644 index 0000000..3e48489 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/controller/OidcJwksController.java @@ -0,0 +1,31 @@ +/** + * + */ +package de.bstly.we.oidc.controller; + +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.oidc.businesslogic.OidcTokenManager; + +/** + * + * @author _bastler@bstly.de + * + */ +@RequestMapping("/oidc/jwks") +@RestController +public class OidcJwksController { + + @Autowired + private OidcTokenManager oidcTokenManager; + + @GetMapping + public Map getJwks() { + return oidcTokenManager.getJwkSet().toJSONObject(); + } +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/controller/OidcTokenController.java b/oidc/src/main/java/de/bstly/we/oidc/controller/OidcTokenController.java new file mode 100644 index 0000000..f3187a7 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/controller/OidcTokenController.java @@ -0,0 +1,265 @@ +/** + * + */ +package de.bstly.we.oidc.controller; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Base64; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import com.google.common.collect.Sets; +import com.nimbusds.jose.JOSEException; + +import de.bstly.we.oidc.businesslogic.OidcAuthorizationCodeManager; +import de.bstly.we.oidc.businesslogic.OidcClientManager; +import de.bstly.we.oidc.businesslogic.OidcTokenManager; +import de.bstly.we.oidc.model.OidcAuthorizationCode; +import de.bstly.we.oidc.model.OidcClient; +import de.bstly.we.oidc.model.OidcClientAuthenticationMethod; +import de.bstly.we.oidc.model.OidcToken; +import de.bstly.we.oidc.model.OidcTokenErrorCode; +import de.bstly.we.oidc.model.OidcTokenRequest; +import de.bstly.we.oidc.model.OidcTokenResponse; + +/** + * + * @author _bastler@bstly.de + * + */ +@RequestMapping("/oidc/token") +@RestController +public class OidcTokenController { + + private Logger logger = LoggerFactory.getLogger(OidcTokenController.class); + + public static final String BASIC_AUTH = "Basic"; + + @Autowired + private OidcClientManager oidcClientManager; + @Autowired + private OidcAuthorizationCodeManager oidcAuthorizationCodeManager; + @Autowired + private OidcTokenManager oidcTokenManager; + + @Value("${oidc.provider.issuer:}") + private String oidcIssuer; + + @PostMapping + public OidcTokenResponse getToken( + // Authorization header for BASIC client authentication method + @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String authorizationHeader, + // token request + @ModelAttribute("tokenRequest") OidcTokenRequest tokenRequest, + // the request + HttpServletRequest request, + // the response + HttpServletResponse response) { + + response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store"); + response.setHeader(HttpHeaders.PRAGMA, "no-cache"); + + OidcClientAuthenticationMethod clientAuthenticationMethod = OidcClientAuthenticationMethod.post; + + if (StringUtils.hasText(authorizationHeader) + && StringUtils.startsWithIgnoreCase(authorizationHeader, BASIC_AUTH)) { + String decoded = new String(Base64.getDecoder().decode(authorizationHeader + .replaceFirst(BASIC_AUTH, "").trim().getBytes(StandardCharsets.UTF_8)), + StandardCharsets.UTF_8); + if (decoded.indexOf(":") != -1) { + tokenRequest.setClient_id(decoded.split(":")[0]); + tokenRequest.setClient_secret(decoded.split(":")[1]); + clientAuthenticationMethod = OidcClientAuthenticationMethod.basic; + } else { + logger.debug("invalid_basic_authentication: " + decoded); + throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_CLIENT, + "invalid_basic_authentication"); + } + } + + OidcClient client = oidcClientManager.getByClientIdAndSecret(tokenRequest.getClient_id(), + tokenRequest.getClient_secret()); + + if (client == null) { + logger.debug("client not found: " + tokenRequest.getClient_id()); + throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_CLIENT, "invalid_client"); + } + + if (!client.getClientAuthenticationMethods().contains(clientAuthenticationMethod)) { + logger.debug("invalid_authentication_method: " + clientAuthenticationMethod); + throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_REQUEST, + "invalid_authentication_method"); + } + + if (!client.getAuthorizationGrantTypes().contains(tokenRequest.getGrant_type())) { + logger.debug("invalid_grant_type: " + tokenRequest.getGrant_type()); + throw new InvalidTokenRequestError(OidcTokenErrorCode.UNAUTHORIZED_CLIENT, + "invalid_grant_type"); + } + + if (tokenRequest.getRedirect_uri() != null + && !client.getRedirectUris().contains(tokenRequest.getRedirect_uri().toString())) { + logger.debug("invalid redirect_uri: " + tokenRequest.getRedirect_uri().toString() + + " allowed: " + client.getRedirectUris()); + throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_REQUEST, + "invalid_redirect_uri"); + } + + OidcToken token = null; + switch (tokenRequest.getGrant_type()) { + case authorization_code: + OidcAuthorizationCode authorizationCode = oidcAuthorizationCodeManager + .getByCode(tokenRequest.getCode()); + if (authorizationCode == null) { + logger.debug("invalid authorization code: " + tokenRequest.getCode()); + throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_GRANT, + "invalid_authorization_code"); + } + if (Instant.now().isAfter(authorizationCode.getExpiry())) { + logger.debug("authorization code expired: " + authorizationCode.getExpiry()); + throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_GRANT, + "invalid_authorization_code"); + } + + if (!tokenRequest.getClient_id().equals(authorizationCode.getClientId())) { + logger.debug("invalid client for authorization code, expected: " + + authorizationCode.getClientId() + " got: " + tokenRequest.getClient_id()); + throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_CLIENT, + "invalid_client"); + } + + Set scopes = StringUtils.hasText(tokenRequest.getScope()) + ? Sets.newHashSet(tokenRequest.getScope().split(" ")) + : authorizationCode.getScopes(); + + if (!scopes.contains("openid") || !client.getScopes().containsAll(scopes)) { + logger.debug("missing openid scope: " + scopes + " - " + client.getClientName()); + throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_SCOPE, + "invalid scopes"); + } + + String issuer = oidcIssuer; + + if (!StringUtils.hasText(issuer)) { + issuer = request.getScheme() + "://" + request.getServerName(); + if (request.getServerPort() != 443 && request.getServerPort() != 80) { + issuer += ":" + request.getServerPort(); + } + } + + try { + token = oidcTokenManager.createTokenWithIdToken(client, + authorizationCode.getUserId(), authorizationCode.getNonce(), scopes, + issuer); + } catch (JOSEException e) { + logger.error("error creating token", client, authorizationCode); + e.printStackTrace(); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR); + } + + oidcAuthorizationCodeManager.removeByCode(tokenRequest.getCode()); + break; + case client_credentials: + token = oidcTokenManager.createToken(client, client.getId()); + break; + } + + OidcTokenResponse tokenResponse = new OidcTokenResponse(); + + tokenResponse.setAccess_token(token.getAccessToken()); + tokenResponse.setId_token(token.getIdToken()); + tokenResponse.setToken_type(OidcTokenManager.BEARER_TOKEN_TYPE); + tokenResponse.setExpires_in(client.getTokenLifetime()); + + oidcAuthorizationCodeManager.removeByCode(tokenRequest.getCode()); + + return tokenResponse; + } + + /** + * + * @param exception + * @param httpServletResponse + * @return + * @throws IOException + */ + @ExceptionHandler(InvalidTokenRequestError.class) + public ResponseEntity handle(InvalidTokenRequestError exception, + HttpServletResponse response) throws IOException { + // response.sendError(400, "redirect uri mismatch"); + return ResponseEntity.badRequest().contentType(MediaType.APPLICATION_JSON) + .body(" {\"error\": \"" + exception.getMessage() + "\"}"); + } + + /** + * + * @author _bastler@bstly.de + * + */ + static class InvalidTokenRequestError extends RuntimeException { + + /** + * default serialVersionUID + */ + private static final long serialVersionUID = 1L; + + private OidcTokenErrorCode errorCode; + private String errorDescription; + + InvalidTokenRequestError(OidcTokenErrorCode errorCode, String errorDescription) { + super(errorDescription); + this.errorCode = errorCode; + this.errorDescription = errorDescription; + } + + /** + * @return the errorCode + */ + public OidcTokenErrorCode getErrorCode() { + return errorCode; + } + + /** + * @param errorCode the errorCode to set + */ + public void setErrorCode(OidcTokenErrorCode errorCode) { + this.errorCode = errorCode; + } + + /** + * @return the errorDescription + */ + public String getErrorDescription() { + return errorDescription; + } + + /** + * @param errorDescription the errorDescription to set + */ + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + } +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/controller/OidcUserInfoController.java b/oidc/src/main/java/de/bstly/we/oidc/controller/OidcUserInfoController.java new file mode 100644 index 0000000..210abe4 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/controller/OidcUserInfoController.java @@ -0,0 +1,75 @@ +/** + * + */ +package de.bstly.we.oidc.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.google.common.net.HttpHeaders; +import com.nimbusds.jwt.JWTClaimsSet.Builder; + +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.model.User; +import de.bstly.we.oidc.businesslogic.OidcTokenManager; +import de.bstly.we.oidc.model.OidcToken; + +/** + * + * @author _bastler@bstly.de + * + */ +@RequestMapping("/oidc/userinfo") +@RestController +public class OidcUserInfoController extends BaseController { + + @Autowired + private OidcTokenManager oidcTokenManager; + @Autowired + private UserManager userManager; + + /** + * + * @param authorizationHeader + * @return + */ + @RequestMapping(method = { RequestMethod.GET, RequestMethod.POST }) + public void getUserInfo( + @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String authorizationHeader) { + + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + Long userId = getCurrentUserId(); + if (!auth.isAuthenticated()) { + if (authorizationHeader == null) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + String accessToken = authorizationHeader.replaceFirst(OidcTokenManager.BEARER_TOKEN_TYPE, "").trim(); + + OidcToken token = oidcTokenManager.getByAccessToken(accessToken); + if (token == null) { + throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED); + } + userId = token.getUserId(); + } + + User user = userManager.get(userId); + + if (user == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + Builder claimsSetBuilder = oidcTokenManager.createUserClaims(user); + + throw new EntityResponseStatusException(claimsSetBuilder.build().toJSONObject(), HttpStatus.OK); + + } +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/controller/model/OidcClientModel.java b/oidc/src/main/java/de/bstly/we/oidc/controller/model/OidcClientModel.java new file mode 100755 index 0000000..adbfab0 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/controller/model/OidcClientModel.java @@ -0,0 +1,126 @@ +/** + * + */ +package de.bstly.we.oidc.controller.model; + +import java.util.Set; + +import de.bstly.we.oidc.model.OidcAuthorizationGrantType; +import de.bstly.we.oidc.model.OidcClientAuthenticationMethod; + +/** + * + * @author _bastler@bstly.de + * + */ +public class OidcClientModel { + + private String name; + private Set registeredRedirectUris; + private Set clientAuthenticationMethods; + private Set authorizationGrantTypes; + private Set scopes; + private String loginUrl; + private boolean alwaysPermitted; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the registeredRedirectUris + */ + public Set getRegisteredRedirectUris() { + return registeredRedirectUris; + } + + /** + * @param registeredRedirectUris the registeredRedirectUris to set + */ + public void setRegisteredRedirectUris(Set registeredRedirectUris) { + this.registeredRedirectUris = registeredRedirectUris; + } + + /** + * @return the clientAuthenticationMethods + */ + public Set getClientAuthenticationMethods() { + return clientAuthenticationMethods; + } + + /** + * @param clientAuthenticationMethods the clientAuthenticationMethods to set + */ + public void setClientAuthenticationMethods( + Set clientAuthenticationMethods) { + this.clientAuthenticationMethods = clientAuthenticationMethods; + } + + /** + * @return the authorizationGrantTypes + */ + public Set getAuthorizationGrantTypes() { + return authorizationGrantTypes; + } + + /** + * @param authorizationGrantTypes the authorizationGrantTypes to set + */ + public void setAuthorizationGrantTypes( + Set authorizationGrantTypes) { + this.authorizationGrantTypes = authorizationGrantTypes; + } + + /** + * @return the scopes + */ + public Set getScopes() { + return scopes; + } + + /** + * @param scopes the scopes to set + */ + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + /** + * @return the loginUrl + */ + public String getLoginUrl() { + return loginUrl; + } + + /** + * @param loginUrl the loginUrl to set + */ + public void setLoginUrl(String loginUrl) { + this.loginUrl = loginUrl; + } + + /** + * @return the alwaysPermitted + */ + public boolean isAlwaysPermitted() { + return alwaysPermitted; + } + + /** + * @param alwaysPermitted the alwaysPermitted to set + */ + public void setAlwaysPermitted(boolean alwaysPermitted) { + this.alwaysPermitted = alwaysPermitted; + } + +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/model/OidcAuthorizationCode.java b/oidc/src/main/java/de/bstly/we/oidc/model/OidcAuthorizationCode.java new file mode 100644 index 0000000..4ec5977 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/model/OidcAuthorizationCode.java @@ -0,0 +1,99 @@ +/** + * + */ +package de.bstly.we.oidc.model; + +import java.net.URI; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Set; + +import org.apache.commons.lang3.RandomStringUtils; + +/** + * + * @author _bastler@bstly.de + * + */ +public class OidcAuthorizationCode { + + public static final int CODE_LENGTH = 32; + public static final int EXPIRY_MINUTES = 2; + + private final String clientId; + private final URI redirectUri; + private final Set scopes; + private final String code; + private final Instant expiry; + private final Long userId; + private final String nonce; + + /** + * @param clientId + * @param redirectUri + * @param scopes + * @param code + * @param expiry + * @param subject + */ + public OidcAuthorizationCode(String clientId, URI redirectUri, Set scopes, Long userId, + String nonce) { + this.clientId = clientId; + this.redirectUri = redirectUri; + this.scopes = scopes; + this.code = RandomStringUtils.random(CODE_LENGTH, true, true); + this.expiry = Instant.now().plus(EXPIRY_MINUTES, ChronoUnit.MINUTES); + this.userId = userId; + this.nonce = nonce; + } + + /** + * @return the clientId + */ + public String getClientId() { + return clientId; + } + + /** + * @return the redirectUri + */ + public URI getRedirectUri() { + return redirectUri; + } + + /** + * @return the scopes + */ + public Set getScopes() { + return scopes; + } + + /** + * @return the code + */ + public String getCode() { + return code; + } + + /** + * @return the expiry + */ + public Instant getExpiry() { + return expiry; + } + + /** + * @return the userId + */ + public Long getUserId() { + return userId; + } + + /** + * @return the nonce + */ + public String getNonce() { + return nonce; + } + +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/model/OidcAuthorizationErrorCode.java b/oidc/src/main/java/de/bstly/we/oidc/model/OidcAuthorizationErrorCode.java new file mode 100644 index 0000000..0b849f5 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/model/OidcAuthorizationErrorCode.java @@ -0,0 +1,30 @@ +/** + * + */ +package de.bstly.we.oidc.model; + +/** + * + * @author _bastler@bstly.de + * + */ +public enum OidcAuthorizationErrorCode { + INVALID_REQUEST("invalid_request"), UNAUTHORIZED_CLIENT("unauthorized_client"), ACCESS_DENIED("access_denied"), + UNSUPPORTED_RESPONSE_TYPE("unsupported_response_type"), INVALID_SCOPE("invalid_scope"), + SERVER_ERROR("server_error"), TEMPORARILY_UNAVAILABLE("temporarily_unavailable"), + INTERACTION_REQUIRED("interaction_required"), LOGIN_REQUIRED("login_required"), + ACCOUNT_SELECTION_REQUIRED("account_selection_required"), CONSENT_REQUIRED("consent_required"), + INVALID_REQUEST_URI("invalid_request_uri"), INVALID_REQUEST_OBJECT("invalid_request_object"), + REQUEST_NOT_SUPPORTED("request_not_supported"), REQUEST_URI_NOT_SUPPORTED("invalid_request"), + REGISTRATION_NOT_SUPPORTED("registration_not_supported"); + + private final String authorizationErrorCode; + + OidcAuthorizationErrorCode(String authorizationErrorCode) { + this.authorizationErrorCode = authorizationErrorCode; + } + + public String getAuthorizationErrorCode() { + return authorizationErrorCode; + } +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/model/OidcAuthorizationGrantType.java b/oidc/src/main/java/de/bstly/we/oidc/model/OidcAuthorizationGrantType.java new file mode 100644 index 0000000..53d4543 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/model/OidcAuthorizationGrantType.java @@ -0,0 +1,13 @@ +/** + * + */ +package de.bstly.we.oidc.model; + +/** + * + * @author _bastler@bstly.de + * + */ +public enum OidcAuthorizationGrantType { + authorization_code, client_credentials +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/model/OidcAuthorizationResponseType.java b/oidc/src/main/java/de/bstly/we/oidc/model/OidcAuthorizationResponseType.java new file mode 100644 index 0000000..7120294 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/model/OidcAuthorizationResponseType.java @@ -0,0 +1,13 @@ +/** + * + */ +package de.bstly.we.oidc.model; + +/** + * + * @author _bastler@bstly.de + * + */ +public enum OidcAuthorizationResponseType { + code +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/model/OidcClient.java b/oidc/src/main/java/de/bstly/we/oidc/model/OidcClient.java new file mode 100644 index 0000000..124dfce --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/model/OidcClient.java @@ -0,0 +1,238 @@ +/** + * + */ +package de.bstly.we.oidc.model; + +import java.util.Set; + +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; + +/** + * + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "oidc_clients") +public class OidcClient { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + @Column(name = "client_name", unique = true) + private String clientName; + @Column(name = "client_id", unique = true) + private String clientId; + @Column(name = "client_secret") + private String clientSecret; + @ElementCollection + @LazyCollection(LazyCollectionOption.FALSE) + @CollectionTable(name = "oidc_clients_methods") + @Enumerated(EnumType.STRING) + private Set clientAuthenticationMethods; + @ElementCollection + @LazyCollection(LazyCollectionOption.FALSE) + @CollectionTable(name = "oidc_clients_grant_types") + @Enumerated(EnumType.STRING) + private Set authorizationGrantTypes; + @ElementCollection + @LazyCollection(LazyCollectionOption.FALSE) + @CollectionTable(name = "oidc_clients_redirect_uris") + private Set redirectUris; + @ElementCollection + @LazyCollection(LazyCollectionOption.FALSE) + @CollectionTable(name = "oidc_clients_scopes") + private Set scopes; + @Column(name = "token_lifetime") + private Long tokenLifetime; + @Column(name = "login_url", length = 1024) + private String loginUrl; + @Column(name = "always_permitted", columnDefinition = "boolean default false") + private boolean alwaysPermitted; + @Column(name = "category") + private String category; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the clientName + */ + public String getClientName() { + return clientName; + } + + /** + * @param clientName the clientName to set + */ + public void setClientName(String clientName) { + this.clientName = clientName; + } + + /** + * @return the clientId + */ + public String getClientId() { + return clientId; + } + + /** + * @param clientId the clientId to set + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * @return the clientSecret + */ + public String getClientSecret() { + return clientSecret; + } + + /** + * @param clientSecret the clientSecret to set + */ + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + /** + * @return the clientAuthenticationMethods + */ + public Set getClientAuthenticationMethods() { + return clientAuthenticationMethods; + } + + /** + * @param clientAuthenticationMethods the clientAuthenticationMethods to set + */ + public void setClientAuthenticationMethods( + Set clientAuthenticationMethods) { + this.clientAuthenticationMethods = clientAuthenticationMethods; + } + + /** + * @return the authorizationGrantTypes + */ + public Set getAuthorizationGrantTypes() { + return authorizationGrantTypes; + } + + /** + * @param authorizationGrantTypes the authorizationGrantTypes to set + */ + public void setAuthorizationGrantTypes( + Set authorizationGrantTypes) { + this.authorizationGrantTypes = authorizationGrantTypes; + } + + /** + * @return the redirectUris + */ + public Set getRedirectUris() { + return redirectUris; + } + + /** + * @param redirectUris the redirectUris to set + */ + public void setRedirectUris(Set redirectUris) { + this.redirectUris = redirectUris; + } + + /** + * @return the scopes + */ + public Set getScopes() { + return scopes; + } + + /** + * @param scopes the scopes to set + */ + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + /** + * @return the tokenLifetime + */ + public Long getTokenLifetime() { + return tokenLifetime; + } + + /** + * @param tokenLifetime the tokenLifetime to set + */ + public void setTokenLifetime(Long tokenLifetime) { + this.tokenLifetime = tokenLifetime; + } + + /** + * @return the loginUrl + */ + public String getLoginUrl() { + return loginUrl; + } + + /** + * @param loginUrl the loginUrl to set + */ + public void setLoginUrl(String loginUrl) { + this.loginUrl = loginUrl; + } + + /** + * @return the alwaysPermitted + */ + public boolean isAlwaysPermitted() { + return alwaysPermitted; + } + + /** + * @param alwaysPermitted the alwaysPermitted to set + */ + public void setAlwaysPermitted(boolean alwaysPermitted) { + this.alwaysPermitted = alwaysPermitted; + } + + /** + * @return the category + */ + public String getCategory() { + return category; + } + + /** + * @param category the category to set + */ + public void setCategory(String category) { + this.category = category; + } + +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/model/OidcClientAuthenticationMethod.java b/oidc/src/main/java/de/bstly/we/oidc/model/OidcClientAuthenticationMethod.java new file mode 100644 index 0000000..e169527 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/model/OidcClientAuthenticationMethod.java @@ -0,0 +1,13 @@ +/** + * + */ +package de.bstly.we.oidc.model; + +/** + * + * @author _bastler@bstly.de + * + */ +public enum OidcClientAuthenticationMethod { + basic, post +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/model/OidcConfiguration.java b/oidc/src/main/java/de/bstly/we/oidc/model/OidcConfiguration.java new file mode 100644 index 0000000..351f416 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/model/OidcConfiguration.java @@ -0,0 +1,169 @@ +/** + * + */ +package de.bstly.we.oidc.model; + +import java.net.URI; +import java.util.Set; + +/** + * @author _bastler@bstly.de + * + */ +public class OidcConfiguration { + + private String issuer; + private URI authorization_endpoint; + private URI token_endpoint; + private URI userinfo_endpoint; + private URI jwks_uri; + private Set scopes_supported; + private Set response_types_supported; + private Set subject_types_supported; + private Set grant_types_supported; + private Set token_endpoint_auth_methods_supported; + + /** + * @return the issuer + */ + public String getIssuer() { + return issuer; + } + + /** + * @param issuer the issuer to set + */ + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + /** + * @return the authorization_endpoint + */ + public URI getAuthorization_endpoint() { + return authorization_endpoint; + } + + /** + * @param authorization_endpoint the authorization_endpoint to set + */ + public void setAuthorization_endpoint(URI authorization_endpoint) { + this.authorization_endpoint = authorization_endpoint; + } + + /** + * @return the token_endpoint + */ + public URI getToken_endpoint() { + return token_endpoint; + } + + /** + * @param token_endpoint the token_endpoint to set + */ + public void setToken_endpoint(URI token_endpoint) { + this.token_endpoint = token_endpoint; + } + + /** + * @return the userinfo_endpoint + */ + public URI getUserinfo_endpoint() { + return userinfo_endpoint; + } + + /** + * @param userinfo_endpoint the userinfo_endpoint to set + */ + public void setUserinfo_endpoint(URI userinfo_endpoint) { + this.userinfo_endpoint = userinfo_endpoint; + } + + /** + * @return the jwks_uri + */ + public URI getJwks_uri() { + return jwks_uri; + } + + /** + * @param jwks_uri the jwks_uri to set + */ + public void setJwks_uri(URI jwks_uri) { + this.jwks_uri = jwks_uri; + } + + /** + * @return the scopes_supported + */ + public Set getScopes_supported() { + return scopes_supported; + } + + /** + * @param scopes_supported the scopes_supported to set + */ + public void setScopes_supported(Set scopes_supported) { + this.scopes_supported = scopes_supported; + } + + /** + * @return the response_types_supported + */ + public Set getResponse_types_supported() { + return response_types_supported; + } + + /** + * @param response_types_supported the response_types_supported to set + */ + public void setResponse_types_supported(Set response_types_supported) { + this.response_types_supported = response_types_supported; + } + + /** + * @return the subject_types_supported + */ + public Set getSubject_types_supported() { + return subject_types_supported; + } + + /** + * @param subject_types_supported the subject_types_supported to set + */ + public void setSubject_types_supported(Set subject_types_supported) { + this.subject_types_supported = subject_types_supported; + } + + /** + * @return the grant_types_supported + */ + public Set getGrant_types_supported() { + return grant_types_supported; + } + + /** + * @param grant_types_supported the grant_types_supported to set + */ + public void setGrant_types_supported(Set grant_types_supported) { + this.grant_types_supported = grant_types_supported; + } + + /** + * @return the token_endpoint_auth_methods_supported + */ + public Set getToken_endpoint_auth_methods_supported() { + return token_endpoint_auth_methods_supported; + } + + /** + * @param token_endpoint_auth_methods_supported the + * token_endpoint_auth_methods_supported + * to set + */ + public void setToken_endpoint_auth_methods_supported( + Set token_endpoint_auth_methods_supported) { + this.token_endpoint_auth_methods_supported = token_endpoint_auth_methods_supported; + } + +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/model/OidcToken.java b/oidc/src/main/java/de/bstly/we/oidc/model/OidcToken.java new file mode 100644 index 0000000..4a6af62 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/model/OidcToken.java @@ -0,0 +1,163 @@ +/** + * + */ +package de.bstly.we.oidc.model; + +import java.util.Set; + +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "oidc_tokens") +public class OidcToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + @Column(name = "user_id") + private Long userId; + @Column(name = "client_id") + private Long client; + @Column(name = "access_token") + private String accessToken; + @Column(name = "refresh_token") + private String refreshToken; + @Column(name = "expires_in") + private Long expiresIn; + @Column(name = "id_token", length = 4000) + private String idToken; + @JsonIgnore + @ElementCollection + @LazyCollection(LazyCollectionOption.FALSE) + private Set scopes; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the userId + */ + public Long getUserId() { + return userId; + } + + /** + * @param userId the userId to set + */ + public void setUserId(Long userId) { + this.userId = userId; + } + + /** + * @return the client + */ + public Long getClient() { + return client; + } + + /** + * @param client the client to set + */ + public void setClient(Long client) { + this.client = client; + } + + /** + * @return the accessToken + */ + public String getAccessToken() { + return accessToken; + } + + /** + * @param accessToken the accessToken to set + */ + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + /** + * @return the refreshToken + */ + public String getRefreshToken() { + return refreshToken; + } + + /** + * @param refreshToken the refreshToken to set + */ + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + /** + * @return the expiresIn + */ + public Long getExpiresIn() { + return expiresIn; + } + + /** + * @param expiresIn the expiresIn to set + */ + public void setExpiresIn(Long expiresIn) { + this.expiresIn = expiresIn; + } + + /** + * @return the idToken + */ + public String getIdToken() { + return idToken; + } + + /** + * @param idToken the idToken to set + */ + public void setIdToken(String idToken) { + this.idToken = idToken; + } + + /** + * @return the scopes + */ + public Set getScopes() { + return scopes; + } + + /** + * @param scopes the scopes to set + */ + public void setScopes(Set scopes) { + this.scopes = scopes; + } + +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/model/OidcTokenErrorCode.java b/oidc/src/main/java/de/bstly/we/oidc/model/OidcTokenErrorCode.java new file mode 100644 index 0000000..84287ab --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/model/OidcTokenErrorCode.java @@ -0,0 +1,25 @@ +/** + * + */ +package de.bstly.we.oidc.model; + +/** + * + * @author _bastler@bstly.de + * + */ +public enum OidcTokenErrorCode { + INVALID_REQUEST("invalid_request"), INVALID_CLIENT("invalid_client"), + INVALID_GRANT("invalid_grant"), UNAUTHORIZED_CLIENT("unauthorized_client"), + UNSUPPORTED_GRANT_TYPE("unsupported_grant_type"), INVALID_SCOPE("invalid_scope"); + + private final String tokenErrorCode; + + OidcTokenErrorCode(String tokenErrorCode) { + this.tokenErrorCode = tokenErrorCode; + } + + public String getTokenErrorCode() { + return tokenErrorCode; + } +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/model/OidcTokenRequest.java b/oidc/src/main/java/de/bstly/we/oidc/model/OidcTokenRequest.java new file mode 100644 index 0000000..7bbaf93 --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/model/OidcTokenRequest.java @@ -0,0 +1,97 @@ +/** + * + */ +package de.bstly.we.oidc.model; + +import java.net.URI; + +/** + * + * @author _bastler@bstly.de + * + */ +public class OidcTokenRequest { + + private final String code; + private final OidcAuthorizationGrantType grant_type; + private String client_id; + private String client_secret; + private final URI redirect_uri; + private final String scope; + + /** + * @param code + * @param grant_type + * @param client_id + * @param client_secret + * @param redirect_uri + * @param scope + */ + public OidcTokenRequest(String code, OidcAuthorizationGrantType grant_type, String client_id, + String client_secret, URI redirect_uri, String scope) { + super(); + this.code = code; + this.grant_type = grant_type; + this.client_id = client_id; + this.client_secret = client_secret; + this.redirect_uri = redirect_uri; + this.scope = scope; + } + + /** + * @return the client_id + */ + public String getClient_id() { + return client_id; + } + + /** + * @param client_id the client_id to set + */ + public void setClient_id(String client_id) { + this.client_id = client_id; + } + + /** + * @return the client_secret + */ + public String getClient_secret() { + return client_secret; + } + + /** + * @param client_secret the client_secret to set + */ + public void setClient_secret(String client_secret) { + this.client_secret = client_secret; + } + + /** + * @return the code + */ + public String getCode() { + return code; + } + + /** + * @return the grant_type + */ + public OidcAuthorizationGrantType getGrant_type() { + return grant_type; + } + + /** + * @return the redirect_uri + */ + public URI getRedirect_uri() { + return redirect_uri; + } + + /** + * @return the scope + */ + public String getScope() { + return scope; + } + +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/model/OidcTokenResponse.java b/oidc/src/main/java/de/bstly/we/oidc/model/OidcTokenResponse.java new file mode 100644 index 0000000..5c5f74a --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/model/OidcTokenResponse.java @@ -0,0 +1,89 @@ +/** + * + */ +package de.bstly.we.oidc.model; + +/** + * + * @author _bastler@bstly.de + * + */ +public class OidcTokenResponse { + + private String access_token; + private String token_type; + private String refresh_token; + private long expires_in; + private String id_token; + + /** + * @return the access_token + */ + public String getAccess_token() { + return access_token; + } + + /** + * @param access_token the access_token to set + */ + public void setAccess_token(String access_token) { + this.access_token = access_token; + } + + /** + * @return the token_type + */ + public String getToken_type() { + return token_type; + } + + /** + * @param token_type the token_type to set + */ + public void setToken_type(String token_type) { + this.token_type = token_type; + } + + /** + * @return the refresh_token + */ + public String getRefresh_token() { + return refresh_token; + } + + /** + * @param refresh_token the refresh_token to set + */ + public void setRefresh_token(String refresh_token) { + this.refresh_token = refresh_token; + } + + /** + * @return the expires_in + */ + public long getExpires_in() { + return expires_in; + } + + /** + * @param expires_in the expires_in to set + */ + public void setExpires_in(long expires_in) { + this.expires_in = expires_in; + } + + /** + * @return the id_token + */ + public String getId_token() { + return id_token; + } + + /** + * @param id_token the id_token to set + */ + public void setId_token(String id_token) { + this.id_token = id_token; + } + +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/repository/OidcClientRepository.java b/oidc/src/main/java/de/bstly/we/oidc/repository/OidcClientRepository.java new file mode 100755 index 0000000..874ff7f --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/repository/OidcClientRepository.java @@ -0,0 +1,19 @@ +/** + * + */ +package de.bstly.we.oidc.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.oidc.model.OidcClient; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface OidcClientRepository extends JpaRepository, QuerydslPredicateExecutor { +} diff --git a/oidc/src/main/java/de/bstly/we/oidc/repository/OidcTokenRepository.java b/oidc/src/main/java/de/bstly/we/oidc/repository/OidcTokenRepository.java new file mode 100644 index 0000000..6dfe79f --- /dev/null +++ b/oidc/src/main/java/de/bstly/we/oidc/repository/OidcTokenRepository.java @@ -0,0 +1,19 @@ +/** + * + */ +package de.bstly.we.oidc.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.oidc.model.OidcToken; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface OidcTokenRepository extends JpaRepository, QuerydslPredicateExecutor { +} diff --git a/partey/pom.xml b/partey/pom.xml new file mode 100755 index 0000000..6b62a16 --- /dev/null +++ b/partey/pom.xml @@ -0,0 +1,31 @@ + + 4.0.0 + + de.bstly.we + webstly-main + ${revision} + + + partey + webstly-partey + + + + de.bstly.we + webstly-core + ${revision} + + + de.bstly.we + webstly-email + ${revision} + + + de.bstly.we + webstly-i18n + ${revision} + + + diff --git a/partey/src/main/java/de/bstly/we/partey/api/controller/DebugController.java b/partey/src/main/java/de/bstly/we/partey/api/controller/DebugController.java new file mode 100644 index 0000000..49283c6 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/api/controller/DebugController.java @@ -0,0 +1,42 @@ +/** + * + */ +package de.bstly.we.partey.api.controller; + +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.partey.api.controller.support.DebugLogger; +import de.bstly.we.partey.api.security.ParteyApiAuthentication; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/partey/api") +public class DebugController extends DebugLogger { + + @Autowired + private ParteyApiAuthentication parteyApiAuthentication; + + /** + * + */ + @RequestMapping("/**") + public void debug(@RequestBody Optional payload, HttpServletRequest request, + HttpServletResponse response) { + parteyApiAuthentication.authenticateRequest(request); + + debugPrintRequest(request, payload); + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/api/controller/MapController.java b/partey/src/main/java/de/bstly/we/partey/api/controller/MapController.java new file mode 100644 index 0000000..77bf5b6 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/api/controller/MapController.java @@ -0,0 +1,73 @@ +/** + * + */ +package de.bstly.we.partey.api.controller; + +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +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 com.beust.jcommander.internal.Lists; + +import de.bstly.we.partey.api.controller.model.MapDetailsData; +import de.bstly.we.partey.api.controller.support.DebugLogger; +import de.bstly.we.partey.api.security.ParteyApiAuthentication; +import de.bstly.we.partey.businesslogic.ParteyMapManager; +import de.bstly.we.partey.businesslogic.model.Room; +import de.bstly.we.partey.model.GameRoomPolicyTypes; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/partey/api/map") +public class MapController extends DebugLogger { + + @Autowired + private ParteyApiAuthentication parteyApiAuthentication; + @Autowired + private ParteyMapManager parteyMapManager; + + /** + * + * @param playUri + * @param request + * @return + */ + @GetMapping + public MapDetailsData getMapData(@RequestParam("playUri") String playUri, HttpServletRequest request, + HttpServletResponse response) { + parteyApiAuthentication.authenticateRequest(request); + + debugPrintRequest(request); + + MapDetailsData mapData = new MapDetailsData(); + mapData.setTags(Lists.newArrayList()); + mapData.setPolicy_type(GameRoomPolicyTypes.UNKNOWN); + + Room room = parteyMapManager.parseRoom(playUri, request); + mapData.setMapUrl(room.getMapUrl()); + + if (room.getTags() != null) { + mapData.setTags(room.getTags()); + } + + if (room.getPolicyType() != null) { + mapData.setPolicy_type(room.getPolicyType()); + } + + debugPrintResponse(request, Optional.of(mapData)); + + return mapData; + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/api/controller/ReportController.java b/partey/src/main/java/de/bstly/we/partey/api/controller/ReportController.java new file mode 100644 index 0000000..b030955 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/api/controller/ReportController.java @@ -0,0 +1,42 @@ +/** + * + */ +package de.bstly.we.partey.api.controller; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +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; + +import de.bstly.we.partey.api.controller.model.UserReport; +import de.bstly.we.partey.api.controller.support.DebugLogger; +import de.bstly.we.partey.api.security.ParteyApiAuthentication; +import de.bstly.we.partey.businesslogic.ParteyUserReportManager; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/partey/api/report") +public class ReportController extends DebugLogger { + + @Autowired + private ParteyApiAuthentication parteyApiAuthentication; + @Autowired + private ParteyUserReportManager parteyUserReportManager; + + @PostMapping("") + public void report(@RequestBody UserReport userReport, HttpServletRequest request) { + parteyApiAuthentication.authenticateRequest(request); + debugPrintRequest(request); + + parteyUserReportManager.create(Long.valueOf(userReport.getReporterUserUuid()), + Long.valueOf(userReport.getReportedUserUuid()), userReport.getReportedUserComment(), + userReport.getReportWorldSlug()); + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/api/controller/RoomController.java b/partey/src/main/java/de/bstly/we/partey/api/controller/RoomController.java new file mode 100644 index 0000000..04eb708 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/api/controller/RoomController.java @@ -0,0 +1,128 @@ +/** + * + */ +package de.bstly.we.partey.api.controller; + +import java.util.List; +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.StringUtils; +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 com.beust.jcommander.internal.Lists; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.model.User; +import de.bstly.we.partey.api.controller.model.MemberData; +import de.bstly.we.partey.api.controller.support.DebugLogger; +import de.bstly.we.partey.api.security.ParteyApiAuthentication; +import de.bstly.we.partey.businesslogic.ParteyMapManager; +import de.bstly.we.partey.businesslogic.ParteyPermissions; +import de.bstly.we.partey.businesslogic.ParteyUserTagManager; +import de.bstly.we.partey.businesslogic.model.Room; +import de.bstly.we.partey.model.ParteyUserTag; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/partey/api/room") +public class RoomController extends DebugLogger { + + @Autowired + private ParteyApiAuthentication parteyApiAuthentication; + @Autowired + private PermissionManager permissionManager; + @Autowired + private ParteyMapManager parteyMapManager; + @Autowired + private ParteyUserTagManager parteyUserTagManager; + @Autowired + private UserManager userManager; + + @Value("${we.bstly.partey.visitCardUrlFormat:}") + private String visitCardUrlFormat; + + private List worldMapCache = Lists.newArrayList(); + + /** + * + * @param userIdentifier + * @param roomId + * @param request + * @return + */ + @GetMapping("/access") + public MemberData access(@RequestParam("userIdentifier") String userIdentifier, + @RequestParam("roomId") String roomId, HttpServletRequest request) { + parteyApiAuthentication.authenticateRequest(request); + + debugPrintRequest(request); + + Room room = parteyMapManager.parseRoom(roomId, request); + + if (!worldMapCache.contains(room.getUrl())) { + worldMapCache.add(room.getUrl()); + } + + MemberData memberData = new MemberData(); + memberData.setUserUuid(userIdentifier); + memberData.setAnonymous(true); + memberData.setTags(Lists.newArrayList()); + try { + Long userId = Long.parseLong(userIdentifier); + + if (StringUtils.hasText(visitCardUrlFormat)) { + User user = userManager.get(userId); + memberData.setVisitCardUrl(String.format(visitCardUrlFormat, user.getUsername())); + } + + if (permissionManager.isFullUser(userId) + && permissionManager.hasPermission(userId, ParteyPermissions.PARTEY)) { + memberData.setAnonymous(false); + + for (ParteyUserTag parteyUserTag : parteyUserTagManager.getForTarget(userId)) { + memberData.getTags().add(parteyUserTag.getTag()); + } + } + } catch (NumberFormatException e) { + // ignore + } + + debugPrintResponse(request, Optional.of(memberData)); + + return memberData; + } + + /** + * + * @param roomUrl + * @param request + * @return + */ + @GetMapping("/sameWorld") + public List sameWorld(@RequestParam("roomUrl") String roomUrl, + HttpServletRequest request) { + parteyApiAuthentication.authenticateRequest(request); + + debugPrintRequest(request); + + Room room = parteyMapManager.parseRoom(roomUrl, request); + + if (!worldMapCache.contains(room.getUrl())) { + worldMapCache.add(room.getUrl()); + } + + return worldMapCache; + } +} diff --git a/partey/src/main/java/de/bstly/we/partey/api/controller/model/CharacterTexture.java b/partey/src/main/java/de/bstly/we/partey/api/controller/model/CharacterTexture.java new file mode 100644 index 0000000..2d3019f --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/api/controller/model/CharacterTexture.java @@ -0,0 +1,74 @@ +/** + * + */ +package de.bstly.we.partey.api.controller.model; + +/** + * + * @author _bastler@bstly.de + * + */ +public class CharacterTexture { + + private int id; + private int level; + private String url; + private String rights; + + /** + * @return the id + */ + public int getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(int id) { + this.id = id; + } + + /** + * @return the level + */ + public int getLevel() { + return level; + } + + /** + * @param level the level to set + */ + public void setLevel(int level) { + this.level = level; + } + + /** + * @return the url + */ + public String getUrl() { + return url; + } + + /** + * @param url the url to set + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * @return the rights + */ + public String getRights() { + return rights; + } + + /** + * @param rights the rights to set + */ + public void setRights(String rights) { + this.rights = rights; + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/api/controller/model/MapDetailsData.java b/partey/src/main/java/de/bstly/we/partey/api/controller/model/MapDetailsData.java new file mode 100644 index 0000000..30f0b99 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/api/controller/model/MapDetailsData.java @@ -0,0 +1,110 @@ +/** + * + */ +package de.bstly.we.partey.api.controller.model; + +import java.util.List; + +import com.google.common.collect.Lists; + +import de.bstly.we.partey.model.GameRoomPolicyTypes; + +/** + * + * @author _bastler@bstly.de + * + */ +public class MapDetailsData { + + private String mapUrl; + private GameRoomPolicyTypes policy_type = GameRoomPolicyTypes.ANONYMOUS_POLICY; + private List tags = Lists.newArrayList(); + private List textures = Lists.newArrayList(); + private boolean authenticationMandatory; + private String iframeAuthentication; + + /** + * @return the mapUrl + */ + public String getMapUrl() { + return mapUrl; + } + + /** + * @param mapUrl the mapUrl to set + */ + public void setMapUrl(String mapUrl) { + this.mapUrl = mapUrl; + } + + /** + * @return the policy_type + */ + public GameRoomPolicyTypes getPolicy_type() { + return policy_type; + } + + /** + * @param policy_type the policy_type to set + */ + public void setPolicy_type(GameRoomPolicyTypes policy_type) { + this.policy_type = policy_type; + } + + /** + * @return the tags + */ + public List getTags() { + return tags; + } + + /** + * @param tags the tags to set + */ + public void setTags(List tags) { + this.tags = tags; + } + + /** + * @return the textures + */ + public List getTextures() { + return textures; + } + + /** + * @param textures the textures to set + */ + public void setTextures(List textures) { + this.textures = textures; + } + + /** + * @return the authenticationMandatory + */ + public boolean isAuthenticationMandatory() { + return authenticationMandatory; + } + + /** + * @param authenticationMandatory the authenticationMandatory to set + */ + public void setAuthenticationMandatory(boolean authenticationMandatory) { + this.authenticationMandatory = authenticationMandatory; + } + + /** + * @return the iframeAuthentication + */ + public String getIframeAuthentication() { + return iframeAuthentication; + } + + /** + * @param iframeAuthentication the iframeAuthentication to set + */ + public void setIframeAuthentication(String iframeAuthentication) { + this.iframeAuthentication = iframeAuthentication; + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/api/controller/model/MemberData.java b/partey/src/main/java/de/bstly/we/partey/api/controller/model/MemberData.java new file mode 100644 index 0000000..1d4b43b --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/api/controller/model/MemberData.java @@ -0,0 +1,108 @@ +/** + * + */ +package de.bstly.we.partey.api.controller.model; + +import java.util.List; + +import com.beust.jcommander.internal.Lists; + +/** + * + * @author _bastler@bstly.de + * + */ +public class MemberData { + + private String userUuid; + private List tags = Lists.newArrayList(); + private String visitCardUrl; + private List textures = Lists.newArrayList(); + private List messages = Lists.newArrayList(); + private boolean anonymous; + + /** + * @return the userUuid + */ + public String getUserUuid() { + return userUuid; + } + + /** + * @param userUuid the userUuid to set + */ + public void setUserUuid(String userUuid) { + this.userUuid = userUuid; + } + + /** + * @return the tags + */ + public List getTags() { + return tags; + } + + /** + * @param tags the tags to set + */ + public void setTags(List tags) { + this.tags = tags; + } + + /** + * @return the visitCardUrl + */ + public String getVisitCardUrl() { + return visitCardUrl; + } + + /** + * @param visitCardUrl the visitCardUrl to set + */ + public void setVisitCardUrl(String visitCardUrl) { + this.visitCardUrl = visitCardUrl; + } + + /** + * @return the textures + */ + public List getTextures() { + return textures; + } + + /** + * @param textures the textures to set + */ + public void setTextures(List textures) { + this.textures = textures; + } + + /** + * @return the messages + */ + public List getMessages() { + return messages; + } + + /** + * @param messages the messages to set + */ + public void setMessages(List messages) { + this.messages = messages; + } + + /** + * @return the anonymous + */ + public boolean isAnonymous() { + return anonymous; + } + + /** + * @param anonymous the anonymous to set + */ + public void setAnonymous(boolean anonymous) { + this.anonymous = anonymous; + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/api/controller/model/UserReport.java b/partey/src/main/java/de/bstly/we/partey/api/controller/model/UserReport.java new file mode 100644 index 0000000..3cf0b0f --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/api/controller/model/UserReport.java @@ -0,0 +1,73 @@ +/** + * + */ +package de.bstly.we.partey.api.controller.model; + +/** + * @author _bastler@bstly.de + * + */ +public class UserReport { + + private String reportedUserUuid; + private String reportedUserComment; + private String reporterUserUuid; + private String reportWorldSlug; + + /** + * @return the reportedUserUuid + */ + public String getReportedUserUuid() { + return reportedUserUuid; + } + + /** + * @param reportedUserUuid the reportedUserUuid to set + */ + public void setReportedUserUuid(String reportedUserUuid) { + this.reportedUserUuid = reportedUserUuid; + } + + /** + * @return the reportedUserComment + */ + public String getReportedUserComment() { + return reportedUserComment; + } + + /** + * @param reportedUserComment the reportedUserComment to set + */ + public void setReportedUserComment(String reportedUserComment) { + this.reportedUserComment = reportedUserComment; + } + + /** + * @return the reporterUserUuid + */ + public String getReporterUserUuid() { + return reporterUserUuid; + } + + /** + * @param reporterUserUuid the reporterUserUuid to set + */ + public void setReporterUserUuid(String reporterUserUuid) { + this.reporterUserUuid = reporterUserUuid; + } + + /** + * @return the reportWorldSlug + */ + public String getReportWorldSlug() { + return reportWorldSlug; + } + + /** + * @param reportWorldSlug the reportWorldSlug to set + */ + public void setReportWorldSlug(String reportWorldSlug) { + this.reportWorldSlug = reportWorldSlug; + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/api/controller/support/DebugLogger.java b/partey/src/main/java/de/bstly/we/partey/api/controller/support/DebugLogger.java new file mode 100644 index 0000000..85fff73 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/api/controller/support/DebugLogger.java @@ -0,0 +1,79 @@ +/** + * + */ +package de.bstly.we.partey.api.controller.support; + +import java.util.Iterator; +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * @author _bastler@bstly.de + * + */ +public class DebugLogger { + + protected Logger logger = LoggerFactory.getLogger(getClass()); + protected Gson gson = new Gson(); + + /** + * + * @param request + */ + public void debugPrintRequest(HttpServletRequest request) { + debugPrintRequest(request, Optional.empty()); + } + + /** + * + * @param request + * @param payload + */ + public void debugPrintRequest(HttpServletRequest request, Optional payload) { + logger.debug( + "Request: " + request.getMethod().toUpperCase() + " " + request.getRequestURI()); + + logger.debug("Headers:"); + + for (Iterator it = request.getHeaderNames().asIterator(); it.hasNext();) { + String headerName = it.next(); + logger.debug("\t" + headerName + ": " + request.getHeader(headerName)); + } + + if (!request.getParameterMap().isEmpty()) { + logger.debug("Parameters:"); + for (Iterator it = request.getParameterNames().asIterator(); it.hasNext();) { + String parameterName = it.next(); + logger.debug("\t" + parameterName + ": " + request.getParameter(parameterName)); + } + } + + if (payload.isPresent()) { + logger.debug("Body:"); + logger.debug(gson.toJson(payload.get())); + } + + logger.debug(""); + } + + /** + * + * @param response + * @param payload + */ + public void debugPrintResponse(HttpServletRequest response, Optional payload) { + if (payload.isPresent()) { + logger.debug("Response: " + response.getRequestURI()); + logger.debug("Body:"); + logger.debug(gson.toJson(payload.get())); + logger.debug(""); + } + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/api/security/ParteyApiAuthentication.java b/partey/src/main/java/de/bstly/we/partey/api/security/ParteyApiAuthentication.java new file mode 100644 index 0000000..836a113 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/api/security/ParteyApiAuthentication.java @@ -0,0 +1,50 @@ +/** + * + */ +package de.bstly.we.partey.api.security; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.Permissions; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.security.model.LocalUserDetails; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class ParteyApiAuthentication { + + @Value("${we.bstly.partey.apiToken:}") + private String apiToken; + + @Autowired + private PermissionManager permissionManager; + + public void authenticateRequest(HttpServletRequest request) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.isAuthenticated() + && auth.getPrincipal() instanceof LocalUserDetails) { + LocalUserDetails details = (LocalUserDetails) auth.getPrincipal(); + if (!permissionManager.hasPermission(details.getUserId(), Permissions.ROLE_ADMIN)) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + } else { + String token = request.getHeader(HttpHeaders.AUTHORIZATION); + if (!StringUtils.hasText(token) || !token.equals(apiToken)) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + } + } +} diff --git a/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyMapManager.java b/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyMapManager.java new file mode 100644 index 0000000..c1d5eaa --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyMapManager.java @@ -0,0 +1,136 @@ +/** + * + */ +package de.bstly.we.partey.businesslogic; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ResponseStatusException; + +import com.beust.jcommander.internal.Lists; + +import de.bstly.we.partey.businesslogic.model.Room; +import de.bstly.we.partey.model.GameRoomPolicyTypes; +import de.bstly.we.partey.model.ParteyMap; +import de.bstly.we.partey.repository.ParteyMapRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class ParteyMapManager { + + @Autowired + private ParteyMapRepository parteyMapRepository; + + @Value("${we.bstly.partey.mapUrl:http://localhost/}") + private String internalMapUri; + + private Pattern internalMapUriPattern = Pattern.compile("\\/@\\/(.+)"); + private Pattern externalMapUriPattern = Pattern.compile("\\/_\\/(.+)"); + + /** + * + * @param id + * @return + */ + public ParteyMap get(String id) { + return parteyMapRepository.findById(id).orElse(null); + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + PageRequest pageRequest = PageRequest.of(page, size, + descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending()); + + return parteyMapRepository.findAll(pageRequest); + } + + /** + * + * @param name + * @return + */ + public ParteyMap save(ParteyMap parteyMap) { + return parteyMapRepository.save(parteyMap); + } + + /** + * + * @param id + */ + public void delete(String id) { + parteyMapRepository.deleteById(id); + } + + /** + * + * @param url + * @param request + * @return + */ + public Room parseRoom(String url, HttpServletRequest request) { + Room room = new Room(); + room.setPolicyType(GameRoomPolicyTypes.UNKNOWN); + room.setTags(Lists.newArrayList()); + room.setUrl(url); + URI uri; + try { + uri = new URI(url); + } catch (URISyntaxException e) { + throw new ResponseStatusException(HttpStatus.CONFLICT, "invalid playUri"); + } + + Matcher internalMatcher = internalMapUriPattern.matcher(uri.getPath()); + Matcher externalMatcher = externalMapUriPattern.matcher(uri.getPath()); + + if (internalMatcher.matches()) { + room.setPolicyType(GameRoomPolicyTypes.MEMBERS_ONLY_POLICY); + room.setMapId(internalMatcher.group(1)); + room.setMapUrl(internalMapUri + room.getMapId()); + } else if (externalMatcher.matches()) { + room.setMapId(externalMatcher.group(1)); + if (StringUtils.hasText(uri.getScheme())) { + room.setMapUrl(uri.getScheme() + "://" + room.getMapId()); + } else { + room.setMapUrl(request.getScheme() + "://" + room.getMapId()); + } + } else { + throw new ResponseStatusException(HttpStatus.CONFLICT, "invalid playUri"); + } + + ParteyMap parteyMap = get(room.getMapId()); + if (parteyMap != null) { + if (parteyMap.getPolicyType() != null) { + room.setPolicyType(parteyMap.getPolicyType()); + } + if (parteyMap.getTags() != null) { + room.setTags(parteyMap.getTags()); + } + } + + return room; + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyPermissions.java b/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyPermissions.java new file mode 100644 index 0000000..03073b1 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyPermissions.java @@ -0,0 +1,13 @@ +/** + * + */ +package de.bstly.we.partey.businesslogic; + +/** + * @author _bastler@bstly.de + * + */ +public interface ParteyPermissions { + + public static final String PARTEY = "partey"; +} diff --git a/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyQuotas.java b/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyQuotas.java new file mode 100644 index 0000000..6d28811 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyQuotas.java @@ -0,0 +1,13 @@ +/** + * + */ +package de.bstly.we.partey.businesslogic; + +/** + * @author _bastler@bstly.de + * + */ +public interface ParteyQuotas { + + public static final String PARTEY_TIMESLOT = "partey_timeslot"; +} diff --git a/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyUserReportManager.java b/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyUserReportManager.java new file mode 100644 index 0000000..97481d8 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyUserReportManager.java @@ -0,0 +1,103 @@ +/** + * + */ +package de.bstly.we.partey.businesslogic; + +import java.time.Instant; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; + +import de.bstly.we.partey.model.ParteyUserReport; +import de.bstly.we.partey.model.QParteyUserReport; +import de.bstly.we.partey.repository.ParteyUserReportRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class ParteyUserReportManager { + + @Autowired + private ParteyUserReportRepository parteyUserReportRepository; + @Autowired + private ParteyUserReportModeratorManager parteyUserReportModeratorManager; + private QParteyUserReport qParteyUserReport = QParteyUserReport.parteyUserReport; + + /** + * + * @param reporter + * @param user + * @param comment + * @param world + * @return + */ + public ParteyUserReport create(Long reporter, Long user, String comment, String world) { + ParteyUserReport parteyUserReport = new ParteyUserReport(); + + parteyUserReport.setReporter(reporter); + parteyUserReport.setUser(user); + parteyUserReport.setComment(comment); + parteyUserReport.setWorld(world); + parteyUserReport.setCreated(Instant.now()); + + parteyUserReport = parteyUserReportRepository.save(parteyUserReport); + + parteyUserReportModeratorManager.sendMail(parteyUserReport); + + return parteyUserReport; + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + PageRequest pageRequest = PageRequest.of(page, size, + descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending()); + + return parteyUserReportRepository.findAll(pageRequest); + } + + /** + * + * @param parteyUserReport + * @return + */ + public ParteyUserReport save(ParteyUserReport parteyUserReport) { + return parteyUserReportRepository.save(parteyUserReport); + } + + /** + * + * @param id + */ + public void delete(Long id) { + parteyUserReportRepository.deleteById(id); + } + + /** + * + */ + public void deleteAll() { + parteyUserReportRepository.deleteAll(); + } + + /** + * + * @param before + */ + public void deleteAllBefore(Instant before) { + parteyUserReportRepository.deleteAll( + parteyUserReportRepository.findAll(qParteyUserReport.created.before(before))); + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyUserReportModeratorManager.java b/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyUserReportModeratorManager.java new file mode 100644 index 0000000..cb9049e --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyUserReportModeratorManager.java @@ -0,0 +1,110 @@ +/** + * + */ +package de.bstly.we.partey.businesslogic; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; + +import com.beust.jcommander.internal.Lists; +import com.google.gson.JsonObject; + +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.email.businesslogic.EmailManager; +import de.bstly.we.i18n.businesslogic.I18nManager; +import de.bstly.we.model.User; +import de.bstly.we.partey.model.ParteyUserReport; +import de.bstly.we.partey.model.ParteyUserReportModerator; +import de.bstly.we.partey.model.QParteyUserReportModerator; +import de.bstly.we.partey.repository.ParteyUserReportModeratorRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class ParteyUserReportModeratorManager { + + @Autowired + private ParteyUserReportModeratorRepository parteyUserReportModeratorRepository; + @Autowired + private UserManager userManager; + @Autowired + private EmailManager emailManager; + @Autowired + private I18nManager i18nManager; + private QParteyUserReportModerator qParteyUserReportModerator = QParteyUserReportModerator.parteyUserReportModerator; + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, + boolean descending) { + PageRequest pageRequest = PageRequest.of(page, size, + descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending()); + + return parteyUserReportModeratorRepository.findAll(pageRequest); + } + + /** + * + * @param parteyUserReport + */ + public void sendMail(ParteyUserReport parteyUserReport) { + String subject = "User in partey reported"; + String textTemplate = "User '%s' reported '%s' on map '%s' at %s with following comment:\n\n'%s'"; + + JsonObject label = i18nManager.getLabel("en"); + if (label != null) { + if (label.has("partey.report.email.text")) { + textTemplate = label.get("partey.report.email.text").getAsString(); + } + if (label.has("partey.report.email.subject")) { + subject = label.get("partey.report.email.subject").getAsString(); + } + } + + User reporter = userManager.get(parteyUserReport.getReporter()); + User user = userManager.get(parteyUserReport.getUser()); + + String text = String.format(textTemplate, reporter.getUsername(), user.getUsername(), + parteyUserReport.getWorld(), parteyUserReport.getCreated(), + parteyUserReport.getComment()); + + List bcc = Lists.newArrayList(); + for (ParteyUserReportModerator parteyUserReportModerator : parteyUserReportModeratorRepository + .findAll(qParteyUserReportModerator.disabled.isFalse())) { + bcc.add(parteyUserReportModerator.getEmail()); + } + + emailManager.sendBcc(bcc.toArray(new String[] {}), "no-reply@prty.bstly.de", subject, text); + } + + /** + * + * @param parteyUserReportModerator + * @return + */ + public ParteyUserReportModerator save(ParteyUserReportModerator parteyUserReportModerator) { + return parteyUserReportModeratorRepository.save(parteyUserReportModerator); + } + + /** + * + * @param id + */ + public void delete(Long id) { + parteyUserReportModeratorRepository.deleteById(id); + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyUserTagManager.java b/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyUserTagManager.java new file mode 100644 index 0000000..a1bf3b3 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/businesslogic/ParteyUserTagManager.java @@ -0,0 +1,82 @@ +/** + * + */ +package de.bstly.we.partey.businesslogic; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; + +import com.google.common.collect.Lists; + +import de.bstly.we.partey.model.ParteyUserTag; +import de.bstly.we.partey.model.QParteyUserTag; +import de.bstly.we.partey.repository.ParteyUserTagRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class ParteyUserTagManager { + + @Autowired + private ParteyUserTagRepository parteyUserTagRepository; + private QParteyUserTag qParteyUserTag = QParteyUserTag.parteyUserTag; + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + PageRequest pageRequest = PageRequest.of(page, size, + descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending()); + + return parteyUserTagRepository.findAll(pageRequest); + } + + /** + * + * @param target + * @return + */ + public List getForTarget(long target) { + return Lists + .newArrayList(parteyUserTagRepository.findAll(qParteyUserTag.target.eq(target))); + } + + /** + * + * @param parteyUserTag + * @return + */ + public ParteyUserTag save(ParteyUserTag parteyUserTag) { + return parteyUserTagRepository.save(parteyUserTag); + } + + /** + * + * @param id + */ + public void delete(Long id) { + parteyUserTagRepository.deleteById(id); + } + + /** + * + * @param before + */ + public void deleteAllForTarget(Long target) { + parteyUserTagRepository + .deleteAll(parteyUserTagRepository.findAll(qParteyUserTag.target.eq(target))); + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/businesslogic/model/Room.java b/partey/src/main/java/de/bstly/we/partey/businesslogic/model/Room.java new file mode 100644 index 0000000..9bdce1e --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/businesslogic/model/Room.java @@ -0,0 +1,94 @@ +/** + * + */ +package de.bstly.we.partey.businesslogic.model; + +import java.util.List; + +import com.google.common.collect.Lists; + +import de.bstly.we.partey.model.GameRoomPolicyTypes; + +/** + * @author _bastler@bstly.de + * + */ +public class Room { + + private String url; + private String mapId; + private String mapUrl; + private GameRoomPolicyTypes policyType; + private List tags = Lists.newArrayList(); + + /** + * @return the url + */ + public String getUrl() { + return url; + } + + /** + * @param url the url to set + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * @return the mapId + */ + public String getMapId() { + return mapId; + } + + /** + * @param mapId the mapId to set + */ + public void setMapId(String mapId) { + this.mapId = mapId; + } + + /** + * @return the mapUrl + */ + public String getMapUrl() { + return mapUrl; + } + + /** + * @param mapUrl the mapUrl to set + */ + public void setMapUrl(String mapUrl) { + this.mapUrl = mapUrl; + } + + /** + * @return the policyType + */ + public GameRoomPolicyTypes getPolicyType() { + return policyType; + } + + /** + * @param policyType the policyType to set + */ + public void setPolicyType(GameRoomPolicyTypes policyType) { + this.policyType = policyType; + } + + /** + * @return the tags + */ + public List getTags() { + return tags; + } + + /** + * @param tags the tags to set + */ + public void setTags(List tags) { + this.tags = tags; + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/controller/MapManagementController.java b/partey/src/main/java/de/bstly/we/partey/controller/MapManagementController.java new file mode 100644 index 0000000..49e2ecd --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/controller/MapManagementController.java @@ -0,0 +1,90 @@ +/** + * + */ +package de.bstly.we.partey.controller; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.beust.jcommander.internal.Lists; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.partey.businesslogic.ParteyMapManager; +import de.bstly.we.partey.model.GameRoomPolicyTypes; +import de.bstly.we.partey.model.ParteyMap; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/partey/maps") +public class MapManagementController extends BaseController { + + @Autowired + private ParteyMapManager parteyMapManager; + + /** + * + * @param pageParameter + * @param sizeParameter + * @param sortParameter + * @param descParameter + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getParteyMaps(@RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter, + @RequestParam("sort") Optional sortParameter, + @RequestParam("desc") Optional descParameter) { + + Page page = parteyMapManager.get(pageParameter.orElse(0), + sizeParameter.orElse(10), sortParameter.orElse("id"), descParameter.orElse(false)); + + return page; + + } + + /** + * + * @param parteyMap + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public ParteyMap createOrUpdateParteyMap(@RequestBody ParteyMap parteyMap) { + if (parteyMap.getPolicyType() == null) { + parteyMap.setPolicyType(GameRoomPolicyTypes.MEMBERS_ONLY_POLICY); + } + + if (parteyMap.getTags() == null) { + parteyMap.setTags(Lists.newArrayList()); + } + + return parteyMapManager.save(parteyMap); + } + + /** + * + * @param id + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("{id}") + public void deleteParteyMap(@PathVariable("id") String id) { + parteyMapManager.delete(id); + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/controller/UserReportManagementController.java b/partey/src/main/java/de/bstly/we/partey/controller/UserReportManagementController.java new file mode 100644 index 0000000..9b39438 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/controller/UserReportManagementController.java @@ -0,0 +1,97 @@ +/** + * + */ +package de.bstly.we.partey.controller; + +import java.time.Instant; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.partey.businesslogic.ParteyUserReportManager; +import de.bstly.we.partey.model.ParteyUserReport; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/partey/reports") +public class UserReportManagementController extends BaseController { + + @Autowired + private ParteyUserReportManager parteyUserReportManager; + + /** + * + * @param pageParameter + * @param sizeParameter + * @param sortParameter + * @param descParameter + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getParteyUserReports( + @RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter, + @RequestParam("sort") Optional sortParameter, + @RequestParam("desc") Optional descParameter) { + + Page page = parteyUserReportManager.get(pageParameter.orElse(0), + sizeParameter.orElse(10), sortParameter.orElse("id"), descParameter.orElse(false)); + + return page; + + } + + /** + * + * @param parteyUserReport + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public ParteyUserReport createOrUpdateParteyUserReport( + @RequestBody ParteyUserReport parteyUserReport) { + return parteyUserReportManager.save(parteyUserReport); + } + + /** + * + * @param id + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("{id}") + public void deleteParteyUserReport(@PathVariable("id") Long id) { + parteyUserReportManager.delete(id); + } + + /** + * + * @param beforeParameter + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping() + public void deleteAllParteyUserReport( + @RequestParam("before") Optional beforeParameter) { + if (beforeParameter.isPresent()) { + parteyUserReportManager.deleteAllBefore(beforeParameter.get()); + } else { + parteyUserReportManager.deleteAll(); + } + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/controller/UserReportModeratorManagementController.java b/partey/src/main/java/de/bstly/we/partey/controller/UserReportModeratorManagementController.java new file mode 100644 index 0000000..01fae43 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/controller/UserReportModeratorManagementController.java @@ -0,0 +1,82 @@ +/** + * + */ +package de.bstly.we.partey.controller; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.partey.businesslogic.ParteyUserReportModeratorManager; +import de.bstly.we.partey.model.ParteyUserReportModerator; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/partey/reports/moderators") +public class UserReportModeratorManagementController extends BaseController { + + @Autowired + private ParteyUserReportModeratorManager parteyUserReportModeratorManager; + + /** + * + * @param pageParameter + * @param sizeParameter + * @param sortParameter + * @param descParameter + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getParteyUserReportModerators( + @RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter, + @RequestParam("sort") Optional sortParameter, + @RequestParam("desc") Optional descParameter) { + + Page page = parteyUserReportModeratorManager.get( + pageParameter.orElse(0), sizeParameter.orElse(10), sortParameter.orElse("id"), + descParameter.orElse(false)); + + return page; + + } + + /** + * + * @param parteyUserReport + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public ParteyUserReportModerator createOrUpdateParteyUserReportModerator( + @RequestBody ParteyUserReportModerator parteyUserReportModerator) { + return parteyUserReportModeratorManager.save(parteyUserReportModerator); + } + + /** + * + * @param id + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("{id}") + public void deleteParteyUserReportModerator(@PathVariable("id") Long id) { + parteyUserReportModeratorManager.delete(id); + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/controller/UserTagController.java b/partey/src/main/java/de/bstly/we/partey/controller/UserTagController.java new file mode 100644 index 0000000..81bd7dd --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/controller/UserTagController.java @@ -0,0 +1,41 @@ +/** + * + */ +package de.bstly.we.partey.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.partey.businesslogic.ParteyUserTagManager; +import de.bstly.we.partey.model.ParteyUserTag; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/partey/tags") +public class UserTagController extends BaseController { + + @Autowired + private ParteyUserTagManager parteyUserTagManager; + + /** + * + * @param target + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping + public List getParteyUserTagsForTarget() { + return parteyUserTagManager.getForTarget(getCurrentUserId()); + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/controller/UserTagManagementController.java b/partey/src/main/java/de/bstly/we/partey/controller/UserTagManagementController.java new file mode 100644 index 0000000..21fc2e0 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/controller/UserTagManagementController.java @@ -0,0 +1,104 @@ +/** + * + */ +package de.bstly.we.partey.controller; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.model.User; +import de.bstly.we.partey.businesslogic.ParteyUserTagManager; +import de.bstly.we.partey.model.ParteyUserTag; + +/** + * + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/partey/tags/manage") +public class UserTagManagementController extends BaseController { + + @Autowired + private ParteyUserTagManager parteyUserTagManager; + @Autowired + private UserManager userManager; + + /** + * + * @param pageParameter + * @param sizeParameter + * @param sortParameter + * @param descParameter + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getParteyUserTags( + @RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter, + @RequestParam("sort") Optional sortParameter, + @RequestParam("desc") Optional descParameter) { + + Page page = parteyUserTagManager.get(pageParameter.orElse(0), + sizeParameter.orElse(10), sortParameter.orElse("id"), descParameter.orElse(false)); + + return page; + } + + /** + * + * @param target + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("{username}") + public List getParteyUserTagsForTarget( + @PathVariable("username") String username) { + User user = userManager.getByUsername(username); + + if (user == null) { + throw new EntityResponseStatusException(HttpStatus.NO_CONTENT); + } + + return parteyUserTagManager.getForTarget(user.getId()); + } + + /** + * + * @param parteyUserReport + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public ParteyUserTag createOrUpdateParteyUserTag(@RequestBody ParteyUserTag parteyUserTag) { + return parteyUserTagManager.save(parteyUserTag); + } + + /** + * + * @param id + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("{id}") + public void deleteParteyUserTag(@PathVariable("id") Long id) { + parteyUserTagManager.delete(id); + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/model/GameRoomPolicyTypes.java b/partey/src/main/java/de/bstly/we/partey/model/GameRoomPolicyTypes.java new file mode 100644 index 0000000..1456428 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/model/GameRoomPolicyTypes.java @@ -0,0 +1,16 @@ +/** + * + */ +package de.bstly.we.partey.model; + +import com.fasterxml.jackson.annotation.JsonFormat; + +/** + * + * @author _bastler@bstly.de + * + */ +@JsonFormat(shape = JsonFormat.Shape.NUMBER) +public enum GameRoomPolicyTypes { + UNKNOWN, ANONYMOUS_POLICY, MEMBERS_ONLY_POLICY, USE_TAGS_POLICY +} diff --git a/partey/src/main/java/de/bstly/we/partey/model/ParteyMap.java b/partey/src/main/java/de/bstly/we/partey/model/ParteyMap.java new file mode 100644 index 0000000..d95a7cb --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/model/ParteyMap.java @@ -0,0 +1,80 @@ +/** + * + */ +package de.bstly.we.partey.model; + +import java.util.List; + +import javax.persistence.CollectionTable; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; + +import com.google.common.collect.Lists; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "partey_maps") +public class ParteyMap { + + @Id + private String id; + @Enumerated(EnumType.STRING) + private GameRoomPolicyTypes policyType = GameRoomPolicyTypes.MEMBERS_ONLY_POLICY; + @ElementCollection + @LazyCollection(LazyCollectionOption.FALSE) + @CollectionTable(name = "partey_maps_tags") + private List tags = Lists.newArrayList(); + + /** + * @return the id + */ + public String getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(String id) { + this.id = id; + } + + /** + * @return the policyType + */ + public GameRoomPolicyTypes getPolicyType() { + return policyType; + } + + /** + * @param policyType the policyType to set + */ + public void setPolicyType(GameRoomPolicyTypes policyType) { + this.policyType = policyType; + } + + /** + * @return the tags + */ + public List getTags() { + return tags; + } + + /** + * @param tags the tags to set + */ + public void setTags(List tags) { + this.tags = tags; + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/model/ParteyUserReport.java b/partey/src/main/java/de/bstly/we/partey/model/ParteyUserReport.java new file mode 100644 index 0000000..8276715 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/model/ParteyUserReport.java @@ -0,0 +1,117 @@ +/** + * + */ +package de.bstly.we.partey.model; + +import java.time.Instant; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Lob; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +public class ParteyUserReport { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + private Long reporter; + private Long user; + @Lob + private String comment; + private String world; + private Instant created; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the reporter + */ + public Long getReporter() { + return reporter; + } + + /** + * @param reporter the reporter to set + */ + public void setReporter(Long reporter) { + this.reporter = reporter; + } + + /** + * @return the user + */ + public Long getUser() { + return user; + } + + /** + * @param user the user to set + */ + public void setUser(Long user) { + this.user = user; + } + + /** + * @return the comment + */ + public String getComment() { + return comment; + } + + /** + * @param comment the comment to set + */ + public void setComment(String comment) { + this.comment = comment; + } + + /** + * @return the world + */ + public String getWorld() { + return world; + } + + /** + * @param world the world to set + */ + public void setWorld(String world) { + this.world = world; + } + + /** + * @return the created + */ + public Instant getCreated() { + return created; + } + + /** + * @param created the created to set + */ + public void setCreated(Instant created) { + this.created = created; + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/model/ParteyUserReportModerator.java b/partey/src/main/java/de/bstly/we/partey/model/ParteyUserReportModerator.java new file mode 100644 index 0000000..294f54c --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/model/ParteyUserReportModerator.java @@ -0,0 +1,69 @@ +/** + * + */ +package de.bstly.we.partey.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +public class ParteyUserReportModerator { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + private String email; + @Column(columnDefinition = "boolean default false") + private boolean disabled; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the email + */ + public String getEmail() { + return email; + } + + /** + * @param email the email to set + */ + public void setEmail(String email) { + this.email = email; + } + + /** + * @return the disabled + */ + public boolean isDisabled() { + return disabled; + } + + /** + * @param disabled the disabled to set + */ + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/model/ParteyUserTag.java b/partey/src/main/java/de/bstly/we/partey/model/ParteyUserTag.java new file mode 100644 index 0000000..5282b23 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/model/ParteyUserTag.java @@ -0,0 +1,68 @@ +/** + * + */ +package de.bstly.we.partey.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +public class ParteyUserTag { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + private Long target; + private String tag; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the target + */ + public Long getTarget() { + return target; + } + + /** + * @param target the target to set + */ + public void setTarget(Long target) { + this.target = target; + } + + /** + * @return the tag + */ + public String getTag() { + return tag; + } + + /** + * @param tag the tag to set + */ + public void setTag(String tag) { + this.tag = tag; + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/repository/ParteyMapRepository.java b/partey/src/main/java/de/bstly/we/partey/repository/ParteyMapRepository.java new file mode 100755 index 0000000..15452a6 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/repository/ParteyMapRepository.java @@ -0,0 +1,20 @@ +/** + * + */ +package de.bstly.we.partey.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.partey.model.ParteyMap; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface ParteyMapRepository + extends JpaRepository, QuerydslPredicateExecutor { +} diff --git a/partey/src/main/java/de/bstly/we/partey/repository/ParteyUserReportModeratorRepository.java b/partey/src/main/java/de/bstly/we/partey/repository/ParteyUserReportModeratorRepository.java new file mode 100644 index 0000000..5649c57 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/repository/ParteyUserReportModeratorRepository.java @@ -0,0 +1,21 @@ +/** + * + */ +package de.bstly.we.partey.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.partey.model.ParteyUserReportModerator; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface ParteyUserReportModeratorRepository + extends JpaRepository, + QuerydslPredicateExecutor { +} diff --git a/partey/src/main/java/de/bstly/we/partey/repository/ParteyUserReportRepository.java b/partey/src/main/java/de/bstly/we/partey/repository/ParteyUserReportRepository.java new file mode 100644 index 0000000..60e603f --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/repository/ParteyUserReportRepository.java @@ -0,0 +1,20 @@ +/** + * + */ +package de.bstly.we.partey.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.partey.model.ParteyUserReport; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface ParteyUserReportRepository + extends JpaRepository, QuerydslPredicateExecutor { +} diff --git a/partey/src/main/java/de/bstly/we/partey/repository/ParteyUserTagRepository.java b/partey/src/main/java/de/bstly/we/partey/repository/ParteyUserTagRepository.java new file mode 100644 index 0000000..d0bcf01 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/repository/ParteyUserTagRepository.java @@ -0,0 +1,20 @@ +/** + * + */ +package de.bstly.we.partey.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.partey.model.ParteyUserTag; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface ParteyUserTagRepository + extends JpaRepository, QuerydslPredicateExecutor { +} diff --git a/partey/src/main/java/de/bstly/we/partey/timeslot/businesslogic/TimeslotManager.java b/partey/src/main/java/de/bstly/we/partey/timeslot/businesslogic/TimeslotManager.java new file mode 100644 index 0000000..46e9ba3 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/timeslot/businesslogic/TimeslotManager.java @@ -0,0 +1,185 @@ +/** + * + */ +package de.bstly.we.partey.timeslot.businesslogic; + +import java.time.Instant; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.google.common.collect.Lists; +import com.querydsl.core.BooleanBuilder; + +import de.bstly.we.businesslogic.UserDataProvider; +import de.bstly.we.model.UserData; +import de.bstly.we.model.Visibility; +import de.bstly.we.partey.timeslot.model.QTimeslot; +import de.bstly.we.partey.timeslot.model.Timeslot; +import de.bstly.we.partey.timeslot.model.TimeslotType; +import de.bstly.we.partey.timeslot.repository.TimeslotRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class TimeslotManager implements UserDataProvider { + + public static final String TIMESLOT_MINUTES = "timeslot.minutes"; + public static final long TIMESLOT_MINUTES_DEFAULT = 480; // 8h + public static final String TIMESLOT_TOLERANCE = "timeslot.tolerance"; + public static final long TIMESLOT_TOLERANCE_DEFAULT = 5; + + @Autowired + private TimeslotRepository timeslotRepository; + private QTimeslot qTimeslot = QTimeslot.timeslot; + + /** + * + * @param id + * @return + */ + public Timeslot get(Long id) { + return timeslotRepository.findById(id).orElse(null); + } + + /** + * + * @param owner + * @return + */ + public List getAllByOwner(Long owner) { + return Lists.newArrayList(timeslotRepository.findAll(qTimeslot.owner.eq(owner))); + } + + /** + * + * @param owner + * @param after + * @param type + * @param visibility + * @param search + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(Long owner, boolean invertOwner, Instant after, TimeslotType type, + Visibility visibility, String search, int page, int size, String sortBy, + boolean descending) { + + PageRequest pageRequest = PageRequest.of(page, size, + descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending()); + + BooleanBuilder query = new BooleanBuilder(); + + if (owner != null) { + if (invertOwner) { + query.and(qTimeslot.owner.ne(owner)); + } else { + query.and(qTimeslot.owner.eq(owner)); + } + } + + if (after != null) { + query.and(qTimeslot.starts.after(after).or(qTimeslot.ends.after(after))); + } + + if (type != null) { + query.and(qTimeslot.type.eq(type)); + } + + if (visibility != null) { + query.and(qTimeslot.visibility.eq(visibility)); + } + + if (StringUtils.hasText(search)) { + query.and(qTimeslot.title.containsIgnoreCase(search) + .or(qTimeslot.description.containsIgnoreCase(search))); + } + + if (query.hasValue()) { + return timeslotRepository.findAll(query.getValue(), pageRequest); + } + + return timeslotRepository.findAll(pageRequest); + } + + /** + * + * @param owner + * @return + */ + public long quota(Long owner) { + return timeslotRepository.count(qTimeslot.owner.eq(owner).and( + qTimeslot.starts.after(Instant.now()).or(qTimeslot.ends.after(Instant.now())))); + } + + /** + * + * @param timeslot + * @return + */ + public Timeslot save(Timeslot timeslot) { + Assert.isTrue(timeslot.getStarts().isBefore(timeslot.getEnds()), + "starts must be before ends."); + return timeslotRepository.save(timeslot); + } + + /** + * + * @param id + */ + public void delete(Long id) { + timeslotRepository.deleteById(id); + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getId() + */ + @Override + public String getId() { + return "partey-timeslots"; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getUserData(java.lang.Long) + */ + @Override + public List getUserData(Long userId) { + List result = Lists.newArrayList(); + for (Timeslot timeslot : getAllByOwner(userId)) { + result.add(timeslot); + } + return result; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#purgeUserData(java.lang.Long) + */ + @Override + public void purgeUserData(Long userId) { + for (Timeslot timeslot : getAllByOwner(userId)) { + timeslotRepository.delete(timeslot); + } + } + + /** + * + */ + @Scheduled(cron = "0 */5 * * * *") + protected void clearEndedTimeslots() { + timeslotRepository.deleteAll(timeslotRepository + .findAll(qTimeslot.clearAfter.isTrue().and(qTimeslot.ends.before(Instant.now())))); + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/timeslot/controller/TimeslotController.java b/partey/src/main/java/de/bstly/we/partey/timeslot/controller/TimeslotController.java new file mode 100644 index 0000000..05cee37 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/timeslot/controller/TimeslotController.java @@ -0,0 +1,233 @@ +/** + * + */ +package de.bstly.we.partey.timeslot.controller; + +import java.time.Instant; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.businesslogic.UserManager; +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.model.Quota; +import de.bstly.we.model.Visibility; +import de.bstly.we.partey.businesslogic.ParteyPermissions; +import de.bstly.we.partey.businesslogic.ParteyQuotas; +import de.bstly.we.partey.timeslot.businesslogic.TimeslotManager; +import de.bstly.we.partey.timeslot.controller.validation.TimeslotValidator; +import de.bstly.we.partey.timeslot.model.Timeslot; +import de.bstly.we.partey.timeslot.model.TimeslotType; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/partey/timeslots") +public class TimeslotController extends BaseController { + + @Autowired + private TimeslotManager timeslotManager; + @Autowired + private TimeslotValidator timeslotValidator; + @Autowired + private PermissionManager permissionManager; + @Autowired + private QuotaManager quotaManager; + @Autowired + private UserManager userManager; + + /** + * + * @param pageParameter + * @param sizeParameter + * @param sortParameter + * @param descParameter + * @param ownerParameter + * @param afterParameter + * @param typeParameter + * @param visibilityParameter + * @param searchParameter + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping + public Page getTimeslots(@RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter, + @RequestParam("sort") Optional sortParameter, + @RequestParam("desc") Optional descParameter, + @RequestParam("owner") Optional ownerParameter, + @RequestParam("after") Optional afterParameter, + @RequestParam("type") Optional typeParameter, + @RequestParam("visibility") Optional visibilityParameter, + @RequestParam("search") Optional searchParameter) { + + if (!permissionManager.hasPermission(getCurrentUserId(), ParteyPermissions.PARTEY) + || !permissionManager.isFullUser(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + Long ownerId = getCurrentUserId(); + + if (ownerParameter.isPresent() && "all".equals(ownerParameter.get())) { + ownerId = null; + } + + Page page = timeslotManager.get(ownerId, + ownerParameter.isPresent() && "others".equals(ownerParameter.get()), + afterParameter.orElse(null), typeParameter.orElse(null), + visibilityParameter.orElse(null), searchParameter.orElse(null), + pageParameter.orElse(0), sizeParameter.orElse(10), sortParameter.orElse("id"), + descParameter.orElse(false)); + + for (Timeslot timeslot : page.getContent()) { + if (!getCurrentUserId().equals(timeslot.getOwner())) { + switch (timeslot.getVisibility()) { + case PRIVATE: + timeslot.setUsername(null); + timeslot.setTitle(null); + timeslot.setDescription(null); + break; + default: + timeslot.setUsername(userManager.get(timeslot.getOwner()).getUsername()); + break; + + } + timeslot.setSecret(null); + timeslot.setStream(null); + timeslot.setOwner(null); + } + } + + return page; + + } + + /** + * + * @param timeslot + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping("/quota") + public long quota() { + Quota timeslotQuota = quotaManager.get(getCurrentUserId(), ParteyQuotas.PARTEY_TIMESLOT); + return timeslotQuota == null ? 0 + : timeslotQuota.getValue() - timeslotManager.quota(getCurrentUserId()); + } + + /** + * + * @param timeslot + * @return + */ + @PreAuthorize("isAuthenticated()") + @PostMapping + public Timeslot createTimeslot(@RequestBody Timeslot timeslot) { + if (!permissionManager.hasPermission(getCurrentUserId(), ParteyPermissions.PARTEY) + || !permissionManager.isFullUser(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + Quota timeslotQuota = quotaManager.get(getCurrentUserId(), ParteyQuotas.PARTEY_TIMESLOT); + if (timeslotQuota == null + || timeslotQuota.getValue() < timeslotManager.quota(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + timeslot.setId(null); + timeslot.setOwner(getCurrentUserId()); + Errors errors = new RequestBodyErrors(timeslot); + timeslotValidator.validate(timeslot, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + if (timeslot.getVisibility() == null) { + timeslot.setVisibility(Visibility.PROTECTED); + } + + return timeslotManager.save(timeslot); + } + + /** + * + * @param timeslot + * @return + */ + @PreAuthorize("isAuthenticated()") + @PatchMapping + public Timeslot updateTimeslot(@RequestBody Timeslot timeslot) { + if (!permissionManager.hasPermission(getCurrentUserId(), ParteyPermissions.PARTEY) + || !permissionManager.isFullUser(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + if (timeslot.getId() == null) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + Timeslot existing = timeslotManager.get(timeslot.getId()); + if (existing == null || !getCurrentUserId().equals(existing.getOwner()) + || !permissionManager.isFullUser(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + timeslot.setOwner(getCurrentUserId()); + Errors errors = new RequestBodyErrors(timeslot); + timeslotValidator.validate(timeslot, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + if (timeslot.getStarts().isAfter(Instant.now()) + || timeslot.getEnds().isAfter(Instant.now())) { + Quota timeslotQuota = quotaManager.get(getCurrentUserId(), + ParteyQuotas.PARTEY_TIMESLOT); + if (timeslotQuota == null + || timeslotQuota.getValue() < timeslotManager.quota(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + } + + if (timeslot.getVisibility() == null) { + timeslot.setVisibility(Visibility.PROTECTED); + } + + return timeslotManager.save(timeslot); + } + + /** + * + * @param id + */ + @PreAuthorize("isAuthenticated()") + @DeleteMapping("{id}") + public void deleteTimeslot(@PathVariable("id") Long id) { + Timeslot existing = timeslotManager.get(id); + if (existing == null || !getCurrentUserId().equals(existing.getOwner())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + timeslotManager.delete(id); + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/timeslot/controller/TimeslotManagementController.java b/partey/src/main/java/de/bstly/we/partey/timeslot/controller/TimeslotManagementController.java new file mode 100644 index 0000000..a5f7c10 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/timeslot/controller/TimeslotManagementController.java @@ -0,0 +1,112 @@ +/** + * + */ +package de.bstly.we.partey.timeslot.controller; + +import java.time.Instant; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.model.Visibility; +import de.bstly.we.partey.timeslot.businesslogic.TimeslotManager; +import de.bstly.we.partey.timeslot.controller.validation.TimeslotValidator; +import de.bstly.we.partey.timeslot.model.Timeslot; +import de.bstly.we.partey.timeslot.model.TimeslotType; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/partey/timeslots/manage") +public class TimeslotManagementController extends BaseController { + + @Autowired + private TimeslotManager timeslotManager; + @Autowired + private TimeslotValidator timeslotValidator; + + /** + * + * @param pageParameter + * @param sizeParameter + * @param sortParameter + * @param descParameter + * @param afterParameter + * @param typeParameter + * @param visibilityParameter + * @param searchParameter + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getTimeslots(@RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter, + @RequestParam("sort") Optional sortParameter, + @RequestParam("desc") Optional descParameter, + @RequestParam("owner") Optional ownerParameter, + @RequestParam("owner-invert") Optional invertOwnerParameter, + @RequestParam("after") Optional afterParameter, + @RequestParam("type") Optional typeParameter, + @RequestParam("visibility") Optional visibilityParameter, + @RequestParam("search") Optional searchParameter) { + + Page page = timeslotManager.get(ownerParameter.orElse(null), + invertOwnerParameter.isPresent() && invertOwnerParameter.get(), + afterParameter.orElse(null), typeParameter.orElse(null), + visibilityParameter.orElse(null), searchParameter.orElse(null), + pageParameter.orElse(0), sizeParameter.orElse(10), sortParameter.orElse("id"), + descParameter.orElse(false)); + + return page; + + } + + /** + * + * @param timeslot + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public Timeslot createOrUpdateTimeslot(@RequestBody Timeslot timeslot) { + Errors errors = new RequestBodyErrors(timeslot); + timeslotValidator.validate(timeslot, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + if (timeslot.getVisibility() == null) { + timeslot.setVisibility(Visibility.PROTECTED); + } + + return timeslotManager.save(timeslot); + } + + /** + * + * @param id + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("{id}") + public void deleteTimeslot(@PathVariable("id") Long id) { + timeslotManager.delete(id); + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/timeslot/controller/validation/TimeslotValidator.java b/partey/src/main/java/de/bstly/we/partey/timeslot/controller/validation/TimeslotValidator.java new file mode 100644 index 0000000..bb3ad47 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/timeslot/controller/validation/TimeslotValidator.java @@ -0,0 +1,165 @@ +/** + * + */ +package de.bstly.we.partey.timeslot.controller.validation; + +import java.time.temporal.ChronoUnit; + +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import com.querydsl.core.BooleanBuilder; + +import de.bstly.we.businesslogic.SystemPropertyManager; +import de.bstly.we.partey.timeslot.businesslogic.TimeslotManager; +import de.bstly.we.partey.timeslot.model.QTimeslot; +import de.bstly.we.partey.timeslot.model.Timeslot; +import de.bstly.we.partey.timeslot.repository.TimeslotRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class TimeslotValidator implements Validator { + + public final static int STREAM_SECRET_LENGTH = 64; + + @Autowired + private TimeslotRepository timeslotRepository; + @Autowired + private SystemPropertyManager systemPropertyManager; + private QTimeslot qTimeslot = QTimeslot.timeslot; + + /* + * @see org.springframework.validation.Validator#supports(java.lang.Class) + */ + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(Timeslot.class); + } + + /* + * @see org.springframework.validation.Validator#validate(java.lang.Object, + * org.springframework.validation.Errors) + */ + @Override + public void validate(Object target, Errors errors) { + Timeslot timeslot = (Timeslot) target; + + validateType(timeslot, errors); + validateTime(timeslot, errors); + } + + /** + * @param timeslot + * @param errors + */ + public void validateTime(Timeslot timeslot, Errors errors) { + if (timeslot.getStarts() == null) { + errors.rejectValue("starts", "REQUIRED"); + } + + if (timeslot.getEnds() == null) { + errors.rejectValue("ends", "REQUIRED"); + } + + if (timeslot.getStarts() != null && timeslot.getEnds() != null) { + if (timeslot.getStarts().isAfter(timeslot.getStarts())) { + errors.rejectValue("starts", "NOT_VALID"); + errors.rejectValue("ends", "NOT_VALID"); + } else { + BooleanBuilder timeQuery = new BooleanBuilder(); + + // same type + timeQuery.and(qTimeslot.type.eq(timeslot.getType())); + + // ends after start + timeQuery + .and(qTimeslot.ends.after(timeslot.getStarts().minus( + systemPropertyManager.getLong(TimeslotManager.TIMESLOT_TOLERANCE, + TimeslotManager.TIMESLOT_TOLERANCE_DEFAULT), + ChronoUnit.MINUTES))); + + // starts before end + timeQuery + .and(qTimeslot.starts.before(timeslot.getEnds().plus( + systemPropertyManager.getLong(TimeslotManager.TIMESLOT_TOLERANCE, + TimeslotManager.TIMESLOT_TOLERANCE_DEFAULT), + ChronoUnit.MINUTES))); + + if (timeslot.getId() != null && timeslotRepository.existsById(timeslot.getId())) { + timeQuery.and(qTimeslot.id.ne(timeslot.getId())); + } + + if (timeslotRepository.exists(timeQuery.getValue())) { + errors.rejectValue("starts", "ALREADY_EXISTS"); + errors.rejectValue("ends", "ALREADY_EXISTS"); + } else if (((timeslot.getEnds().getEpochSecond() + - timeslot.getStarts().getEpochSecond()) / 60) > systemPropertyManager + .getLong(TimeslotManager.TIMESLOT_MINUTES, + TimeslotManager.TIMESLOT_MINUTES_DEFAULT)) { + errors.rejectValue("ends", "TOO_LONG", + String.valueOf( + systemPropertyManager.getLong(TimeslotManager.TIMESLOT_MINUTES, + TimeslotManager.TIMESLOT_MINUTES_DEFAULT))); + } + } + } + } + + /** + * @param timeslot + * @param errors + */ + public void validateType(Timeslot timeslot, Errors errors) { + if (timeslot.getId() != null && timeslotRepository.existsById(timeslot.getId())) { + Timeslot existing = timeslotRepository.findById(timeslot.getId()).get(); + timeslot.setType(existing.getType()); + } + + switch (timeslot.getType()) { + case AUDIO: + validateTypeAudio(timeslot, errors); + break; + case VIDEO: + validateTypeVideo(timeslot, errors); + break; + } + + } + + /** + * + * @param timeslot + * @param errors + */ + public void validateTypeAudio(Timeslot timeslot, Errors errors) { + timeslot.setStream(null); + if (timeslot.getId() != null && timeslotRepository.existsById(timeslot.getId())) { + Timeslot existing = timeslotRepository.findById(timeslot.getId()).get(); + timeslot.setSecret(existing.getSecret()); + } + + if (!StringUtils.hasText(timeslot.getSecret())) { + timeslot.setSecret(RandomStringUtils.random(STREAM_SECRET_LENGTH, true, true)); + } + } + + /** + * + * @param timeslot + * @param errors + */ + public void validateTypeVideo(Timeslot timeslot, Errors errors) { + timeslot.setSecret(null); + if (!StringUtils.hasText(timeslot.getStream())) { + errors.rejectValue("stream", "REQUIRED"); + } + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/timeslot/model/Timeslot.java b/partey/src/main/java/de/bstly/we/partey/timeslot/model/Timeslot.java new file mode 100644 index 0000000..9bd12ad --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/timeslot/model/Timeslot.java @@ -0,0 +1,227 @@ +/** + * + */ +package de.bstly.we.partey.timeslot.model; + +import java.time.Instant; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; +import javax.persistence.Transient; + +import de.bstly.we.model.UserData; +import de.bstly.we.model.Visibility; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "timeslots") +public class Timeslot implements UserData { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + @Column(name = "owner", nullable = false) + private Long owner; + @Column(name = "type", nullable = false) + @Enumerated(EnumType.STRING) + private TimeslotType type; + @Column(name = "starts", nullable = false) + private Instant starts; + @Column(name = "ends", nullable = false) + private Instant ends; + @Column(name = "visibility", nullable = false) + private Visibility visibility; + @Column(name = "title", nullable = true) + private String title; + @Column(name = "description", nullable = true) + @Lob + private String description; + @Column(name = "stream", nullable = true) + private String stream; + @Column(name = "secret", nullable = true) + private String secret; + @Column(name = "clear_after", columnDefinition = "boolean default false") + private boolean clearAfter; + @Transient + private String username; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the owner + */ + public Long getOwner() { + return owner; + } + + /** + * @param owner the owner to set + */ + public void setOwner(Long owner) { + this.owner = owner; + } + + /** + * @return the type + */ + public TimeslotType getType() { + return type; + } + + /** + * @param type the type to set + */ + public void setType(TimeslotType type) { + this.type = type; + } + + /** + * @return the starts + */ + public Instant getStarts() { + return starts; + } + + /** + * @param starts the starts to set + */ + public void setStarts(Instant starts) { + this.starts = starts; + } + + /** + * @return the ends + */ + public Instant getEnds() { + return ends; + } + + /** + * @param ends the ends to set + */ + public void setEnds(Instant ends) { + this.ends = ends; + } + + /** + * @return the visibility + */ + public Visibility getVisibility() { + return visibility; + } + + /** + * @param visibility the visibility to set + */ + public void setVisibility(Visibility visibility) { + this.visibility = visibility; + } + + /** + * @return the title + */ + public String getTitle() { + return title; + } + + /** + * @param title the title to set + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @param description the description to set + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * @return the stream + */ + public String getStream() { + return stream; + } + + /** + * @param stream the stream to set + */ + public void setStream(String stream) { + this.stream = stream; + } + + /** + * @return the secret + */ + public String getSecret() { + return secret; + } + + /** + * @param secret the secret to set + */ + public void setSecret(String secret) { + this.secret = secret; + } + + /** + * @return the clearAfter + */ + public boolean isClearAfter() { + return clearAfter; + } + + /** + * @param clearAfter the clearAfter to set + */ + public void setClearAfter(boolean clearAfter) { + this.clearAfter = clearAfter; + } + + /** + * @return the username + */ + public String getUsername() { + return username; + } + + /** + * @param username the username to set + */ + public void setUsername(String username) { + this.username = username; + } + +} diff --git a/partey/src/main/java/de/bstly/we/partey/timeslot/model/TimeslotType.java b/partey/src/main/java/de/bstly/we/partey/timeslot/model/TimeslotType.java new file mode 100644 index 0000000..e48a965 --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/timeslot/model/TimeslotType.java @@ -0,0 +1,12 @@ +/** + * + */ +package de.bstly.we.partey.timeslot.model; + +/** + * @author _bastler@bstly.de + * + */ +public enum TimeslotType { + VIDEO, AUDIO +} diff --git a/partey/src/main/java/de/bstly/we/partey/timeslot/repository/TimeslotRepository.java b/partey/src/main/java/de/bstly/we/partey/timeslot/repository/TimeslotRepository.java new file mode 100755 index 0000000..5c5d9ff --- /dev/null +++ b/partey/src/main/java/de/bstly/we/partey/timeslot/repository/TimeslotRepository.java @@ -0,0 +1,20 @@ +/** + * + */ +package de.bstly.we.partey.timeslot.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.partey.timeslot.model.Timeslot; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface TimeslotRepository + extends JpaRepository, QuerydslPredicateExecutor { +} diff --git a/pom.xml b/pom.xml new file mode 100755 index 0000000..7648992 --- /dev/null +++ b/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + de.bstly.we + webstly-main + main + pom + ${revision} + + + UTF-8 + 11 + 1.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.5 + + + + + application + core + email + i18n + invite + jitsi + membership + minetest + oidc + partey + services + urlshortener + wireguard + + + + + + com.mysema.maven + apt-maven-plugin + 1.1.3 + + + + process + + + target/generated-sources/java + com.querydsl.apt.jpa.JPAAnnotationProcessor + + + + + + + diff --git a/services/pom.xml b/services/pom.xml new file mode 100644 index 0000000..2d9f9d9 --- /dev/null +++ b/services/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + de.bstly.we + webstly-main + ${revision} + + + services + webstly-services + + + + de.bstly.we + webstly-core + ${revision} + + + de.bstly.we + webstly-oidc + ${revision} + + + diff --git a/services/src/main/java/de/bstly/we/services/businesslogic/ServiceManager.java b/services/src/main/java/de/bstly/we/services/businesslogic/ServiceManager.java new file mode 100644 index 0000000..2518710 --- /dev/null +++ b/services/src/main/java/de/bstly/we/services/businesslogic/ServiceManager.java @@ -0,0 +1,138 @@ +/** + * + */ +package de.bstly.we.services.businesslogic; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; + +import com.google.common.collect.Lists; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.Permissions; +import de.bstly.we.model.Permission; +import de.bstly.we.oidc.model.OidcClient; +import de.bstly.we.oidc.model.QOidcClient; +import de.bstly.we.oidc.repository.OidcClientRepository; +import de.bstly.we.services.model.QService; +import de.bstly.we.services.model.Service; +import de.bstly.we.services.repository.ServiceRepository; + +/** + * + * @author _bastler@bstly.de + * + */ +@Component +public class ServiceManager { + + @Autowired + private ServiceRepository serviceRepository; + @Autowired + private OidcClientRepository oidcClientRepository; + @Autowired + private PermissionManager permissionManager; + private QOidcClient qOidcClient = QOidcClient.oidcClient; + private QService qService = QService.service; + + /** + * + * @param name + * @return + */ + public Service get(String name) { + return serviceRepository.findById(name).orElse(null); + } + + /** + * + * @param service + * @return + */ + public Service update(Service service) { + return serviceRepository.save(service); + } + + /** + * + * @param target + * @return + */ + public List getForTarget(Long target) { + List services = Lists.newArrayList(); + + if (target == null) { + return services; + } + + if (permissionManager.hasPermission(target, Permissions.ROLE_ADMIN)) { + services.addAll(serviceRepository.findAll()); + for (OidcClient client : oidcClientRepository + .findAll(qOidcClient.loginUrl.isNotNull())) { + Service service = new Service(client.getClientName(), client.getLoginUrl()); + service.setCategory(client.getCategory()); + services.add(service); + } + return services; + } + + for (Permission permission : permissionManager.getNotExpiresByTarget(target)) { + if (serviceRepository.existsById(permission.getName())) { + services.add(get(permission.getName())); + } else if (oidcClientRepository.exists(qOidcClient.clientName.eq(permission.getName()) + .and(qOidcClient.loginUrl.isNotNull()))) { + OidcClient client = oidcClientRepository + .findOne(qOidcClient.clientName.eq(permission.getName())).get(); + Service service = new Service(client.getClientName(), client.getLoginUrl()); + service.setCategory(client.getCategory()); + services.add(service); + } + + for (Service service : serviceRepository + .findAll(qService.permission.eq(permission.getName()))) { + services.add(service); + } + } + + if (permissionManager.isFullUser(target)) { + for (Service service : serviceRepository.findAll(qService.alwaysPermitted.isTrue())) { + services.add(service); + } + + for (OidcClient client : oidcClientRepository + .findAll(qOidcClient.alwaysPermitted.isTrue())) { + Service service = new Service(client.getClientName(), client.getLoginUrl()); + service.setCategory(client.getCategory()); + services.add(service); + } + } + + return services; + } + + /** + * + * @param name + */ + public void delete(String name) { + serviceRepository.deleteById(name); + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending) { + Sort sort = descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending(); + return serviceRepository.findAll(PageRequest.of(page, size, sort)); + } +} diff --git a/services/src/main/java/de/bstly/we/services/controller/ServiceController.java b/services/src/main/java/de/bstly/we/services/controller/ServiceController.java new file mode 100644 index 0000000..b87f700 --- /dev/null +++ b/services/src/main/java/de/bstly/we/services/controller/ServiceController.java @@ -0,0 +1,37 @@ +/** + * + */ +package de.bstly.we.services.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.services.businesslogic.ServiceManager; +import de.bstly.we.services.model.Service; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/services") +public class ServiceController extends BaseController { + + @Autowired + private ServiceManager serviceManager; + + /** + * + * @return + */ + @GetMapping + public List getServices() { + return serviceManager.getForTarget(getCurrentUserId()); + } + +} diff --git a/services/src/main/java/de/bstly/we/services/controller/ServiceManagementController.java b/services/src/main/java/de/bstly/we/services/controller/ServiceManagementController.java new file mode 100644 index 0000000..d8276d7 --- /dev/null +++ b/services/src/main/java/de/bstly/we/services/controller/ServiceManagementController.java @@ -0,0 +1,79 @@ +/** + * + */ +package de.bstly.we.services.controller; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.services.businesslogic.ServiceManager; +import de.bstly.we.services.model.Service; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/services/manage") +public class ServiceManagementController extends BaseController { + + @Autowired + private ServiceManager serviceManager; + + /** + * + * @param pageParameter + * @param sizeParameter + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getAllServices(@RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter) { + return serviceManager.get(pageParameter.orElse(0), sizeParameter.orElse(10), "name", true); + } + + /** + * + * @param service + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public Service createOrUpdateService(@RequestBody Service service) { + if (service.getName() == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + return serviceManager.update(service); + } + + /** + * + * @param service + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping + public void deleteService(@RequestBody Service service) { + if (serviceManager.get(service.getName()) == null) { + throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED); + + } + + serviceManager.delete(service.getName()); + } + +} diff --git a/services/src/main/java/de/bstly/we/services/model/Service.java b/services/src/main/java/de/bstly/we/services/model/Service.java new file mode 100644 index 0000000..ff9c03f --- /dev/null +++ b/services/src/main/java/de/bstly/we/services/model/Service.java @@ -0,0 +1,136 @@ +/** + * + */ +package de.bstly.we.services.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "services", uniqueConstraints = @UniqueConstraint(columnNames = { "name" })) +public class Service { + + @Id + @Column(name = "name", unique = true, nullable = false) + private String name; + @Column(name = "url") + private String url; + @Column(name = "always_permitted", columnDefinition = "boolean default false") + private boolean alwaysPermitted; + @Column(name = "same_site", columnDefinition = "boolean default false") + private boolean sameSite; + @Column(name = "permission", nullable = true) + private String permission; + @Column(name = "category") + private String category; + + /** + * + */ + public Service() { + super(); + } + + /** + * @param name + * @param url + */ + public Service(String name, String url) { + super(); + this.name = name; + this.url = url; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the url + */ + public String getUrl() { + return url; + } + + /** + * @param url the url to set + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * @return the alwaysPermitted + */ + public boolean isAlwaysPermitted() { + return alwaysPermitted; + } + + /** + * @param alwaysPermitted the alwaysPermitted to set + */ + public void setAlwaysPermitted(boolean alwaysPermitted) { + this.alwaysPermitted = alwaysPermitted; + } + + /** + * @return the sameSite + */ + public boolean isSameSite() { + return sameSite; + } + + /** + * @param sameSite the sameSite to set + */ + public void setSameSite(boolean sameSite) { + this.sameSite = sameSite; + } + + /** + * @return the permission + */ + public String getPermission() { + return permission; + } + + /** + * @param permission the permission to set + */ + public void setPermission(String permission) { + this.permission = permission; + } + + /** + * @return the category + */ + public String getCategory() { + return category; + } + + /** + * @param category the category to set + */ + public void setCategory(String category) { + this.category = category; + } + +} diff --git a/services/src/main/java/de/bstly/we/services/repository/ServiceRepository.java b/services/src/main/java/de/bstly/we/services/repository/ServiceRepository.java new file mode 100644 index 0000000..c061ac2 --- /dev/null +++ b/services/src/main/java/de/bstly/we/services/repository/ServiceRepository.java @@ -0,0 +1,20 @@ +/** + * + */ +package de.bstly.we.services.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.services.model.Service; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface ServiceRepository extends JpaRepository, QuerydslPredicateExecutor { + +} diff --git a/urlshortener/pom.xml b/urlshortener/pom.xml new file mode 100755 index 0000000..8f302ce --- /dev/null +++ b/urlshortener/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + de.bstly.we + webstly-main + ${revision} + + + urlshortener + webstly-urlshortener + + + + de.bstly.we + webstly-core + ${revision} + + + diff --git a/urlshortener/src/main/java/de/bstly/we/urlshortener/businesslogic/ShortenedUrlManager.java b/urlshortener/src/main/java/de/bstly/we/urlshortener/businesslogic/ShortenedUrlManager.java new file mode 100644 index 0000000..8878e43 --- /dev/null +++ b/urlshortener/src/main/java/de/bstly/we/urlshortener/businesslogic/ShortenedUrlManager.java @@ -0,0 +1,316 @@ +/** + * + */ +package de.bstly.we.urlshortener.businesslogic; + +import java.time.Instant; +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.google.common.collect.Lists; +import com.querydsl.core.BooleanBuilder; + +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.businesslogic.SystemPropertyManager; +import de.bstly.we.businesslogic.UserDataProvider; +import de.bstly.we.model.Quota; +import de.bstly.we.model.UserData; +import de.bstly.we.urlshortener.model.QShortenedUrl; +import de.bstly.we.urlshortener.model.ShortenedUrl; +import de.bstly.we.urlshortener.repository.ShortenedUrlRepository; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class ShortenedUrlManager implements SmartInitializingSingleton, UserDataProvider { + + @Autowired + private PasswordEncoder passwordEncoder; + @Autowired + private Environment environment; + @Autowired + private SystemPropertyManager systemPropertyManager; + @Autowired + private QuotaManager quotaManager; + @Autowired + private ShortenedUrlRepository shortenedUrlRepository; + private QShortenedUrl qShortenedUrl = QShortenedUrl.shortenedUrl; + + private int codeLength; + + public static final String SYSTEM_PROPERTY_URL_SHORTENER_CODE_LENGTH = "urlShortener.codeLength"; + public static final String SYSTEM_PROPERTY_URL_SHORTENER_LINK = "urlShortener.link"; + public static final String SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND = "urlShortener.notFoundRedirect"; + public static final String SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD = "urlShortener.passwordRedirect"; + + /* + * @see org.springframework.beans.factory.SmartInitializingSingleton# + * afterSingletonsInstantiated() + */ + @Override + public void afterSingletonsInstantiated() { + if (!systemPropertyManager.has(SYSTEM_PROPERTY_URL_SHORTENER_CODE_LENGTH)) { + systemPropertyManager.add(SYSTEM_PROPERTY_URL_SHORTENER_CODE_LENGTH, + environment.getProperty("we.bstly.urlShortener.codeLength", "6")); + } + if (!systemPropertyManager.has(SYSTEM_PROPERTY_URL_SHORTENER_LINK)) { + systemPropertyManager.add(SYSTEM_PROPERTY_URL_SHORTENER_LINK, + environment.getProperty("we.bstly.urlShortener.link", "")); + } + if (!systemPropertyManager.has(SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND)) { + systemPropertyManager.add(SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND, + environment.getProperty("we.bstly.urlShortener.notFoundRedirect", "")); + } + if (!systemPropertyManager.has(SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD)) { + systemPropertyManager.add(SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD, + environment.getProperty("we.bstly.urlShortener.passwordRedirect", "")); + } + + codeLength = systemPropertyManager.getInteger(SYSTEM_PROPERTY_URL_SHORTENER_CODE_LENGTH); + } + + /** + * + * @param code + * @return + */ + public ShortenedUrl get(String code) { + return shortenedUrlRepository.findById(code).orElse(null); + } + + /** + * + * @param userId + * @return + */ + public List getAllByOwner(Long userId) { + return Lists.newArrayList(shortenedUrlRepository.findAll(qShortenedUrl.owner.eq(userId))); + } + + /** + * + * @param page + * @param size + * @param sortBy + * @param descending + * @param search + * @return + */ + public Page get(int page, int size, String sortBy, boolean descending, + String search) { + if (StringUtils.hasText(search)) { + return shortenedUrlRepository.findAll( + qShortenedUrl.note.containsIgnoreCase(search) + .or(qShortenedUrl.url.containsIgnoreCase(search)) + .or(qShortenedUrl.link.containsIgnoreCase(search)), + PageRequest.of(page, size, descending ? Sort.by(sortBy).descending() + : Sort.by(sortBy).ascending())); + } + + return shortenedUrlRepository.findAll(PageRequest.of(page, size, + descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending())); + } + + /** + * + * @param userId + * @param page + * @param size + * @param sortBy + * @param descending + * @param search + * @return + */ + public Page getForUserId(Long userId, int page, int size, String sortBy, + boolean descending, String search) { + // delete expired urls + shortenedUrlRepository.deleteAll(shortenedUrlRepository.findAll( + qShortenedUrl.owner.eq(userId).and(qShortenedUrl.expires.before(Instant.now())))); + + BooleanBuilder query = new BooleanBuilder(); + + query.and(qShortenedUrl.owner.eq(userId)); + + if (StringUtils.hasText(search)) { + query.and(qShortenedUrl.note.containsIgnoreCase(search) + .or(qShortenedUrl.url.containsIgnoreCase(search)) + .or(qShortenedUrl.link.containsIgnoreCase(search))); + } + + return shortenedUrlRepository.findAll(query.getValue(), PageRequest.of(page, size, + descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending())); + } + + /** + * + * @param owner + * @param url + * @param code + * @param expires + * @param password + * @param quota + * @return + */ + public ShortenedUrl create(Long owner, String url, String note, String code, Instant expires, + String password, boolean queryParameters, boolean quota) { + ShortenedUrl shortenedUrl = new ShortenedUrl(); + + shortenedUrl.setOwner(owner); + shortenedUrl.setUrl(url); + shortenedUrl.setNote(note); + + if (StringUtils.hasText(code)) { + Assert.isTrue(!shortenedUrlRepository.existsById(code), "Given code already exists!"); + } else { + code = RandomStringUtils.random(codeLength, true, true).toUpperCase(); + while (shortenedUrlRepository.existsById(code)) { + code = RandomStringUtils.random(codeLength, true, true).toUpperCase(); + } + } + + shortenedUrl.setCode(code); + shortenedUrl.setExpires(expires); + shortenedUrl.setQueryParameters(queryParameters); + + if (StringUtils.hasText(password)) { + shortenedUrl.setPasswordHash(passwordEncoder.encode(password)); + } + + String link = systemPropertyManager.get(SYSTEM_PROPERTY_URL_SHORTENER_LINK); + if (StringUtils.hasText(link)) { + shortenedUrl.setLink(String.format(link, shortenedUrl.getCode())); + } + + shortenedUrl = shortenedUrlRepository.save(shortenedUrl); + + if (quota) { + Quota shortenedUrlsQuota = quotaManager.get(shortenedUrl.getOwner(), + ShortenedUrlQuotas.URL_SHORTENER); + if (shortenedUrlsQuota != null) { + shortenedUrlsQuota.setValue(shortenedUrlsQuota.getValue() - 1); + quotaManager.update(shortenedUrlsQuota); + } + } + + return shortenedUrl; + } + + /** + * + * @param shortenedUrl + * @return + */ + public ShortenedUrl save(ShortenedUrl shortenedUrl) { + + String link = systemPropertyManager.get(SYSTEM_PROPERTY_URL_SHORTENER_LINK); + if (StringUtils.hasText(link)) { + shortenedUrl.setLink(String.format(link, shortenedUrl.getCode())); + } + + return shortenedUrlRepository.save(shortenedUrl); + } + + /** + * + * @param shortenedUrl + * @param quota + */ + public void delete(ShortenedUrl shortenedUrl, boolean quota) { + if (quota) { + Quota shortenedUrlsQuota = quotaManager.get(shortenedUrl.getOwner(), + ShortenedUrlQuotas.URL_SHORTENER); + if (shortenedUrlsQuota == null) { + shortenedUrlsQuota = quotaManager.create(shortenedUrl.getOwner(), + ShortenedUrlQuotas.URL_SHORTENER, 0, "#", true); + } + + shortenedUrlsQuota.setValue(shortenedUrlsQuota.getValue() + 1); + quotaManager.update(shortenedUrlsQuota); + } + + shortenedUrlRepository.delete(shortenedUrl); + } + + /** + * + * @param owner + * @param quota + */ + public void deleteAll(Long owner, boolean quota) { + List shortenedUrls = Lists + .newArrayList(shortenedUrlRepository.findAll(qShortenedUrl.owner.eq(owner))); + + for (ShortenedUrl shortenedUrl : shortenedUrls) { + delete(shortenedUrl, quota); + } + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getId() + */ + @Override + public String getId() { + return "shortend-urls"; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#getUserData(java.lang.Long) + */ + @Override + public List getUserData(Long userId) { + List result = Lists.newArrayList(); + for (ShortenedUrl shortenedUrl : getAllByOwner(userId)) { + result.add(shortenedUrl); + } + return result; + } + + /* + * @see de.bstly.we.businesslogic.UserDataProvider#purgeUserData(java.lang.Long) + */ + @Override + public void purgeUserData(Long userId) { + for (ShortenedUrl shortenedUrl : getAllByOwner(userId)) { + shortenedUrlRepository.delete(shortenedUrl); + } + } + + /** + * + */ + @Scheduled(cron = "0 */5 * * * *") + public void clearOrUpdateShortenedUrls() { + shortenedUrlRepository.deleteAll( + shortenedUrlRepository.findAll(qShortenedUrl.expires.before(Instant.now()))); + + String link = systemPropertyManager.get(SYSTEM_PROPERTY_URL_SHORTENER_LINK); + String linkRegex = ""; + if (StringUtils.hasText(link)) { + linkRegex = String.format(link, "%"); + } + + if (StringUtils.hasText(linkRegex)) { + for (ShortenedUrl shortenedUrl : shortenedUrlRepository + .findAll(qShortenedUrl.link.isNull().or(qShortenedUrl.link.isEmpty() + .or(qShortenedUrl.link.matches(linkRegex).not())))) { + shortenedUrl.setLink(String.format(link, shortenedUrl.getCode())); + shortenedUrl = shortenedUrlRepository.save(shortenedUrl); + } + } + } + +} diff --git a/urlshortener/src/main/java/de/bstly/we/urlshortener/businesslogic/ShortenedUrlPermissions.java b/urlshortener/src/main/java/de/bstly/we/urlshortener/businesslogic/ShortenedUrlPermissions.java new file mode 100644 index 0000000..54402b1 --- /dev/null +++ b/urlshortener/src/main/java/de/bstly/we/urlshortener/businesslogic/ShortenedUrlPermissions.java @@ -0,0 +1,12 @@ +/** + * + */ +package de.bstly.we.urlshortener.businesslogic; + +/** + * @author _bastler@bstly.de + * + */ +public interface ShortenedUrlPermissions { + public static final String URL_SHORTENER = "url_shortener"; +} diff --git a/urlshortener/src/main/java/de/bstly/we/urlshortener/businesslogic/ShortenedUrlQuotas.java b/urlshortener/src/main/java/de/bstly/we/urlshortener/businesslogic/ShortenedUrlQuotas.java new file mode 100644 index 0000000..4dc3e88 --- /dev/null +++ b/urlshortener/src/main/java/de/bstly/we/urlshortener/businesslogic/ShortenedUrlQuotas.java @@ -0,0 +1,13 @@ +/** + * + */ +package de.bstly.we.urlshortener.businesslogic; + +/** + * @author _bastler@bstly.de + * + */ +public interface ShortenedUrlQuotas { + + public static final String URL_SHORTENER = "url_shortener"; +} diff --git a/urlshortener/src/main/java/de/bstly/we/urlshortener/controller/ShortenedUrlController.java b/urlshortener/src/main/java/de/bstly/we/urlshortener/controller/ShortenedUrlController.java new file mode 100644 index 0000000..b6838e2 --- /dev/null +++ b/urlshortener/src/main/java/de/bstly/we/urlshortener/controller/ShortenedUrlController.java @@ -0,0 +1,355 @@ +/** + * + */ +package de.bstly.we.urlshortener.controller; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.businesslogic.PermissionManager; +import de.bstly.we.businesslogic.QuotaManager; +import de.bstly.we.businesslogic.SystemPropertyManager; +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.controller.support.RequestBodyErrors; +import de.bstly.we.model.Quota; +import de.bstly.we.urlshortener.businesslogic.ShortenedUrlManager; +import de.bstly.we.urlshortener.businesslogic.ShortenedUrlPermissions; +import de.bstly.we.urlshortener.businesslogic.ShortenedUrlQuotas; +import de.bstly.we.urlshortener.controller.model.ShortenedUrlModel; +import de.bstly.we.urlshortener.controller.validation.ShortenedUrlModelValidator; +import de.bstly.we.urlshortener.model.ShortenedUrl; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/url/shortener") +public class ShortenedUrlController extends BaseController { + + @Autowired + private ShortenedUrlManager shortenedUrlManager; + @Autowired + private PermissionManager permissionManager; + @Autowired + private QuotaManager quotaManager; + @Autowired + private PasswordEncoder passwordEncoder; + @Autowired + private SystemPropertyManager systemPropertyManager; + @Autowired + private ShortenedUrlModelValidator shortenedUrlModelValidator; + + /** + * + * @param code + * @param request + * @param response + * @throws IOException + */ + @GetMapping("/{code}") + public void getShortenedUrlLink(@PathVariable("code") String code, HttpServletRequest request, + HttpServletResponse response) throws IOException { + ShortenedUrl shortenedUrl = shortenedUrlManager.get(code); + + try { + if (shortenedUrl == null) { + if (StringUtils.hasText(systemPropertyManager + .get(ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND))) { + response.sendRedirect(systemPropertyManager + .get(ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND)); + } else { + response.sendError(404, "code not found"); + } + } else if (StringUtils.hasText(shortenedUrl.getPasswordHash())) { + if (StringUtils.hasText(systemPropertyManager + .get(ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD))) { + response.sendRedirect(String.format( + systemPropertyManager.get( + ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD), + shortenedUrl.getCode())); + } else { + response.sendError(401, "password protected"); + } + } else { + sendValidRedirect(shortenedUrl, request, response); + } + } catch (IOException e) { + response.sendError(400, code); + } + } + + /** + * + * @param code + * @param password + * @param request + * @param response + * @throws IOException + */ + @PostMapping("/{code}") + public void getProtectedShortenedUrlLink(@PathVariable("code") String code, + @ModelAttribute("password") String password, HttpServletRequest request, + HttpServletResponse response) throws IOException { + ShortenedUrl shortenedUrl = shortenedUrlManager.get(code); + + try { + if (shortenedUrl == null) { + if (StringUtils.hasText(systemPropertyManager + .get(ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND))) { + response.sendRedirect(systemPropertyManager + .get(ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND)); + } else { + response.sendError(404, "code not found"); + } + } else if (!StringUtils.hasText(shortenedUrl.getPasswordHash()) + || passwordEncoder.matches(password, shortenedUrl.getPasswordHash())) { + sendValidRedirect(shortenedUrl, request, response); + } else { + if (StringUtils.hasText(systemPropertyManager + .get(ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD))) { + String passwordErrorUrl = String.format( + systemPropertyManager.get( + ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD), + shortenedUrl.getCode()); + URI passwordErrorUri; + try { + passwordErrorUri = new URI(passwordErrorUrl); + if (StringUtils.hasText(passwordErrorUri.getQuery())) { + passwordErrorUrl += "&error"; + } else { + passwordErrorUrl += "?error"; + } + } catch (URISyntaxException e) { + + } + + response.sendRedirect(passwordErrorUrl); + } else { + response.sendError(401, "password protected"); + } + } + } catch (IOException e) { + response.sendError(400, code); + } + } + + /** + * + * @param shortenedUrl + * @param request + * @param response + * @throws IOException + */ + protected void sendValidRedirect(ShortenedUrl shortenedUrl, HttpServletRequest request, + HttpServletResponse response) throws IOException { + String url = shortenedUrl.getUrl(); + if (shortenedUrl.isQueryParameters() && StringUtils.hasText(request.getQueryString())) { + try { + URI uri = new URI(url); + if (StringUtils.hasText(uri.getQuery())) { + url += "&" + request.getQueryString(); + } else { + url += "?" + request.getQueryString(); + } + } catch (URISyntaxException e) { + } + } + + response.sendRedirect(url); + } + + /** + * + * @param code + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping("/model/{code}") + public ShortenedUrl getShortenedUrl(@PathVariable("code") String code) { + ShortenedUrl shortenedUrl = shortenedUrlManager.get(code); + if (shortenedUrl == null) { + throw new EntityResponseStatusException(HttpStatus.NO_CONTENT); + } + + if (StringUtils.hasText(shortenedUrl.getPasswordHash())) { + shortenedUrl.setUrl(null); + shortenedUrl.setPasswordHash(null); + } + + if (!shortenedUrl.getOwner().equals(getCurrentUserId())) { + shortenedUrl.setOwner(null); + shortenedUrl.setNote(null); + } + + return shortenedUrl; + + } + + /** + * + * @return + */ + @PreAuthorize("isAuthenticated()") + @GetMapping + public Page getShortenedUrls( + @RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter, + @RequestParam("sort") Optional sortParameter, + @RequestParam("desc") Optional descParameter, + @RequestParam("search") Optional searchParameter) { + if (!permissionManager.hasPermission(getCurrentUserId(), + ShortenedUrlPermissions.URL_SHORTENER)) { + shortenedUrlManager.deleteAll(getCurrentUserId(), true); + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + return shortenedUrlManager.getForUserId(getCurrentUserId(), pageParameter.orElse(0), + sizeParameter.orElse(10), sortParameter.orElse("code"), descParameter.orElse(false), + searchParameter.orElse("")); + } + + /** + * + * @param shortenedUrlModel + * @return + */ + @PreAuthorize("isAuthenticated()") + @PostMapping + public ShortenedUrl createShortenedUrl(@RequestBody ShortenedUrlModel shortenedUrlModel) { + if (!permissionManager.hasPermission(getCurrentUserId(), + ShortenedUrlPermissions.URL_SHORTENER) + || !permissionManager.isFullUser(getCurrentUserId())) { + shortenedUrlManager.deleteAll(getCurrentUserId(), true); + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + Quota shortenedUrlsQuota = quotaManager.get(getCurrentUserId(), + ShortenedUrlQuotas.URL_SHORTENER); + if (shortenedUrlsQuota == null || shortenedUrlsQuota.getValue() < 1) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + Errors errors = new RequestBodyErrors(shortenedUrlModel); + + shortenedUrlModelValidator.validate(shortenedUrlModel, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + return shortenedUrlManager.create(getCurrentUserId(), shortenedUrlModel.getUrl(), + shortenedUrlModel.getNote(), shortenedUrlModel.getCode(), + shortenedUrlModel.getExpires(), shortenedUrlModel.getPassword(), + shortenedUrlModel.isQueryParameters(), true); + } + + /** + * + * @param shortenedUrlModel + * @return + */ + @PreAuthorize("isAuthenticated()") + @PatchMapping + public ShortenedUrl updateShortenedUrl(@RequestBody ShortenedUrlModel shortenedUrlModel) { + if (!permissionManager.hasPermission(getCurrentUserId(), + ShortenedUrlPermissions.URL_SHORTENER) + || !permissionManager.isFullUser(getCurrentUserId())) { + shortenedUrlManager.deleteAll(getCurrentUserId(), true); + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + String code = shortenedUrlModel.getCode(); + boolean newShortenedUrl = false; + + ShortenedUrl shortenedUrl = shortenedUrlManager.get(code); + + if (shortenedUrl == null || !shortenedUrl.getOwner().equals(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + Errors errors = new RequestBodyErrors(shortenedUrlModel); + + if (StringUtils.hasText(shortenedUrlModel.getNewCode()) + && !shortenedUrlModel.getNewCode().equals(code)) { + shortenedUrlModel.setCode(shortenedUrlModel.getNewCode()); + newShortenedUrl = true; + } else { + shortenedUrlModel.setCode(null); + } + + shortenedUrlModelValidator.validate(shortenedUrlModel, errors); + if (errors.hasErrors()) { + throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT); + } + + if (newShortenedUrl) { + String passwordHash = shortenedUrl.getPasswordHash(); + shortenedUrlManager.delete(shortenedUrl, false); + shortenedUrl = new ShortenedUrl(); + shortenedUrl.setCode(shortenedUrlModel.getCode()); + shortenedUrl.setPasswordHash(passwordHash); + shortenedUrl.setOwner(getCurrentUserId()); + } + + shortenedUrl.setQueryParameters(shortenedUrlModel.isQueryParameters()); + shortenedUrl.setExpires(shortenedUrlModel.getExpires()); + shortenedUrl.setUrl(shortenedUrlModel.getUrl()); + shortenedUrl.setNote(shortenedUrlModel.getNote()); + + if (shortenedUrlModel.isNewPassword()) { + if (StringUtils.hasText(shortenedUrlModel.getPassword())) { + shortenedUrl + .setPasswordHash(passwordEncoder.encode(shortenedUrlModel.getPassword())); + } else { + shortenedUrl.setPasswordHash(null); + } + } + + return shortenedUrlManager.save(shortenedUrl); + } + + /** + * + * @param id + */ + @PreAuthorize("isAuthenticated()") + @DeleteMapping("/{code}") + public void deleteShortenedUrl(@PathVariable("code") String code) { + if (!permissionManager.hasPermission(getCurrentUserId(), + ShortenedUrlPermissions.URL_SHORTENER)) { + shortenedUrlManager.deleteAll(getCurrentUserId(), true); + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + ShortenedUrl shortenedUrl = shortenedUrlManager.get(code); + if (shortenedUrl == null || !shortenedUrl.getOwner().equals(getCurrentUserId())) { + throw new EntityResponseStatusException(HttpStatus.FORBIDDEN); + } + + shortenedUrlManager.delete(shortenedUrl, true); + } +} diff --git a/urlshortener/src/main/java/de/bstly/we/urlshortener/controller/ShortenedUrlManagementController.java b/urlshortener/src/main/java/de/bstly/we/urlshortener/controller/ShortenedUrlManagementController.java new file mode 100644 index 0000000..f4cf6a2 --- /dev/null +++ b/urlshortener/src/main/java/de/bstly/we/urlshortener/controller/ShortenedUrlManagementController.java @@ -0,0 +1,90 @@ +/** + * + */ +package de.bstly.we.urlshortener.controller; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.bstly.we.controller.BaseController; +import de.bstly.we.controller.support.EntityResponseStatusException; +import de.bstly.we.urlshortener.businesslogic.ShortenedUrlManager; +import de.bstly.we.urlshortener.model.ShortenedUrl; + +/** + * @author _bastler@bstly.de + * + */ +@RestController +@RequestMapping("/url/shortener/manage") +public class ShortenedUrlManagementController extends BaseController { + + @Autowired + private ShortenedUrlManager shortenedUrlManager; + + /** + * + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping + public Page getShortenedUrls( + @RequestParam("page") Optional pageParameter, + @RequestParam("size") Optional sizeParameter, + @RequestParam("search") Optional searchParameter) { + return shortenedUrlManager.get(pageParameter.orElse(0), sizeParameter.orElse(10), "code", + true, searchParameter.orElse("")); + } + + /** + * + * @param shortenedUrl + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping + public ShortenedUrl createOrUpdateShortenedUrl(@RequestBody ShortenedUrl shortenedUrl) { + return shortenedUrlManager.save(shortenedUrl); + } + + /** + * + * @param id + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/{code}") + public void deleteShortenedUrl(@PathVariable("code") String code, + @RequestParam("quota") Optional quota) { + + ShortenedUrl shortenedUrl = shortenedUrlManager.get(code); + if (shortenedUrl == null) { + throw new EntityResponseStatusException(HttpStatus.CONFLICT); + } + + shortenedUrlManager.delete(shortenedUrl, quota.isPresent() && quota.get().booleanValue()); + } + + /** + * + * @param owner + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @DeleteMapping("/all/{owner}") + public void deleteAll(@PathVariable("owner") Long owner, + @RequestParam("quota") Optional quota) { + shortenedUrlManager.deleteAll(owner, quota.isPresent() && quota.get().booleanValue()); + } + +} diff --git a/urlshortener/src/main/java/de/bstly/we/urlshortener/controller/model/ShortenedUrlModel.java b/urlshortener/src/main/java/de/bstly/we/urlshortener/controller/model/ShortenedUrlModel.java new file mode 100644 index 0000000..9aa90ea --- /dev/null +++ b/urlshortener/src/main/java/de/bstly/we/urlshortener/controller/model/ShortenedUrlModel.java @@ -0,0 +1,150 @@ +/** + * + */ +package de.bstly.we.urlshortener.controller.model; + +import java.time.Instant; + +/** + * @author _bastler@bstly.de + * + */ +public class ShortenedUrlModel { + + private String code; + private String newCode; + private String url; + private String note; + private Instant expires; + private String password; + private String password2; + private boolean queryParameters; + private boolean newPassword; + + /** + * @return the code + */ + public String getCode() { + return code; + } + + /** + * @param code the code to set + */ + public void setCode(String code) { + this.code = code; + } + + /** + * @return the newCode + */ + public String getNewCode() { + return newCode; + } + + /** + * @param newCode the newCode to set + */ + public void setNewCode(String newCode) { + this.newCode = newCode; + } + + /** + * @return the url + */ + public String getUrl() { + return url; + } + + /** + * @param url the url to set + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * @return the note + */ + public String getNote() { + return note; + } + + /** + * @param note the note to set + */ + public void setNote(String note) { + this.note = note; + } + + /** + * @return the expires + */ + public Instant getExpires() { + return expires; + } + + /** + * @param expires the expires to set + */ + public void setExpires(Instant expires) { + this.expires = expires; + } + + /** + * @return the password + */ + public String getPassword() { + return password; + } + + /** + * @param password the password to set + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * @return the password2 + */ + public String getPassword2() { + return password2; + } + + /** + * @param password2 the password2 to set + */ + public void setPassword2(String password2) { + this.password2 = password2; + } + + /** + * @return the queryParameters + */ + public boolean isQueryParameters() { + return queryParameters; + } + + /** + * @param queryParameters the queryParameters to set + */ + public void setQueryParameters(boolean queryParameters) { + this.queryParameters = queryParameters; + } + + /** + * @return the newPassword + */ + public boolean isNewPassword() { + return newPassword; + } + + /** + * @param newPassword the newPassword to set + */ + public void setNewPassword(boolean newPassword) { + this.newPassword = newPassword; + } + +} diff --git a/urlshortener/src/main/java/de/bstly/we/urlshortener/controller/validation/ShortenedUrlModelValidator.java b/urlshortener/src/main/java/de/bstly/we/urlshortener/controller/validation/ShortenedUrlModelValidator.java new file mode 100644 index 0000000..210cb39 --- /dev/null +++ b/urlshortener/src/main/java/de/bstly/we/urlshortener/controller/validation/ShortenedUrlModelValidator.java @@ -0,0 +1,69 @@ +/** + * + */ +package de.bstly.we.urlshortener.controller.validation; + +import java.time.Instant; + +import org.apache.commons.validator.routines.UrlValidator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import de.bstly.we.urlshortener.businesslogic.ShortenedUrlManager; +import de.bstly.we.urlshortener.controller.model.ShortenedUrlModel; + +/** + * @author _bastler@bstly.de + * + */ +@Component +public class ShortenedUrlModelValidator implements Validator { + + @Autowired + private ShortenedUrlManager shortenedUrlManager; + private UrlValidator urlValidator = new UrlValidator(UrlValidator.ALLOW_ALL_SCHEMES); + protected static final String codePart = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^-]+(?:\\\\.[A-Z0-9_!#$%&'*+/=?`{|}~^-]+)*$"; + + /* + * @see org.springframework.validation.Validator#supports(java.lang.Class) + */ + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(ShortenedUrlModel.class); + } + + /* + * @see org.springframework.validation.Validator#validate(java.lang.Object, + * org.springframework.validation.Errors) + */ + @Override + public void validate(Object target, Errors errors) { + ShortenedUrlModel shortenedUrlModel = (ShortenedUrlModel) target; + + if (!urlValidator.isValid(shortenedUrlModel.getUrl())) { + errors.rejectValue("url", "INVALID_URL"); + } + + if (StringUtils.hasText(shortenedUrlModel.getCode())) { + if (shortenedUrlManager.get(shortenedUrlModel.getCode()) != null) { + errors.rejectValue("code", "NOT_VALID"); + } else if (!shortenedUrlModel.getCode().matches(codePart)) { + errors.rejectValue("code", "NOT_VALID"); + } + } + + if (shortenedUrlModel.getExpires() != null + && shortenedUrlModel.getExpires().isBefore(Instant.now())) { + errors.rejectValue("expires", "NOT_VALID"); + } + + if (StringUtils.hasText(shortenedUrlModel.getPassword()) + && !(shortenedUrlModel.getPassword().equals(shortenedUrlModel.getPassword2()))) { + errors.rejectValue("password", "NOT_MATCH"); + } + } + +} diff --git a/urlshortener/src/main/java/de/bstly/we/urlshortener/model/ShortenedUrl.java b/urlshortener/src/main/java/de/bstly/we/urlshortener/model/ShortenedUrl.java new file mode 100644 index 0000000..c62de00 --- /dev/null +++ b/urlshortener/src/main/java/de/bstly/we/urlshortener/model/ShortenedUrl.java @@ -0,0 +1,172 @@ +/** + * + */ +package de.bstly.we.urlshortener.model; + +import java.time.Instant; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EntityListeners; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; +import javax.persistence.Transient; + +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import de.bstly.we.businesslogic.support.AbstractModelEventListener; +import de.bstly.we.model.AbstractModel; +import de.bstly.we.model.UserData; + +/** + * @author _bastler@bstly.de + * + */ +@Entity +@Table(name = "shortened_urls") +@EntityListeners(AbstractModelEventListener.class) +public class ShortenedUrl implements UserData, AbstractModel { + + @Id + @Column(name = "code") + private String code; + private Long owner; + @Lob + private String url; + private Instant expires; + @JsonIgnore + @Column(name = "password", nullable = true) + private String passwordHash; + private String link; + @Lob + private String note; + @Column(name = "addon", columnDefinition = "boolean default false") + private boolean queryParameters; + @JsonProperty("password") + @Transient + private boolean hasPassword; + + /** + * @return the code + */ + public String getCode() { + return code; + } + + /** + * @param code the code to set + */ + public void setCode(String code) { + this.code = code; + } + + /** + * @return the owner + */ + public Long getOwner() { + return owner; + } + + /** + * @param owner the owner to set + */ + public void setOwner(Long owner) { + this.owner = owner; + } + + /** + * @return the url + */ + public String getUrl() { + return url; + } + + /** + * @param url the url to set + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * @return the expires + */ + public Instant getExpires() { + return expires; + } + + /** + * @param expires the expires to set + */ + public void setExpires(Instant expires) { + this.expires = expires; + } + + /** + * @return the passwordHash + */ + public String getPasswordHash() { + return passwordHash; + } + + /** + * @param passwordHash the passwordHash to set + */ + public void setPasswordHash(String passwordHash) { + this.passwordHash = passwordHash; + } + + /** + * @return the link + */ + public String getLink() { + return link; + } + + /** + * @param link the link to set + */ + public void setLink(String link) { + this.link = link; + } + + /** + * @return the note + */ + public String getNote() { + return note; + } + + /** + * @param note the note to set + */ + public void setNote(String note) { + this.note = note; + } + + /** + * @return the queryParameters + */ + public boolean isQueryParameters() { + return queryParameters; + } + + /** + * @param queryParameters the queryParameters to set + */ + public void setQueryParameters(boolean queryParameters) { + this.queryParameters = queryParameters; + } + + /** + * @return the hasPassword + */ + public boolean isHasPassword() { + return StringUtils.hasText(passwordHash); + } + +} diff --git a/urlshortener/src/main/java/de/bstly/we/urlshortener/repository/ShortenedUrlRepository.java b/urlshortener/src/main/java/de/bstly/we/urlshortener/repository/ShortenedUrlRepository.java new file mode 100755 index 0000000..5b9beb2 --- /dev/null +++ b/urlshortener/src/main/java/de/bstly/we/urlshortener/repository/ShortenedUrlRepository.java @@ -0,0 +1,20 @@ +/** + * + */ +package de.bstly.we.urlshortener.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +import de.bstly.we.urlshortener.model.ShortenedUrl; + +/** + * + * @author _bastler@bstly.de + * + */ +@Repository +public interface ShortenedUrlRepository + extends JpaRepository, QuerydslPredicateExecutor { +} diff --git a/wireguard/pom.xml b/wireguard/pom.xml new file mode 100755 index 0000000..13b8d6b --- /dev/null +++ b/wireguard/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + de.bstly.we + webstly-main + ${revision} + + + wireguard + webstly-wireguard + + + + de.bstly.we + webstly-core + ${revision} + + +