diff options
Diffstat (limited to 'server')
78 files changed, 0 insertions, 4608 deletions
diff --git a/server/LICENSE b/server/LICENSE deleted file mode 100644 index 45644ff..0000000 --- a/server/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - 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. - - <one line to give the program's name and a brief idea of what it does.> - Copyright (C) <year> <name of author> - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <http://www.gnu.org/licenses/>. - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - <program> Copyright (C) <year> <name of author> - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - 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 GPL, see -<http://www.gnu.org/licenses/>. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/server/Setup.hs b/server/Setup.hs deleted file mode 100644 index 4467109..0000000 --- a/server/Setup.hs +++ /dev/null @@ -1,2 +0,0 @@ -import Distribution.Simple -main = defaultMain diff --git a/server/migrations/1.sql b/server/migrations/1.sql deleted file mode 100644 index d7c300e..0000000 --- a/server/migrations/1.sql +++ /dev/null @@ -1,65 +0,0 @@ -CREATE TABLE IF NOT EXISTS "user" ( - "id" INTEGER PRIMARY KEY, - "creation" TIMESTAMP NOT NULL, - "email" VARCHAR NOT NULL, - "name" VARCHAR NOT NULL, - CONSTRAINT "uniq_user_email" UNIQUE ("email"), - CONSTRAINT "uniq_user_name" UNIQUE ("name") -); - -CREATE TABLE IF NOT EXISTS "job" ( - "id" INTEGER PRIMARY KEY, - "kind" VARCHAR NOT NULL, - "last_execution" TIMESTAMP NULL, - "last_check" TIMESTAMP NULL, - CONSTRAINT "uniq_job_kind" UNIQUE ("kind") -); - -CREATE TABLE IF NOT EXISTS "sign_in"( - "id" INTEGER PRIMARY KEY, - "token" VARCHAR NOT NULL, - "creation" TIMESTAMP NOT NULL, - "email" VARCHAR NOT NULL, - "is_used" BOOLEAN NOT NULL, - CONSTRAINT "uniq_sign_in_token" UNIQUE ("token") -); - -CREATE TABLE IF NOT EXISTS "payment"( - "id" INTEGER PRIMARY KEY, - "user_id" INTEGER NOT NULL REFERENCES "user", - "name" VARCHAR NOT NULL, - "cost" INTEGER NOT NULL, - "date" DATE NOT NULL, - "frequency" VARCHAR NOT NULL, - "created_at" TIMESTAMP NOT NULL, - "edited_at" TIMESTAMP NULL, - "deleted_at" TIMESTAMP NULL -); - -CREATE TABLE IF NOT EXISTS "income"( - "id" INTEGER PRIMARY KEY, - "user_id" INTEGER NOT NULL REFERENCES "user", - "date" DATE NOT NULL, - "amount" INTEGERNOT NULL, - "created_at" TIMESTAMP NOT NULL, - "edited_at" TIMESTAMP NULL, - "deleted_at" TIMESTAMP NULL -); - -CREATE TABLE IF NOT EXISTS "category"( - "id" INTEGER PRIMARY KEY, - "name" VARCHAR NOT NULL, - "color" VARCHAR NOT NULL, - "created_at" TIMESTAMP NOT NULL, - "edited_at" TIMESTAMP NULL, - "deleted_at" TIMESTAMP NULL -); - -CREATE TABLE IF NOT EXISTS "payment_category"( - "id" INTEGER PRIMARY KEY, - "name" VARCHAR NOT NULL, - "category" INTEGER NOT NULL REFERENCES "category", - "created_at" TIMESTAMP NOT NULL, - "edited_at" TIMESTAMP NULL, - CONSTRAINT "uniq_payment_category_name" UNIQUE ("name") -); diff --git a/server/migrations/2.sql b/server/migrations/2.sql deleted file mode 100644 index c1d502f..0000000 --- a/server/migrations/2.sql +++ /dev/null @@ -1,44 +0,0 @@ --- Add payment categories with accents from payment with accents - -INSERT INTO - payment_category (name, category, created_at) -SELECT - DISTINCT lower(payment.name), payment_category.category, datetime('now') -FROM - payment -INNER JOIN - payment_category -ON - replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(lower(payment.name), 'é', 'e'), 'è', 'e'), 'à', 'a'), 'û', 'u'), 'â', 'a'), 'ê', 'e'), 'â', 'a'), 'î', 'i'), 'ï', 'i'), 'ô', 'o'), 'ë', 'e') = payment_category.name -WHERE - payment.name -IN - (SELECT DISTINCT payment.name FROM payment WHERE lower(payment.name) NOT IN (SELECT payment_category.name FROM payment_category) AND payment.deleted_at IS NULL); - --- Remove unused payment categories - -DELETE FROM - payment_category -WHERE - name NOT IN (SELECT DISTINCT lower(name) FROM payment); - --- Add category id to payment table - -PRAGMA foreign_keys = 0; - -ALTER TABLE payment ADD COLUMN "category" INTEGER NOT NULL REFERENCES "category" DEFAULT -1; - -PRAGMA foreign_keys = 1; - -UPDATE - payment -SET - category = (SELECT category FROM payment_category WHERE payment_category.name = LOWER(payment.name)) -WHERE - EXISTS (SELECT category FROM payment_category WHERE payment_category.name = LOWER(payment.name)); - -DELETE FROM payment WHERE category = -1; - --- Remove - -DROP TABLE payment_category; diff --git a/server/migrations/3.sql b/server/migrations/3.sql deleted file mode 100644 index a3d8a13..0000000 --- a/server/migrations/3.sql +++ /dev/null @@ -1,5 +0,0 @@ -DROP TABLE sign_in; - -ALTER TABLE user ADD COLUMN "password" TEXT NOT NULL DEFAULT "password"; - -ALTER TABLE user ADD COLUMN "sign_in_token" TEXT NULL; diff --git a/server/server.cabal b/server/server.cabal deleted file mode 100644 index 5427385..0000000 --- a/server/server.cabal +++ /dev/null @@ -1,131 +0,0 @@ -Name: server -Version: 0.0.1 -License: GPL-3 -License-file: LICENSE -Author: Joris Guyonvarch -Maintainer: joris@guyonvarch.me -Category: Web -Build-type: Simple -Cabal-version: >=1.10 - -Executable server - Main-is: Main.hs - Ghc-options: -Wall -Werror - Hs-source-dirs: src - Default-language: Haskell2010 - - Default-extensions: - ExistentialQuantification - LambdaCase - MultiParamTypeClasses - OverloadedStrings - ScopedTypeVariables - - Build-depends: - aeson - , base >= 4.11 && < 5 - , base64-bytestring - , bcrypt - , blaze-builder - , blaze-html - , bytestring - , clay - , clientsession - , common - , config-manager - , containers - , cookie - , filepath - , http-conduit - , http-types - , jsaddle - , mime-mail - , monad-logger - , mtl - , parsec - , process - , random - , resourcet - , scotty - , sqlite-simple - , text - , time - , transformers - , unordered-containers - , uuid - , validation - , wai - , wai-extra - , wai-middleware-static - - other-modules: - Conf - Controller.Category - Controller.Helper - Controller.Income - Controller.Index - Controller.Payment - Controller.Statistics - Controller.User - Cookie - Design.Appearing - Design.Color - Design.Constants - Design.Errors - Design.Form - Design.Global - Design.Helper - Design.Loadable - Design.Media - Design.Modal - Design.Tooltip - Design.View.ConfirmDialog - Design.View.Header - Design.View.NotFound - Design.View.Pages - Design.View.Payment - Design.View.Payment.Form - Design.View.Payment.HeaderForm - Design.View.Payment.HeaderInfos - Design.View.SignIn - Design.View.Stat - Design.View.Table - Design.Views - Job.Daemon - Job.Frequency - Job.Kind - Job.Model - Job.MonthlyPayment - Job.WeeklyReport - LoginSession - Model.CreateCategory - Model.CreateIncome - Model.CreatePayment - Model.EditCategory - Model.EditIncome - Model.EditPayment - Model.HashedPassword - Model.IncomeResource - Model.Mail - Model.PaymentResource - Model.Query - Model.SignIn - Model.UUID - Payer - Persistence.Category - Persistence.Frequency - Persistence.Income - Persistence.Payment - Persistence.User - Persistence.Util - Resource - Secure - SendMail - Statistics - Util.Time - Validation.Category - Validation.Income - Validation.Payment - Validation.SignIn - View.Mail.WeeklyReport - View.Page diff --git a/server/src/Conf.hs b/server/src/Conf.hs deleted file mode 100644 index ca19c8d..0000000 --- a/server/src/Conf.hs +++ /dev/null @@ -1,39 +0,0 @@ -module Conf - ( get - , Conf(..) - ) where - -import qualified Data.ConfigManager as Conf -import Data.Text (Text) -import qualified Data.Text as T -import Data.Time.Clock (NominalDiffTime) - -import Common.Model (Currency (..)) - -data Conf = Conf - { hostname :: Text - , port :: Int - , signInExpiration :: NominalDiffTime - , currency :: Currency - , noReplyMail :: Text - , https :: Bool - , devMode :: Bool - } deriving Show - -get :: FilePath -> IO Conf -get path = do - conf <- - (flip fmap) (Conf.readConfig path) (\configOrError -> do - conf <- configOrError - Conf <$> - Conf.lookup "hostname" conf <*> - Conf.lookup "port" conf <*> - Conf.lookup "signInExpiration" conf <*> - fmap Currency (Conf.lookup "currency" conf) <*> - Conf.lookup "noReplyMail" conf <*> - Conf.lookup "https" conf <*> - Conf.lookup "devMode" conf - ) - case conf of - Left msg -> error (T.unpack msg) - Right c -> return c diff --git a/server/src/Controller/Category.hs b/server/src/Controller/Category.hs deleted file mode 100644 index 371ba78..0000000 --- a/server/src/Controller/Category.hs +++ /dev/null @@ -1,88 +0,0 @@ -module Controller.Category - ( listAll - , list - , create - , edit - , delete - ) where - -import Control.Monad.IO.Class (liftIO) -import qualified Data.Text.Lazy as TL -import Data.Validation (Validation (..)) -import Network.HTTP.Types.Status (badRequest400, ok200) -import Web.Scotty hiding (delete) - -import Common.Model (CategoryId, CategoryPage (..), - CreateCategoryForm (..), - EditCategoryForm (..)) -import qualified Common.Msg as Msg - -import qualified Controller.Helper as ControllerHelper -import Model.CreateCategory (CreateCategory (..)) -import Model.EditCategory (EditCategory (..)) -import qualified Model.Query as Query -import qualified Persistence.Category as CategoryPersistence -import qualified Persistence.Payment as PaymentPersistence -import qualified Secure -import qualified Validation.Category as CategoryValidation - -listAll :: ActionM () -listAll = - Secure.loggedAction (\_ -> - (liftIO . Query.run $ CategoryPersistence.listAll) >>= json - ) - -list :: Int -> Int -> ActionM () -list page perPage = - Secure.loggedAction (\_ -> - (liftIO . Query.run $ do - categories <- CategoryPersistence.list page perPage - usedCategories <- PaymentPersistence.usedCategories - count <- CategoryPersistence.count - return $ CategoryPage page categories usedCategories count - ) >>= json - ) - -create :: CreateCategoryForm -> ActionM () -create form = - Secure.loggedAction (\_ -> - (liftIO . Query.run $ do - case CategoryValidation.createCategory form of - Success (CreateCategory name color) -> do - Right <$> (CategoryPersistence.create name color) - - Failure validationError -> - return $ Left validationError - ) >>= ControllerHelper.okOrBadRequest - ) - -edit :: EditCategoryForm -> ActionM () -edit form = - Secure.loggedAction (\_ -> - (liftIO . Query.run $ do - case CategoryValidation.editCategory form of - Success (EditCategory categoryId name color) -> - do - isSuccess <- CategoryPersistence.edit categoryId name color - return $ if isSuccess then - Right () - else - Left $ Msg.get Msg.Error_CategoryEdit - - Failure validationError -> - return $ Left validationError - ) >>= ControllerHelper.okOrBadRequest - ) - -delete :: CategoryId -> ActionM () -delete categoryId = - Secure.loggedAction (\_ -> do - deleted <- liftIO . Query.run $ do - CategoryPersistence.delete categoryId - if deleted - then - status ok200 - else do - status badRequest400 - text . TL.fromStrict $ Msg.get Msg.Category_NotDeleted - ) diff --git a/server/src/Controller/Helper.hs b/server/src/Controller/Helper.hs deleted file mode 100644 index dc9cbc4..0000000 --- a/server/src/Controller/Helper.hs +++ /dev/null @@ -1,16 +0,0 @@ -module Controller.Helper - ( okOrBadRequest - ) where - -import Data.Text (Text) -import qualified Data.Text.Lazy as LT -import qualified Network.HTTP.Types.Status as Status -import Web.Scotty (ActionM) -import qualified Web.Scotty as S - -okOrBadRequest :: Either Text () -> ActionM () -okOrBadRequest (Left message) = do - S.status Status.badRequest400 - S.text (LT.fromStrict message) -okOrBadRequest (Right ()) = - S.status Status.ok200 diff --git a/server/src/Controller/Income.hs b/server/src/Controller/Income.hs deleted file mode 100644 index 96ccbbc..0000000 --- a/server/src/Controller/Income.hs +++ /dev/null @@ -1,90 +0,0 @@ -module Controller.Income - ( list - , create - , edit - , delete - ) where - -import Control.Monad.IO.Class (liftIO) -import qualified Data.Map as M -import qualified Data.Time.Clock as Clock -import Data.Validation (Validation (..)) -import qualified Network.HTTP.Types.Status as Status -import Web.Scotty hiding (delete) - -import Common.Model (CreateIncomeForm (..), - EditIncomeForm (..), - IncomeHeader (..), IncomeId, - IncomePage (..), User (..)) -import qualified Common.Msg as Msg - -import qualified Controller.Helper as ControllerHelper -import Model.CreateIncome (CreateIncome (..)) -import Model.EditIncome (EditIncome (..)) -import qualified Model.Query as Query -import qualified Persistence.Income as IncomePersistence -import qualified Persistence.Payment as PaymentPersistence -import qualified Persistence.User as UserPersistence -import qualified Secure -import qualified Validation.Income as IncomeValidation - -list :: Int -> Int -> ActionM () -list page perPage = - Secure.loggedAction (\_ -> do - currentTime <- liftIO Clock.getCurrentTime - (liftIO . Query.run $ do - count <- IncomePersistence.count - - users <- UserPersistence.list - let userIds = _user_id <$> users - - paymentRange <- PaymentPersistence.getRange - incomeDefinedForAll <- IncomePersistence.definedForAll userIds - let since = max <$> (fst <$> paymentRange) <*> incomeDefinedForAll - - cumulativeIncome <- - case since of - Just s -> IncomePersistence.getCumulativeIncome s (Clock.utctDay currentTime) - Nothing -> return M.empty - - incomes <- IncomePersistence.list page perPage - return $ IncomePage page (IncomeHeader since cumulativeIncome) incomes count) >>= json - ) - -create :: CreateIncomeForm -> ActionM () -create form = - Secure.loggedAction (\user -> - (liftIO . Query.run $ do - case IncomeValidation.createIncome form of - Success (CreateIncome amount date) -> do - Right <$> (IncomePersistence.create (_user_id user) date amount) - - Failure validationError -> - return $ Left validationError - ) >>= ControllerHelper.okOrBadRequest - ) - -edit :: EditIncomeForm -> ActionM () -edit form = - Secure.loggedAction (\user -> - (liftIO . Query.run $ do - case IncomeValidation.editIncome form of - Success (EditIncome incomeId amount date) -> - do - isSuccess <- IncomePersistence.edit (_user_id user) incomeId date amount - return $ if isSuccess then - Right () - else - Left $ Msg.get Msg.Error_IncomeEdit - - Failure validationError -> - return $ Left validationError - ) >>= ControllerHelper.okOrBadRequest - ) - -delete :: IncomeId -> ActionM () -delete incomeId = - Secure.loggedAction (\user -> do - _ <- liftIO . Query.run $ IncomePersistence.delete (_user_id user) incomeId - status Status.ok200 - ) diff --git a/server/src/Controller/Index.hs b/server/src/Controller/Index.hs deleted file mode 100644 index 4f4ae77..0000000 --- a/server/src/Controller/Index.hs +++ /dev/null @@ -1,76 +0,0 @@ -module Controller.Index - ( get - , signIn - , signOut - ) where - -import Control.Monad.IO.Class (liftIO) -import Data.Text (Text) -import qualified Data.Text.Lazy as TL -import Data.Validation (Validation (..)) -import qualified Network.HTTP.Types.Status as Status -import Prelude hiding (error, init) -import Web.Scotty (ActionM) -import qualified Web.Scotty as S - -import Common.Model (Init (..), SignInForm (..), - User (..)) -import qualified Common.Msg as Msg - -import Conf (Conf (..)) -import qualified LoginSession -import Model.Query (Query) -import qualified Model.Query as Query -import Model.SignIn (SignIn (..)) -import qualified Persistence.User as UserPersistence -import qualified Validation.SignIn as SignInValidation -import View.Page (page) - -get :: Conf -> ActionM () -get conf = do - init <- do - mbToken <- LoginSession.get - case mbToken of - Nothing -> - return Nothing - Just token -> do - liftIO . Query.run $ getInit conf token - S.html $ page init - -signIn :: Conf -> SignInForm -> ActionM () -signIn conf form = - case SignInValidation.signIn form of - Failure _ -> - textKey Status.badRequest400 Msg.SignIn_InvalidCredentials - Success (SignIn email password) -> do - result <- liftIO . Query.run $ do - isPasswordValid <- UserPersistence.checkPassword email password - if isPasswordValid then - do - signInToken <- UserPersistence.createSignInToken email - init <- getInit conf signInToken - return $ Just (signInToken, init) - else - return Nothing - case result of - Just (signInToken, init) -> do - LoginSession.put conf signInToken - S.json init - - Nothing -> - textKey Status.badRequest400 Msg.SignIn_InvalidCredentials - where textKey st key = S.status st >> (S.text . TL.fromStrict $ Msg.get key) - -getInit :: Conf -> Text -> Query (Maybe Init) -getInit conf signInToken = do - user <- UserPersistence.get signInToken - case user of - Just u -> - do - users <- UserPersistence.list - return . Just $ Init users (_user_id u) (Conf.currency conf) - Nothing -> - return Nothing - -signOut :: Conf -> ActionM () -signOut conf = LoginSession.delete conf >> S.status Status.ok200 diff --git a/server/src/Controller/Payment.hs b/server/src/Controller/Payment.hs deleted file mode 100644 index 4fb4d54..0000000 --- a/server/src/Controller/Payment.hs +++ /dev/null @@ -1,118 +0,0 @@ -module Controller.Payment - ( list - , create - , edit - , delete - , searchCategory - ) where - -import Control.Monad.IO.Class (liftIO) -import qualified Data.Map as M -import Data.Text (Text) -import qualified Data.Time.Clock as Clock -import qualified Data.Time.Calendar as Calendar -import Data.Validation (Validation (Failure, Success)) -import Web.Scotty (ActionM) -import qualified Web.Scotty as S - -import Common.Model (Category (..), CreatePaymentForm (..), - EditPaymentForm (..), Frequency, - PaymentHeader (..), PaymentId, - PaymentPage (..), User (..)) -import qualified Common.Msg as Msg - -import qualified Controller.Helper as ControllerHelper -import Model.CreatePayment (CreatePayment (..)) -import Model.EditPayment (EditPayment (..)) -import qualified Model.Query as Query -import qualified Payer as Payer -import qualified Persistence.Category as CategoryPersistence -import qualified Persistence.Income as IncomePersistence -import qualified Persistence.Payment as PaymentPersistence -import qualified Persistence.User as UserPersistence -import qualified Secure -import qualified Validation.Payment as PaymentValidation - -list :: Frequency -> Int -> Int -> Text -> ActionM () -list frequency page perPage search = - Secure.loggedAction (\_ -> do - currentUtctDay <- liftIO $ Clock.utctDay <$> Clock.getCurrentTime - (liftIO . Query.run $ do - count <- PaymentPersistence.count frequency search - payments <- PaymentPersistence.listActivePage frequency page perPage search - - users <- UserPersistence.list - - paymentRange <- PaymentPersistence.getRange - incomeDefinedForAll <- IncomePersistence.definedForAll (_user_id <$> users) - - cumulativeIncome <- - case (incomeDefinedForAll, paymentRange) of - (Just incomeStart, Just (paymentStart, _)) -> - IncomePersistence.getCumulativeIncome (max incomeStart paymentStart) currentUtctDay - - _ -> - return M.empty - - searchRepartition <- - case paymentRange of - Just (from, to) -> - PaymentPersistence.repartition frequency search from (Calendar.addDays 1 to) - Nothing -> - return M.empty - - (preIncomeRepartition, postIncomeRepartition) <- - PaymentPersistence.getPreAndPostPaymentRepartition paymentRange users - - let exceedingPayers = Payer.getExceedingPayers users cumulativeIncome preIncomeRepartition postIncomeRepartition - - header = PaymentHeader - { _paymentHeader_exceedingPayers = exceedingPayers - , _paymentHeader_repartition = searchRepartition - } - - return $ PaymentPage page frequency header payments count) >>= S.json - ) - -create :: CreatePaymentForm -> ActionM () -create form = - Secure.loggedAction (\user -> - (liftIO . Query.run $ do - cs <- map _category_id <$> CategoryPersistence.listAll - case PaymentValidation.createPayment cs form of - Success (CreatePayment name cost date category frequency) -> - Right <$> PaymentPersistence.create (_user_id user) name cost date category frequency - Failure validationError -> - return $ Left validationError - ) >>= ControllerHelper.okOrBadRequest - ) - -edit :: EditPaymentForm -> ActionM () -edit form = - Secure.loggedAction (\user -> - (liftIO . Query.run $ do - cs <- map _category_id <$> CategoryPersistence.listAll - case PaymentValidation.editPayment cs form of - Success (EditPayment paymentId name cost date category frequency) -> do - isSuccess <- PaymentPersistence.edit (_user_id user) paymentId name cost date category frequency - return $ if isSuccess then - Right () - else - Left $ Msg.get Msg.Error_PaymentEdit - Failure validationError -> - return $ Left validationError - ) >>= ControllerHelper.okOrBadRequest - ) - -delete :: PaymentId -> ActionM () -delete paymentId = - Secure.loggedAction (\user -> - liftIO . Query.run $ PaymentPersistence.delete (_user_id user) paymentId - ) - -searchCategory :: Text -> ActionM () -searchCategory paymentName = - Secure.loggedAction (\_ -> do - (liftIO $ Query.run (PaymentPersistence.searchCategory paymentName)) - >>= S.json - ) diff --git a/server/src/Controller/Statistics.hs b/server/src/Controller/Statistics.hs deleted file mode 100644 index 500c93c..0000000 --- a/server/src/Controller/Statistics.hs +++ /dev/null @@ -1,21 +0,0 @@ -module Controller.Statistics - ( paymentsAndIncomes - ) where - -import Control.Monad.IO.Class (liftIO) -import Web.Scotty (ActionM) -import qualified Web.Scotty as S - -import qualified Model.Query as Query -import qualified Persistence.Income as IncomePersistence -import qualified Persistence.Payment as PaymentPersistence -import qualified Secure -import qualified Statistics - -paymentsAndIncomes :: ActionM () -paymentsAndIncomes = - Secure.loggedAction (\_ -> do - payments <- liftIO $ Query.run PaymentPersistence.listAllPunctual - incomes <- liftIO $ Query.run IncomePersistence.listAll - S.json (Statistics.paymentsAndIncomes payments incomes) - ) diff --git a/server/src/Controller/User.hs b/server/src/Controller/User.hs deleted file mode 100644 index a7bb136..0000000 --- a/server/src/Controller/User.hs +++ /dev/null @@ -1,17 +0,0 @@ -module Controller.User - ( list - ) where - -import Control.Monad.IO.Class (liftIO) -import Web.Scotty (ActionM) -import qualified Web.Scotty as S - -import qualified Model.Query as Query -import qualified Persistence.User as UserPersistence -import qualified Secure - -list :: ActionM () -list = - Secure.loggedAction (\_ -> - (liftIO . Query.run $ UserPersistence.list) >>= S.json - ) diff --git a/server/src/Cookie.hs b/server/src/Cookie.hs deleted file mode 100644 index 00d73f2..0000000 --- a/server/src/Cookie.hs +++ /dev/null @@ -1,55 +0,0 @@ -module Cookie - ( makeSimpleCookie - , setCookie - , setSimpleCookie - , getCookie - , getCookies - , deleteCookie - ) where - -import Control.Monad (liftM) - -import qualified Data.Text as TS -import qualified Data.Text.Encoding as TS -import qualified Data.Text.Lazy.Encoding as TL - -import Conf (Conf) -import qualified Conf - -import qualified Data.Map as Map - -import qualified Data.ByteString.Lazy as BSL - -import Data.Time.Clock.POSIX (posixSecondsToUTCTime) - -import Blaze.ByteString.Builder (toLazyByteString) - -import Web.Cookie -import Web.Scotty.Trans - -makeSimpleCookie :: Conf -> TS.Text -> TS.Text -> SetCookie -makeSimpleCookie conf name value = - def - { setCookieName = TS.encodeUtf8 name - , setCookieValue = TS.encodeUtf8 value - , setCookiePath = Just $ TS.encodeUtf8 "/" - , setCookieSecure = Conf.https conf - , setCookieHttpOnly = True - } - -setCookie :: (Monad m) => SetCookie -> ActionT e m () -setCookie name = addHeader "Set-Cookie" (TL.decodeUtf8 . toLazyByteString $ renderSetCookie name) - -setSimpleCookie :: (Monad m) => Conf -> TS.Text -> TS.Text -> ActionT e m () -setSimpleCookie conf name value = setCookie $ makeSimpleCookie conf name value - -getCookie :: (Monad m, ScottyError e) => TS.Text -> ActionT e m (Maybe TS.Text) -getCookie name = liftM (Map.lookup name) getCookies - -getCookies :: (Monad m, ScottyError e) => ActionT e m (Map.Map TS.Text TS.Text) -getCookies = - liftM (Map.fromList . maybe [] parse) $ header "Cookie" - where parse = parseCookiesText . BSL.toStrict . TL.encodeUtf8 - -deleteCookie :: (Monad m) => Conf -> TS.Text -> ActionT e m () -deleteCookie conf name = setCookie $ (makeSimpleCookie conf name "") { setCookieExpires = Just $ posixSecondsToUTCTime 0 } diff --git a/server/src/Design/Appearing.hs b/server/src/Design/Appearing.hs deleted file mode 100644 index 79b94b3..0000000 --- a/server/src/Design/Appearing.hs +++ /dev/null @@ -1,25 +0,0 @@ -module Design.Appearing - ( design - ) where - -import Clay - -design :: Css -design = do - - appearKeyframe - - ".g-Appearing" ? do - appearAnimation - -appearAnimation :: Css -appearAnimation = do - animationName "appear" - animationDuration (sec 0.2) - animationTimingFunction easeIn - -appearKeyframe :: Css -appearKeyframe = keyframes - "appear" - [ (0, "opacity" -: "0") - ] diff --git a/server/src/Design/Color.hs b/server/src/Design/Color.hs deleted file mode 100644 index e7f5aec..0000000 --- a/server/src/Design/Color.hs +++ /dev/null @@ -1,40 +0,0 @@ -module Design.Color where - -import Clay -import qualified Clay.Color as C -import Data.Text (Text) - --- http://chir.ag/projects/name-that-color/#969696 - -white :: C.Color -white = C.white - -black :: C.Color -black = C.black - -chestnutRose :: C.Color -chestnutRose = C.rgb 207 92 86 - -unknown :: C.Color -unknown = C.rgb 86 92 207 - -mossGreen :: C.Color -mossGreen = C.rgb 159 210 165 - -gothic :: C.Color -gothic = C.rgb 108 162 164 - -negroni :: C.Color -negroni = C.rgb 255 223 196 - -wildSand :: C.Color -wildSand = C.rgb 245 245 245 - -silver :: C.Color -silver = C.rgb 200 200 200 - -dustyGray :: C.Color -dustyGray = C.rgb 150 150 150 - -toString :: C.Color -> Text -toString = plain . unValue . value diff --git a/server/src/Design/Constants.hs b/server/src/Design/Constants.hs deleted file mode 100644 index a3123d9..0000000 --- a/server/src/Design/Constants.hs +++ /dev/null @@ -1,27 +0,0 @@ -module Design.Constants where - -import Clay - -iconFontSize :: Size LengthUnit -iconFontSize = px 32 - -radius :: Size LengthUnit -radius = px 3 - -blockPadding :: Size LengthUnit -blockPadding = px 15 - -blockPercentWidth :: Double -blockPercentWidth = 90 - -blockPercentMargin :: Double -blockPercentMargin = (100 - blockPercentWidth) / 2 - -inputHeight :: Double -inputHeight = 40 - -focusLighten :: Color -> Color -focusLighten baseColor = baseColor +. 20 - -focusDarken :: Color -> Color -focusDarken baseColor = baseColor -. 20 diff --git a/server/src/Design/Errors.hs b/server/src/Design/Errors.hs deleted file mode 100644 index 9f435eb..0000000 --- a/server/src/Design/Errors.hs +++ /dev/null @@ -1,53 +0,0 @@ -module Design.Errors - ( design - ) where - -import Clay - -import Design.Color as Color - -design :: Css -design = do - position fixed - top (px 20) - left (pct 50) - "transform" -: "translateX(-50%)" - margin (px 0) (px 0) (px 0) (px 0) - disapearKeyframes - - ".error" ? do - disapearAnimation - let errorColor = Color.chestnutRose -. 15 - color errorColor - border solid (px 2) errorColor - backgroundColor Color.white - borderRadius (px 5) (px 5) (px 5) (px 5) - padding (px 5) (px 5) (px 5) (px 5) - - before & display none - -disapearAnimation :: Css -disapearAnimation = do - animationName "disapear" - animationDelay (sec 5) - animationDuration (sec 1) - animationFillMode forwards - -disapearKeyframes :: Css -disapearKeyframes = keyframes - "disapear" - [ ( 10 - , do - opacity 0 - height (px 40) - lineHeight (px 40) - marginBottom (px 10) - ) - , ( 100 - , do - opacity 0 - height (px 0) - lineHeight (px 0) - marginBottom (px 0) - ) - ] diff --git a/server/src/Design/Form.hs b/server/src/Design/Form.hs deleted file mode 100644 index 5713bfe..0000000 --- a/server/src/Design/Form.hs +++ /dev/null @@ -1,101 +0,0 @@ -module Design.Form - ( design - ) where - -import Data.Monoid ((<>)) - -import Clay - -import Design.Color as Color - -design :: Css -design = do - - let inputHeight = 30 - let inputTop = 22 - let inputPaddingBottom = 3 - - ".textInput" ? do - position relative - marginBottom (em 2) - paddingTop (px inputTop) - marginTop (px (-10)) - - input ? do - width (pct 100) - position relative - backgroundColor transparent - paddingBottom (px inputPaddingBottom) - paddingRight (px 14) -- Space for the delete icon - borderStyle none - borderBottom solid (px 1) Color.dustyGray - marginBottom (px 5) - height (px inputHeight) - lineHeight (px inputHeight) - focus & do - borderWidth (px 2) - paddingBottom (px $ inputPaddingBottom - 1) - - ".label" ? do - zIndex (-1) - color Color.silver - lineHeight (px inputHeight) - position absolute - top (px inputTop) - left (px 0) - transition "all" (sec 0.2) easeInOut (sec 0) - - button ? do - position absolute - right (px 0) - top (px 27) - svg ? "path" ? - ("fill" -: Color.toString Color.silver) - hover & svg ? "path" ? - ("fill" -: Color.toString (Color.silver -. 25)) - - (input # ".filled" |+ ".label") <> (input # focus |+ ".label") ? do - top (px 0) - fontSize (pct 80) - - ".error" & do - input ? do - borderBottomColor Color.chestnutRose - - ".errorMessage" ? do - position absolute - color Color.chestnutRose - fontSize (pct 80) - - ".colorInput" ? do - display flex - alignItems center - marginBottom (em 1.5) - - input ? do - borderColor transparent - backgroundColor transparent - - ".selectInput" ? do - - ".label" ? do - color Color.silver - display block - marginBottom (px 10) - fontSize (pct 80) - - select ? do - width (pct 100) - backgroundColor Color.white - border solid (px 1) Color.silver - sym borderRadius (px 3) - sym2 padding (px 5) (px 8) - option ? sym2 padding (px 5) (px 8) - focus & backgroundColor Color.wildSand - - ".error" & do - select ? borderColor Color.chestnutRose - ".errorMessage" ? do - color Color.chestnutRose - fontSize (pct 80) - marginTop (em 0.5) diff --git a/server/src/Design/Global.hs b/server/src/Design/Global.hs deleted file mode 100644 index c67db7c..0000000 --- a/server/src/Design/Global.hs +++ /dev/null @@ -1,165 +0,0 @@ -module Design.Global - ( globalDesign - ) where - -import Clay -import Clay.Color as C -import Data.Text.Lazy (Text) - -import qualified Design.Appearing as Appearing -import qualified Design.Color as Color -import qualified Design.Constants as Constants -import qualified Design.Errors as Errors -import qualified Design.Form as Form -import qualified Design.Helper as Helper -import qualified Design.Loadable as Loadable -import qualified Design.Media as Media -import qualified Design.Modal as Modal -import qualified Design.Tooltip as Tooltip -import qualified Design.Views as Views - -globalDesign :: Text -globalDesign = renderWith compact [] global - -global :: Css -global = do - ".errors" ? Errors.design - Appearing.design - Modal.design - ".tooltip" ? Tooltip.design - Views.design - Form.design - Loadable.design - - spinKeyframes - appearKeyframe - - html ? do - height (pct 100) - - "g-Body--Modal" ? - overflowY hidden - - body ? do - position relative - minWidth (px 320) - height (pct 100) - fontFamily ["Cantarell"] [sansSerif] - Media.tablet $ do - fontSize (px 15) - button ? fontSize (px 15) - input ? fontSize (px 15) - Media.mobile $ do - fontSize (px 14) - button ? fontSize (px 14) - input ? fontSize (px 14) - - ".app" ? do - appearAnimation - display flex - height (pct 100) - flexDirection column - - -- "main" ? - -- appearAnimation - - ".pageSpinner" ? do - display flex - alignItems center - justifyContent center - flexGrow 1 - - ".spinner" ? do - display flex - alignItems center - justifyContent center - width (pct 100) - height (pct 100) - paddingBottom (pct 10) - - before & do - display block - content (stringContent "") - width (px 50) - height (px 50) - border solid (px 3) (C.setA 0.3 Color.chestnutRose) - sym borderRadius (pct 50) - borderTopColor Color.chestnutRose - spinKeyframes - spinAnimation - - a ? cursor pointer - - input ? fontSize inherit - - h1 ? do - color Color.chestnutRose - lineHeight (em 1.3) - - Media.desktop $ fontSize (px 24) - Media.tablet $ fontSize (px 22) - Media.mobile $ fontSize (px 20) - - ul ? do - "margin-top" -: "1vh" - "margin-bottom" -: "3vh" - "margin-left" -: "1vh" - li <? do - "margin-bottom" -: "2vh" - before & do - content (stringContent "• ") - color Color.chestnutRose - "margin-right" -: "0.3vw" - ul <? do - "margin-left" -: "3vh" - "margin-top" -: "2vh" - - ".dialog" ? ".content" ? button ? do - ".confirm" & Helper.button Color.chestnutRose Color.white (px Constants.inputHeight) Constants.focusLighten - ".undo" & Helper.button Color.silver Color.white (px Constants.inputHeight) Constants.focusLighten - - svg ? height (pct 100) - - button ? do - position relative - - ".content" ? do - display flex - - svg # ".loader" ? do - display none - position absolute - - ".waiting" & do - ".content" ? do - opacity 0 - svg # ".loader" ? do - display block - spinAnimation - - select ? cursor pointer - -spinAnimation :: Css -spinAnimation = do - animationName "rotate" - animationDuration (sec 1) - animationTimingFunction easeInOut - animationIterationCount infinite - -spinKeyframes :: Css -spinKeyframes = keyframes - "rotate" - [ (100, "transform" -: "rotate(360deg)") - ] - -appearAnimation :: Css -appearAnimation = do - animationName "appear" - animationDuration (sec 0.2) - animationTimingFunction easeIn - -appearKeyframe :: Css -appearKeyframe = keyframes - "appear" - [ (0, "opacity" -: "0") - ] diff --git a/server/src/Design/Helper.hs b/server/src/Design/Helper.hs deleted file mode 100644 index e586d56..0000000 --- a/server/src/Design/Helper.hs +++ /dev/null @@ -1,48 +0,0 @@ -module Design.Helper - ( clearFix - , button - , centeredWithMargin - , verticalCentering - ) where - -import Prelude hiding (span) - -import Clay hiding (button) - -import Design.Constants - -clearFix :: Css -clearFix = - after & do - content (stringContent "") - display displayTable - clear both - -button :: Color -> Color -> Size a -> (Color -> Color) -> Css -button backgroundCol textCol h focusOp = do - display flex - alignItems center - justifyContent center - backgroundColor backgroundCol - padding (px 0) (px 10) (px 0) (px 10) - color textCol - borderRadius radius radius radius radius - verticalAlign middle - cursor pointer - lineHeight h - height h - textAlign (alignSide sideCenter) - hover & backgroundColor (focusOp backgroundCol) - focus & backgroundColor (focusOp backgroundCol) - -centeredWithMargin :: Css -centeredWithMargin = do - width (pct blockPercentWidth) - marginLeft auto - marginRight auto - -verticalCentering :: Css -verticalCentering = do - position absolute - top (pct 50) - "transform" -: "translateY(-50%)" diff --git a/server/src/Design/Loadable.hs b/server/src/Design/Loadable.hs deleted file mode 100644 index 6b13f2d..0000000 --- a/server/src/Design/Loadable.hs +++ /dev/null @@ -1,29 +0,0 @@ -module Design.Loadable - ( design - ) where - -import Clay - -design :: Css -design = do - ".g-Loadable" ? do - position relative - width (pct 100) - height (pct 100) - - ".g-Loadable__Spinner" ? do - position absolute - top (px 0) - left (px 0) - width (pct 100) - height (pct 100) - display none - - ".g-Loadable__Spinner--Loading" ? do - display block - - ".g-Loadable__Content" ? - transition "opacity" (sec 0.4) ease (sec 0) - - ".g-Loadable__Content--Loading" ? - opacity 0.5 diff --git a/server/src/Design/Media.hs b/server/src/Design/Media.hs deleted file mode 100644 index 19a3b8c..0000000 --- a/server/src/Design/Media.hs +++ /dev/null @@ -1,36 +0,0 @@ -module Design.Media - ( mobile - , mobileTablet - , tablet - , tabletDesktop - , desktop - ) where - -import Clay hiding (query) -import qualified Clay -import qualified Clay.Media as Media -import Clay.Stylesheet (Feature) - -mobile :: Css -> Css -mobile = query [Media.maxWidth mobileTabletLimit] - -mobileTablet :: Css -> Css -mobileTablet = query [Media.maxWidth tabletDesktopLimit] - -tablet :: Css -> Css -tablet = query [Media.minWidth mobileTabletLimit, Media.maxWidth tabletDesktopLimit] - -tabletDesktop :: Css -> Css -tabletDesktop = query [Media.minWidth mobileTabletLimit] - -desktop :: Css -> Css -desktop = query [Media.minWidth tabletDesktopLimit] - -query :: [Feature] -> Css -> Css -query = Clay.query Media.screen - -mobileTabletLimit :: Size LengthUnit -mobileTabletLimit = (px 520) - -tabletDesktopLimit :: Size LengthUnit -tabletDesktopLimit = (px 950) diff --git a/server/src/Design/Modal.hs b/server/src/Design/Modal.hs deleted file mode 100644 index 1195e10..0000000 --- a/server/src/Design/Modal.hs +++ /dev/null @@ -1,69 +0,0 @@ -module Design.Modal - ( design - ) where - -import Clay -import Data.Monoid ((<>)) - -import qualified Design.View.Payment.Form as Form - -design :: Css -design = do - - appearKeyframe - - ".g-Modal" ? do - display none - appearAnimation - transition "all" (sec 0.2) ease (sec 0) - opacity 0 - - ".g-Modal--Show" & do - display block - opacity 1 - - ".g-Modal--Hiding" & do - display block - - ".g-Modal__Curtain" ? do - position fixed - top (px 0) - left (px 0) - width (pct 100) - height (pct 100) - backgroundColor (rgba 0 0 0 0.6) - zIndex 1 - - ".g-Modal__Content" ? do - minWidth (px 300) - position fixed - top (pct 25) - left (pct 50) - "transform" -: "translate(-50%, -25%)" - zIndex 1 - backgroundColor white - sym borderRadius (px 5) - boxShadow . pure . bsColor (rgba 0 0 0 0.5) $ shadowWithBlur (px 0) (px 0) (px 15) - - ".form" ? Form.design - - ".paymentModal" & do - ".radioGroup" ? ".title" ? display none - ".selectInput" ? do - select ? width (pct 100) - marginBottom (em 1) - - ".deletePaymentModal" <> ".deleteIncomeModal" ? do - h1 ? marginBottom (em 1.5) - -appearAnimation :: Css -appearAnimation = do - animationName "appear" - animationDuration (sec 0.15) - animationTimingFunction easeIn - -appearKeyframe :: Css -appearKeyframe = keyframes - "appear" - [ (0, "opacity" -: "0") - ] diff --git a/server/src/Design/Tooltip.hs b/server/src/Design/Tooltip.hs deleted file mode 100644 index eef804e..0000000 --- a/server/src/Design/Tooltip.hs +++ /dev/null @@ -1,14 +0,0 @@ -module Design.Tooltip - ( design - ) where - -import Clay - -import Design.Color as Color - -design :: Css -design = do - backgroundColor Color.mossGreen - borderRadius (px 5) (px 5) (px 5) (px 5) - padding (px 5) (px 5) (px 5) (px 5) - color Color.white diff --git a/server/src/Design/View/ConfirmDialog.hs b/server/src/Design/View/ConfirmDialog.hs deleted file mode 100644 index 410d4d8..0000000 --- a/server/src/Design/View/ConfirmDialog.hs +++ /dev/null @@ -1,36 +0,0 @@ -module Design.View.ConfirmDialog - ( design - ) where - -import Clay - -import qualified Design.Color as Color -import qualified Design.Constants as Constants -import qualified Design.Helper as Helper - -design :: Css -design = do - ".confirm" ? do - ".confirmHeader" ? do - backgroundColor Color.chestnutRose - fontSize (px 18) - color Color.white - sym padding (px 20) - textAlign (alignSide sideCenter) - borderRadius (px 5) (px 5) (px 0) (px 0) - - ".confirmContent" ? do - sym padding (px 20) - - ".buttons" ? do - display flex - justifyContent spaceAround - marginTop (em 1.5) - - ".confirm" ? - Helper.button Color.chestnutRose Color.white (px Constants.inputHeight) Constants.focusLighten - ".undo" ? - Helper.button Color.silver Color.white (px Constants.inputHeight) Constants.focusLighten - - (".confirm" <> ".undo") ? - width (px 90) diff --git a/server/src/Design/View/Header.hs b/server/src/Design/View/Header.hs deleted file mode 100644 index 2ad0455..0000000 --- a/server/src/Design/View/Header.hs +++ /dev/null @@ -1,93 +0,0 @@ -module Design.View.Header - ( design - ) where - -import Data.Monoid ((<>)) - -import Clay - -import Design.Color as Color -import qualified Design.Media as Media - -desktopLineHeight :: Double -desktopLineHeight = 80 - -tabletLineHeight :: Double -tabletLineHeight = 60 - -mobileLineHeight :: Double -mobileLineHeight = 40 - -design :: Css -design = do - display flex - "flex-wrap" -: "wrap" - position relative - backgroundColor Color.chestnutRose - color Color.white - - Media.desktop $ do - minHeight (px desktopLineHeight) - lineHeight (px desktopLineHeight) - marginBottom (em 3) - Media.tablet $ do - minHeight (px (tabletLineHeight * 2)) - lineHeight (px tabletLineHeight) - marginBottom (em 2) - Media.mobile $ do - minHeight (px (mobileLineHeight * 2)) - lineHeight (px mobileLineHeight) - marginBottom (em 1.5) - - ".title" <> ".item" ? do - Media.tabletDesktop $ sym2 padding (px 0) (px 20) - Media.mobile $ sym2 padding (px 0) (px 10) - - ".title" ? do - textAlign (alignSide sideLeft) - - Media.desktop $ do - fontSize (px 35) - display inlineBlock - Media.tablet $ do - fontSize (px 28) - display inlineBlock - width (pct 100) - Media.mobile $ do - fontSize (px 22) - width (pct 100) - - ".item" ? do - display inlineBlock - transition "background-color" (ms 50) easeIn (sec 0) - ".current" & backgroundColor (Color.chestnutRose -. 20) - Media.mobile $ fontSize (px 13) - - (".item" # hover) <> (".item" # focus) ? - backgroundColor (Color.chestnutRose +. 10) - - (".item.current" # hover) <> (".item.current" # focus) ? - backgroundColor (Color.chestnutRose -. 10) - - ".nameSignOut" ? do - display flex - position absolute - top (px 0) - right (px 0) - - Media.desktop $ height (px desktopLineHeight) - Media.tablet $ height (px tabletLineHeight) - Media.mobile $ height (px mobileLineHeight) - - ".name" ? do - Media.mobile $ display none - Media.tabletDesktop $ sym2 padding (px 0) (px 20) - - ".signOut" ? do - display flex - justifyContent center - alignItems center - svg ? do - Media.tabletDesktop $ width (px 30) - Media.mobile $ width (px 20) - "path" ? ("fill" -: "white") diff --git a/server/src/Design/View/NotFound.hs b/server/src/Design/View/NotFound.hs deleted file mode 100644 index 150c6fc..0000000 --- a/server/src/Design/View/NotFound.hs +++ /dev/null @@ -1,21 +0,0 @@ -module Design.View.NotFound - ( design - ) where - -import Clay -import Prelude hiding (rem) - -import qualified Design.Color as Color - -design :: Css -design = do - - marginLeft (rem 3) - - ".link" ? do - display block - marginTop (rem 1) - color Color.chestnutRose - textDecoration underline - hover & - color (Color.chestnutRose +. 15) diff --git a/server/src/Design/View/Pages.hs b/server/src/Design/View/Pages.hs deleted file mode 100644 index 1482ef4..0000000 --- a/server/src/Design/View/Pages.hs +++ /dev/null @@ -1,55 +0,0 @@ -module Design.View.Pages - ( design - ) where - -import Clay - -import qualified Design.Color as Color -import qualified Design.Constants as Constants -import qualified Design.Helper as Helper -import qualified Design.Media as Media - -design :: Css -design = - ".pages" ? do - display flex - justifyContent center - - Media.desktop $ do - padding (px 40) (px 30) (px 30) (px 30) - - Media.tablet $ do - padding (px 30) (px 30) (px 30) (px 30) - - Media.mobile $ do - padding (px 20) (px 0) (px 20) (px 0) - lineHeight (px 40) - - svg ? "path" ? ("fill" -: Color.toString Color.dustyGray) - - ".page" ? do - display inlineBlock - fontWeight bold - - Media.desktop $ do - Helper.button Color.white Color.dustyGray (px 50) Constants.focusDarken - - Media.tabletDesktop $ do - border solid (px 2) Color.dustyGray - marginRight (px 10) - - Media.tablet $ do - Helper.button Color.white Color.dustyGray (px 40) Constants.focusDarken - fontSize (px 15) - - Media.mobile $ do - Helper.button Color.white Color.dustyGray (px 30) Constants.focusDarken - fontSize (px 12) - border solid (px 1) Color.dustyGray - marginRight (px 5) - - ":not(.current)" & cursor pointer - - ".current" & do - borderColor Color.chestnutRose - color Color.chestnutRose diff --git a/server/src/Design/View/Payment.hs b/server/src/Design/View/Payment.hs deleted file mode 100644 index 94e4f85..0000000 --- a/server/src/Design/View/Payment.hs +++ /dev/null @@ -1,15 +0,0 @@ -module Design.View.Payment - ( design - ) where - -import Clay - -import qualified Design.Color as Color -import qualified Design.View.Payment.HeaderForm as HeaderForm -import qualified Design.View.Payment.HeaderInfos as HeaderInfos - -design :: Css -design = do - HeaderForm.design - HeaderInfos.design - ".g-Payment__Refund" ? color Color.mossGreen diff --git a/server/src/Design/View/Payment/Add.hs b/server/src/Design/View/Payment/Add.hs deleted file mode 100644 index 5ecae7a..0000000 --- a/server/src/Design/View/Payment/Add.hs +++ /dev/null @@ -1,35 +0,0 @@ -module Design.View.Payment.Add - ( design - ) where - -import Clay - -import qualified Design.Color as Color -import qualified Design.Constants as Constants -import qualified Design.Helper as Helper - -design :: Css -design = do - ".addHeader" ? do - backgroundColor Color.chestnutRose - fontSize (px 18) - color Color.white - sym2 padding (px 20) (px 30) - textAlign (alignSide sideCenter) - borderRadius (px 5) (px 5) (px 0) (px 0) - - ".addContent" ? do - sym2 padding (px 20) (px 30) - - ".buttons" ? do - display flex - justifyContent spaceAround - marginTop (em 1.5) - - ".confirm" ? - Helper.button Color.chestnutRose Color.white (px Constants.inputHeight) Constants.focusLighten - ".undo" ? - Helper.button Color.silver Color.white (px Constants.inputHeight) Constants.focusLighten - - (".confirm" <> ".undo") ? - width (px 90) diff --git a/server/src/Design/View/Payment/Form.hs b/server/src/Design/View/Payment/Form.hs deleted file mode 100644 index aada12b..0000000 --- a/server/src/Design/View/Payment/Form.hs +++ /dev/null @@ -1,35 +0,0 @@ -module Design.View.Payment.Form - ( design - ) where - -import Clay - -import qualified Design.Color as Color -import qualified Design.Constants as Constants -import qualified Design.Helper as Helper - -design :: Css -design = do - ".formHeader" ? do - backgroundColor Color.chestnutRose - fontSize (px 18) - color Color.white - sym2 padding (px 20) (px 30) - textAlign (alignSide sideCenter) - borderRadius (px 5) (px 5) (px 0) (px 0) - - ".formContent" ? do - sym2 padding (px 20) (px 30) - - ".buttons" ? do - display flex - justifyContent spaceAround - marginTop (em 1.5) - - ".confirm" ? - Helper.button Color.chestnutRose Color.white (px Constants.inputHeight) Constants.focusLighten - ".undo" ? - Helper.button Color.silver Color.white (px Constants.inputHeight) Constants.focusLighten - - (".confirm" <> ".undo") ? - width (px 90) diff --git a/server/src/Design/View/Payment/HeaderForm.hs b/server/src/Design/View/Payment/HeaderForm.hs deleted file mode 100644 index 6081443..0000000 --- a/server/src/Design/View/Payment/HeaderForm.hs +++ /dev/null @@ -1,40 +0,0 @@ -module Design.View.Payment.HeaderForm - ( design - ) where - -import Clay - -import qualified Design.Color as Color -import qualified Design.Constants as Constants -import qualified Design.Helper as Helper -import qualified Design.Media as Media - -design :: Css -design = do - - ".g-PaymentHeaderForm" ? do - marginBottom (em 2) - marginLeft (pct Constants.blockPercentMargin) - marginRight (pct Constants.blockPercentMargin) - display flex - justifyContent spaceBetween - alignItems center - Media.mobile $ flexDirection column - - ".textInput" ? do - display inlineBlock - marginBottom (px 0) - - Media.tabletDesktop $ marginRight (px 30) - Media.mobile $ do - marginBottom (em 1) - width (pct 100) - - ".selectInput" ? do - Media.tabletDesktop $ display inlineBlock - Media.mobile $ marginBottom (em 2) - - ".addPayment" ? do - Helper.button Color.chestnutRose Color.white (px Constants.inputHeight) Constants.focusLighten - Media.mobile $ width (pct 100) - flexShrink 0 diff --git a/server/src/Design/View/Payment/HeaderInfos.hs b/server/src/Design/View/Payment/HeaderInfos.hs deleted file mode 100644 index acb393b..0000000 --- a/server/src/Design/View/Payment/HeaderInfos.hs +++ /dev/null @@ -1,50 +0,0 @@ -module Design.View.Payment.HeaderInfos - ( design - ) where - -import Data.Monoid ((<>)) - -import Clay - -import qualified Design.Color as Color -import qualified Design.Constants as Constants -import qualified Design.Media as Media - -design :: Css -design = do - - ".g-PaymentHeaderInfos" ? do - Media.desktop $ marginBottom (em 2) - Media.mobileTablet $ marginBottom (em 1) - marginLeft (pct Constants.blockPercentMargin) - marginRight (pct Constants.blockPercentMargin) - - ".g-PaymentHeaderInfos__ExceedingPayers" ? do - backgroundColor Color.mossGreen - borderRadius (px 5) (px 5) (px 5) (px 5) - color Color.white - lineHeight (px Constants.inputHeight) - paddingLeft (px 10) - paddingRight (px 10) - marginBottom (em 1) - - Media.mobile $ do - textAlign (alignSide sideCenter) - - ".exceedingPayer:not(:last-child)::after" ? content (stringContent ", ") - - ".userName" ? marginRight (px 8) - - ".g-PaymentHeaderInfos__Repartition" ? do - Media.tabletDesktop $ lineHeight (px Constants.inputHeight) - Media.mobile $ lineHeight (px 25) - - ".total" <> ".partition" ? do - Media.mobileTablet $ display block - Media.mobile $ do - fontSize (pct 90) - textAlign (alignSide sideCenter) - - ".partition" ? do - color Color.dustyGray - Media.desktop $ marginLeft (px 15) diff --git a/server/src/Design/View/SignIn.hs b/server/src/Design/View/SignIn.hs deleted file mode 100644 index 42c9621..0000000 --- a/server/src/Design/View/SignIn.hs +++ /dev/null @@ -1,36 +0,0 @@ -module Design.View.SignIn - ( design - ) where - -import Clay -import Data.Monoid ((<>)) -import Prelude hiding (rem) - -import qualified Design.Color as Color -import qualified Design.Constants as Constants -import qualified Design.Helper as Helper - -design :: Css -design = do - let inputHeight = 50 - width (px 350) - sym2 padding (rem 0) (rem 2) - marginTop (px 100) - marginLeft auto - marginRight auto - - button # ".validate" ? do - Helper.button Color.gothic Color.white (px inputHeight) Constants.focusLighten - display flex - alignItems center - justifyContent center - width (pct 100) - fontSize (em 1.2) - svg ? "path" ? ("fill" -: "white") - - ".success" <> ".error" ? do - marginTop (px 40) - textAlign (alignSide sideCenter) - - ".success" ? color Color.mossGreen - ".error" ? color Color.chestnutRose diff --git a/server/src/Design/View/Stat.hs b/server/src/Design/View/Stat.hs deleted file mode 100644 index 2e4ecad..0000000 --- a/server/src/Design/View/Stat.hs +++ /dev/null @@ -1,17 +0,0 @@ -module Design.View.Stat - ( design - ) where - -import Clay - -design :: Css -design = do - h1 ? paddingBottom (px 0) - - ".exceedingPayers" ? ".userName" ? marginRight (px 5) - - ".mean" ? marginBottom (em 1.5) - - ".g-Chart" ? do - width (pct 75) - sym2 margin (px 0) auto diff --git a/server/src/Design/View/Table.hs b/server/src/Design/View/Table.hs deleted file mode 100644 index 56bd389..0000000 --- a/server/src/Design/View/Table.hs +++ /dev/null @@ -1,99 +0,0 @@ -module Design.View.Table - ( design - ) where - -import Data.Monoid ((<>)) - -import Clay - -import Design.Color as Color -import qualified Design.Media as Media - -design :: Css -design = do - ".emptyTableMsg" ? do - margin (em 2) (em 2) (em 2) (em 2) - textAlign (alignSide sideCenter) - - ".table" ? do - minHeight (px 540) - - ".lines" ? do - Media.tabletDesktop $ display displayTable - width (pct 100) - textAlign (alignSide (sideCenter)) - - ".header" <> ".row" ? do - Media.tabletDesktop $ display tableRow - - ".header" ? do - Media.desktop $ do - fontSize (px 18) - height (px 70) - - Media.tabletDesktop $ do - backgroundColor Color.gothic - color Color.white - - Media.tablet $ do - fontSize (px 16) - height (px 60) - - Media.mobile $ do - display none - - ".row" ? do - nthChild "even" & backgroundColor Color.wildSand - - Media.desktop $ do - fontSize (px 18) - height (px 60) - - Media.tablet $ do - height (px 50) - - Media.mobile $ do - lineHeight (px 25) - paddingTop (px 10) - paddingBottom (px 10) - - ".cell" ? do - Media.tabletDesktop $ display tableCell - position relative - verticalAlign middle - - firstChild & do - Media.mobile $ do - fontSize (px 20) - lineHeight (px 30) - color Color.gothic - - ".refund" & color Color.mossGreen - - Media.desktop $ do - ".shortDate" ? display none - ".longDate" ? display inline - Media.tablet $ do - ".shortDate" ? display inline - ".longDate" ? display none - Media.mobile $ do - ".shortDate" ? display none - ".longDate" ? display inline - marginBottom (em 0.5) - - ".cell.button" & do - position relative - textAlign (alignSide sideCenter) - button ? do - padding (px 10) (px 10) (px 10) (px 10) - svg ? do - "path" ? ("fill" -: Color.toString Color.chestnutRose) - width (px 18) - hover & "svg path" ? do - "fill" -: "rgb(237, 122, 116)" - - Media.tabletDesktop $ width (pct 3) - - Media.mobile $ do - display inlineBlock - button ? display flex diff --git a/server/src/Design/Views.hs b/server/src/Design/Views.hs deleted file mode 100644 index 4552796..0000000 --- a/server/src/Design/Views.hs +++ /dev/null @@ -1,56 +0,0 @@ -module Design.Views - ( design - ) where - -import Clay - -import qualified Design.Color as Color -import qualified Design.Constants as Constants -import qualified Design.Helper as Helper -import qualified Design.Media as Media -import qualified Design.View.ConfirmDialog as ConfirmDialog -import qualified Design.View.Header as Header -import qualified Design.View.NotFound as NotFound -import qualified Design.View.Pages as Pages -import qualified Design.View.Payment as Payment -import qualified Design.View.SignIn as SignIn -import qualified Design.View.Stat as Stat -import qualified Design.View.Table as Table - -design :: Css -design = do - header ? Header.design - Payment.design - ".signIn" ? SignIn.design - Stat.design - ".notfound" ? NotFound.design - Table.design - Pages.design - ConfirmDialog.design - - ".withMargin" ? do - "margin" -: "0 2vw" - - ".titleButton" ? do - display flex - marginBottom (em 1) - - Media.tabletDesktop $ do - justifyContent spaceBetween - alignItems center - - Media.mobile $ do - flexDirection column - "h1" ? marginBottom (em 0.5) - - button ? do - Helper.button Color.chestnutRose Color.white (px Constants.inputHeight) Constants.focusLighten - Media.mobile $ do - width (pct 100) - marginBottom (px 20) - - ".tag" ? do - sym borderRadius (px 4) - sym2 padding (px 2) (px 5) - boxShadow . pure . bsColor (rgba 0 0 0 0.3) $ shadowWithBlur (px 2) (px 2) (px 5) - color Color.white diff --git a/server/src/Job/Daemon.hs b/server/src/Job/Daemon.hs deleted file mode 100644 index d8cd522..0000000 --- a/server/src/Job/Daemon.hs +++ /dev/null @@ -1,37 +0,0 @@ -module Job.Daemon - ( runDaemons - ) where - -import Control.Concurrent (ThreadId, forkIO, threadDelay) -import Control.Monad (forever) -import Data.Time.Clock (UTCTime) - -import Conf (Conf) -import Job.Frequency (Frequency (..), microSeconds) -import Job.Kind (Kind (..)) -import Job.Model (actualizeLastCheck, actualizeLastExecution, - getLastExecution) -import Job.MonthlyPayment (monthlyPayment) -import Job.WeeklyReport (weeklyReport) -import qualified Model.Query as Query -import Util.Time (belongToCurrentMonth, belongToCurrentWeek) - -runDaemons :: Conf -> IO () -runDaemons conf = do - _ <- runDaemon MonthlyPayment EveryHour (fmap not . belongToCurrentMonth) monthlyPayment - _ <- runDaemon WeeklyReport EveryHour (fmap not . belongToCurrentWeek) (weeklyReport conf) - return () - -runDaemon :: Kind -> Frequency -> (UTCTime -> IO Bool) -> (Maybe UTCTime -> IO UTCTime) -> IO ThreadId -runDaemon kind frequency isLastExecutionTooOld runJob = - forkIO . forever $ do - mbLastExecution <- Query.run $ do - actualizeLastCheck kind - getLastExecution kind - hasToRun <- case mbLastExecution of - Just lastExecution -> isLastExecutionTooOld lastExecution - Nothing -> return True - if hasToRun - then runJob mbLastExecution >>= (Query.run . actualizeLastExecution kind) - else return () - threadDelay . microSeconds $ frequency diff --git a/server/src/Job/Frequency.hs b/server/src/Job/Frequency.hs deleted file mode 100644 index c5bef42..0000000 --- a/server/src/Job/Frequency.hs +++ /dev/null @@ -1,13 +0,0 @@ -module Job.Frequency - ( Frequency(..) - , microSeconds - ) where - -data Frequency = - EveryHour - | EveryDay - deriving (Eq, Read, Show) - -microSeconds :: Frequency -> Int -microSeconds EveryHour = 1000000 * 60 * 60 -microSeconds EveryDay = (microSeconds EveryHour) * 24 diff --git a/server/src/Job/Kind.hs b/server/src/Job/Kind.hs deleted file mode 100644 index 17997f7..0000000 --- a/server/src/Job/Kind.hs +++ /dev/null @@ -1,23 +0,0 @@ -module Job.Kind - ( Kind(..) - ) where - -import qualified Data.Text as T -import Database.SQLite.Simple (SQLData (SQLText)) -import Database.SQLite.Simple.FromField (FromField (fromField), - fieldData) -import Database.SQLite.Simple.Ok (Ok (Errors, Ok)) -import Database.SQLite.Simple.ToField (ToField (toField)) - -data Kind = - MonthlyPayment - | WeeklyReport - deriving (Eq, Show, Read) - -instance FromField Kind where - fromField field = case fieldData field of - SQLText text -> Ok (read (T.unpack text) :: Kind) - _ -> Errors [error "SQLText field required for job kind"] - -instance ToField Kind where - toField kind = SQLText . T.pack . show $ kind diff --git a/server/src/Job/Model.hs b/server/src/Job/Model.hs deleted file mode 100644 index 1dd6c63..0000000 --- a/server/src/Job/Model.hs +++ /dev/null @@ -1,49 +0,0 @@ -module Job.Model - ( Job(..) - , getLastExecution - , actualizeLastExecution - , actualizeLastCheck - ) where - -import Data.Time.Clock (UTCTime, getCurrentTime) -import Database.SQLite.Simple (Only (Only)) -import qualified Database.SQLite.Simple as SQLite -import Prelude hiding (id) - -import Job.Kind -import Model.Query (Query (Query)) - -data Job = Job - { id :: String - , kind :: Kind - , lastExecution :: Maybe UTCTime - , lastCheck :: Maybe UTCTime - } deriving (Show) - -getLastExecution :: Kind -> Query (Maybe UTCTime) -getLastExecution jobKind = - Query (\conn -> do - result <- SQLite.query conn "SELECT last_execution FROM job WHERE kind = ?" (Only jobKind) :: IO [Only UTCTime] - return $ case result of - [Only time] -> Just time - _ -> Nothing - ) - -actualizeLastExecution :: Kind -> UTCTime -> Query () -actualizeLastExecution jobKind time = - Query (\conn -> do - result <- SQLite.query conn "SELECT 1 FROM job WHERE kind = ?" (Only jobKind) :: IO [Only Int] - let hasJob = case result of - [Only _] -> True - _ -> False - if hasJob - then SQLite.execute conn "UPDATE job SET last_execution = ? WHERE kind = ?" (time, jobKind) - else SQLite.execute conn "INSERT INTO job (kind, last_execution, last_check) VALUES (?, ?, ?)" (jobKind, time, time) - ) - -actualizeLastCheck :: Kind -> Query () -actualizeLastCheck jobKind = - Query (\conn -> do - now <- getCurrentTime - SQLite.execute conn "UPDATE job SET kind = ? WHERE last_check = ?" (jobKind, now) - ) diff --git a/server/src/Job/MonthlyPayment.hs b/server/src/Job/MonthlyPayment.hs deleted file mode 100644 index dfbe8b4..0000000 --- a/server/src/Job/MonthlyPayment.hs +++ /dev/null @@ -1,26 +0,0 @@ -module Job.MonthlyPayment - ( monthlyPayment - ) where - -import Data.Time.Clock (UTCTime, getCurrentTime) - -import Common.Model (Frequency (..), Payment (..)) -import qualified Common.Util.Time as Time - -import qualified Model.Query as Query -import qualified Persistence.Payment as PaymentPersistence - -monthlyPayment :: Maybe UTCTime -> IO UTCTime -monthlyPayment _ = do - monthlyPayments <- Query.run PaymentPersistence.listActiveMonthlyOrderedByName - now <- getCurrentTime - actualDay <- Time.timeToDay now - let punctualPayments = map - (\p -> p - { _payment_frequency = Punctual - , _payment_date = actualDay - , _payment_createdAt = now - }) - monthlyPayments - _ <- Query.run (PaymentPersistence.createMany punctualPayments) - return now diff --git a/server/src/Job/WeeklyReport.hs b/server/src/Job/WeeklyReport.hs deleted file mode 100644 index 282f2f1..0000000 --- a/server/src/Job/WeeklyReport.hs +++ /dev/null @@ -1,52 +0,0 @@ -module Job.WeeklyReport - ( weeklyReport - ) where - -import qualified Data.Map as M -import qualified Data.Time.Clock as Clock -import Data.Time.Clock (UTCTime, getCurrentTime) - -import Common.Model (User (..)) - -import Conf (Conf) -import qualified Model.Query as Query -import qualified Persistence.Income as IncomePersistence -import qualified Persistence.Payment as PaymentPersistence -import qualified Persistence.User as UserPersistence -import qualified SendMail -import qualified View.Mail.WeeklyReport as WeeklyReport - -weeklyReport :: Conf -> Maybe UTCTime -> IO UTCTime -weeklyReport conf mbLastExecution = do - now <- getCurrentTime - - case mbLastExecution of - Nothing -> - return () - - Just lastExecution -> do - (weekPayments, cumulativeIncome, preIncomeRepartition, postIncomeRepartition, weekIncomes, users) <- Query.run $ do - users <- UserPersistence.list - paymentRange <- PaymentPersistence.getRange - incomeDefinedForAll <- IncomePersistence.definedForAll (_user_id <$> users) - cumulativeIncome <- - case (incomeDefinedForAll, paymentRange) of - (Just incomeStart, Just (paymentStart, _)) -> - IncomePersistence.getCumulativeIncome (max incomeStart paymentStart) (Clock.utctDay now) - - _ -> - return M.empty - weekPayments <- PaymentPersistence.listModifiedPunctualSince lastExecution - weekIncomes <- IncomePersistence.listModifiedSince lastExecution - (preIncomeRepartition, postIncomeRepartition) <- - PaymentPersistence.getPreAndPostPaymentRepartition paymentRange users - return (weekPayments, cumulativeIncome, preIncomeRepartition, postIncomeRepartition, weekIncomes, users) - - _ <- - SendMail.sendMail - conf - (WeeklyReport.mail conf users weekIncomes weekPayments cumulativeIncome preIncomeRepartition postIncomeRepartition lastExecution now) - - return () - - return now diff --git a/server/src/LoginSession.hs b/server/src/LoginSession.hs deleted file mode 100644 index 86f1329..0000000 --- a/server/src/LoginSession.hs +++ /dev/null @@ -1,52 +0,0 @@ -module LoginSession - ( put - , get - , delete - ) where - -import Cookie (deleteCookie, getCookie, - setSimpleCookie) -import qualified Web.ClientSession as CS -import Web.Scotty (ActionM) - -import Control.Monad.IO.Class (liftIO) - -import Data.Text (Text) -import qualified Data.Text.Encoding as TE - -import Conf (Conf) - -sessionName :: Text -sessionName = "SESSION" - -sessionKeyFile :: FilePath -sessionKeyFile = "sessionKey" - -put :: Conf -> Text -> ActionM () -put conf value = do - encrypted <- liftIO $ encrypt value - setSimpleCookie conf sessionName encrypted - -encrypt :: Text -> IO Text -encrypt value = do - iv <- CS.randomIV - key <- CS.getKey sessionKeyFile - return . TE.decodeUtf8 $ CS.encrypt key iv (TE.encodeUtf8 value) - -get :: ActionM (Maybe Text) -get = do - maybeEncrypted <- getCookie sessionName - case maybeEncrypted of - Just encrypted -> - liftIO $ decrypt encrypted - Nothing -> - return Nothing - -decrypt :: Text -> IO (Maybe Text) -decrypt encrypted = do - key <- CS.getKey sessionKeyFile - let decrypted = TE.decodeUtf8 <$> CS.decrypt key (TE.encodeUtf8 encrypted) - return decrypted - -delete :: Conf -> ActionM () -delete conf = deleteCookie conf sessionName diff --git a/server/src/Main.hs b/server/src/Main.hs deleted file mode 100644 index 659a0fa..0000000 --- a/server/src/Main.hs +++ /dev/null @@ -1,106 +0,0 @@ -module Main - ( main - ) where - -import qualified Network.HTTP.Types.Status as Status -import Network.Wai.Middleware.Gzip (GzipFiles (GzipCompress)) -import qualified Network.Wai.Middleware.Gzip as W -import Network.Wai.Middleware.Static -import qualified Web.Scotty as S - -import qualified Conf -import qualified Controller.Category as Category -import qualified Controller.Income as Income -import qualified Controller.Index as Index -import qualified Controller.Payment as Payment -import qualified Controller.Statistics as Statistics -import qualified Controller.User as User -import qualified Design.Global as Design -import Job.Daemon (runDaemons) - -main :: IO () -main = do - conf <- Conf.get "application.conf" - putStrLn . show $ conf - _ <- runDaemons conf - S.scotty (Conf.port conf) $ do - - S.middleware $ - W.gzip $ W.def { W.gzipFiles = GzipCompress } - - S.middleware . staticPolicy $ - noDots >-> addBase "public" - - S.get "/css/main.css" $ do - S.setHeader "Content-Type" "text/css" - S.text Design.globalDesign - - S.post "/api/signIn" $ - S.jsonData >>= Index.signIn conf - - S.post "/api/signOut" $ - Index.signOut conf - - S.get "/api/users"$ - User.list - - S.get "/api/payments" $ do - frequency <- S.param "frequency" - page <- S.param "page" - perPage <- S.param "perPage" - search <- S.param "search" - Payment.list (read frequency) page perPage search - - S.get "/api/payment/category" $ do - name <- S.param "name" - Payment.searchCategory name - - S.post "/api/payment" $ - S.jsonData >>= Payment.create - - S.put "/api/payment" $ - S.jsonData >>= Payment.edit - - S.delete "/api/payment/:id" $ do - paymentId <- S.param "id" - Payment.delete paymentId - - S.get "/api/incomes" $ do - page <- S.param "page" - perPage <- S.param "perPage" - Income.list page perPage - - S.post "/api/income" $ - S.jsonData >>= Income.create - - S.put "/api/income" $ - S.jsonData >>= Income.edit - - S.delete "/api/income/:id" $ do - incomeId <- S.param "id" - Income.delete incomeId - - S.get "/api/allCategories" $ do - Category.listAll - - S.get "/api/categories" $ do - page <- S.param "page" - perPage <- S.param "perPage" - Category.list page perPage - - S.post "/api/category" $ - S.jsonData >>= Category.create - - S.put "/api/category" $ - S.jsonData >>= Category.edit - - S.delete "/api/category/:id" $ do - categoryId <- S.param "id" - Category.delete categoryId - - S.get "/api/statistics" $ do - Statistics.paymentsAndIncomes - - S.notFound $ do - S.status Status.ok200 - Index.get conf diff --git a/server/src/Model/CreateCategory.hs b/server/src/Model/CreateCategory.hs deleted file mode 100644 index dae061b..0000000 --- a/server/src/Model/CreateCategory.hs +++ /dev/null @@ -1,10 +0,0 @@ -module Model.CreateCategory - ( CreateCategory(..) - ) where - -import Data.Text (Text) - -data CreateCategory = CreateCategory - { _createCategory_name :: Text - , _createCategory_color :: Text - } deriving (Show) diff --git a/server/src/Model/CreateIncome.hs b/server/src/Model/CreateIncome.hs deleted file mode 100644 index 82451d2..0000000 --- a/server/src/Model/CreateIncome.hs +++ /dev/null @@ -1,10 +0,0 @@ -module Model.CreateIncome - ( CreateIncome(..) - ) where - -import Data.Time.Calendar (Day) - -data CreateIncome = CreateIncome - { _createIncome_amount :: Int - , _createIncome_date :: Day - } deriving (Show) diff --git a/server/src/Model/CreatePayment.hs b/server/src/Model/CreatePayment.hs deleted file mode 100644 index b25d2a4..0000000 --- a/server/src/Model/CreatePayment.hs +++ /dev/null @@ -1,16 +0,0 @@ -module Model.CreatePayment - ( CreatePayment(..) - ) where - -import Data.Text (Text) -import Data.Time.Calendar (Day) - -import Common.Model (CategoryId, Frequency) - -data CreatePayment = CreatePayment - { _createPayment_name :: Text - , _createPayment_cost :: Int - , _createPayment_date :: Day - , _createPayment_category :: CategoryId - , _createPayment_frequency :: Frequency - } deriving (Show) diff --git a/server/src/Model/EditCategory.hs b/server/src/Model/EditCategory.hs deleted file mode 100644 index 8ee26ac..0000000 --- a/server/src/Model/EditCategory.hs +++ /dev/null @@ -1,13 +0,0 @@ -module Model.EditCategory - ( EditCategory(..) - ) where - -import Data.Text (Text) - -import Common.Model (CategoryId) - -data EditCategory = EditCategory - { _editCategory_id :: CategoryId - , _editCategory_name :: Text - , _editCategory_color :: Text - } deriving (Show) diff --git a/server/src/Model/EditIncome.hs b/server/src/Model/EditIncome.hs deleted file mode 100644 index ac3d311..0000000 --- a/server/src/Model/EditIncome.hs +++ /dev/null @@ -1,13 +0,0 @@ -module Model.EditIncome - ( EditIncome(..) - ) where - -import Data.Time.Calendar (Day) - -import Common.Model (IncomeId) - -data EditIncome = EditIncome - { _editIncome_id :: IncomeId - , _editIncome_amount :: Int - , _editIncome_date :: Day - } deriving (Show) diff --git a/server/src/Model/EditPayment.hs b/server/src/Model/EditPayment.hs deleted file mode 100644 index ac4c906..0000000 --- a/server/src/Model/EditPayment.hs +++ /dev/null @@ -1,17 +0,0 @@ -module Model.EditPayment - ( EditPayment(..) - ) where - -import Data.Text (Text) -import Data.Time.Calendar (Day) - -import Common.Model (CategoryId, Frequency, PaymentId) - -data EditPayment = EditPayment - { _editPayment_id :: PaymentId - , _editPayment_name :: Text - , _editPayment_cost :: Int - , _editPayment_date :: Day - , _editPayment_category :: CategoryId - , _editPayment_frequency :: Frequency - } deriving (Show) diff --git a/server/src/Model/HashedPassword.hs b/server/src/Model/HashedPassword.hs deleted file mode 100644 index c71e372..0000000 --- a/server/src/Model/HashedPassword.hs +++ /dev/null @@ -1,27 +0,0 @@ -module Model.HashedPassword - ( hash - , check - , HashedPassword(..) - ) where - -import qualified Crypto.BCrypt as BCrypt -import Data.Text (Text) -import qualified Data.Text.Encoding as TE - -import Common.Model.Password (Password (..)) - -newtype HashedPassword = HashedPassword Text deriving (Show) - -hash :: Password -> IO (Maybe HashedPassword) -hash (Password p) = do - hashed <- BCrypt.hashPasswordUsingPolicy BCrypt.slowerBcryptHashingPolicy (TE.encodeUtf8 p) - case hashed of - Nothing -> - return Nothing - - Just h -> - return . Just . HashedPassword . TE.decodeUtf8 $ h - -check :: Password -> HashedPassword -> Bool -check (Password p) (HashedPassword h) = - BCrypt.validatePassword (TE.encodeUtf8 h) (TE.encodeUtf8 p) diff --git a/server/src/Model/IncomeResource.hs b/server/src/Model/IncomeResource.hs deleted file mode 100644 index 6ab5f18..0000000 --- a/server/src/Model/IncomeResource.hs +++ /dev/null @@ -1,15 +0,0 @@ -module Model.IncomeResource - ( IncomeResource(..) - ) where - -import Common.Model (Income (..)) - -import Resource (Resource, resourceCreatedAt, resourceDeletedAt, - resourceEditedAt) - -newtype IncomeResource = IncomeResource Income - -instance Resource IncomeResource where - resourceCreatedAt (IncomeResource i) = _income_createdAt i - resourceEditedAt (IncomeResource i) = _income_editedAt i - resourceDeletedAt (IncomeResource i) = _income_deletedAt i diff --git a/server/src/Model/Mail.hs b/server/src/Model/Mail.hs deleted file mode 100644 index 780efcc..0000000 --- a/server/src/Model/Mail.hs +++ /dev/null @@ -1,12 +0,0 @@ -module Model.Mail - ( Mail(..) - ) where - -import Data.Text (Text) - -data Mail = Mail - { from :: Text - , to :: [Text] - , subject :: Text - , body :: Text - } deriving (Eq, Show) diff --git a/server/src/Model/PaymentResource.hs b/server/src/Model/PaymentResource.hs deleted file mode 100644 index 1ea978c..0000000 --- a/server/src/Model/PaymentResource.hs +++ /dev/null @@ -1,15 +0,0 @@ -module Model.PaymentResource - ( PaymentResource(..) - ) where - -import Common.Model (Payment (..)) - -import Resource (Resource, resourceCreatedAt, resourceDeletedAt, - resourceEditedAt) - -newtype PaymentResource = PaymentResource Payment - -instance Resource PaymentResource where - resourceCreatedAt (PaymentResource p) = _payment_createdAt p - resourceEditedAt (PaymentResource p) = _payment_editedAt p - resourceDeletedAt (PaymentResource p) = _payment_deletedAt p diff --git a/server/src/Model/Query.hs b/server/src/Model/Query.hs deleted file mode 100644 index 22ae95b..0000000 --- a/server/src/Model/Query.hs +++ /dev/null @@ -1,32 +0,0 @@ -module Model.Query - ( Query(..) - , run - ) where - -import Data.Functor (Functor) -import Database.SQLite.Simple (Connection) -import qualified Database.SQLite.Simple as SQLite - -data Query a = Query (Connection -> IO a) - -instance Functor Query where - fmap f (Query call) = Query (fmap f . call) - -instance Applicative Query where - pure x = Query (const $ return x) - (Query callF) <*> (Query callX) = Query (\conn -> do - x <- callX conn - f <- callF conn - return (f x)) - -instance Monad Query where - (Query callX) >>= f = Query (\conn -> do - x <- callX conn - case f x of Query callY -> callY conn) - -run :: Query a -> IO a -run (Query call) = do - conn <- SQLite.open "database" - result <- call conn - _ <- SQLite.close conn - return result diff --git a/server/src/Model/SignIn.hs b/server/src/Model/SignIn.hs deleted file mode 100644 index a217bae..0000000 --- a/server/src/Model/SignIn.hs +++ /dev/null @@ -1,10 +0,0 @@ -module Model.SignIn - ( SignIn(..) - ) where - -import Common.Model (Email, Password) - -data SignIn = SignIn - { _signIn_email :: Email - , _signIn_password :: Password - } deriving Show diff --git a/server/src/Model/UUID.hs b/server/src/Model/UUID.hs deleted file mode 100644 index 0959a8e..0000000 --- a/server/src/Model/UUID.hs +++ /dev/null @@ -1,10 +0,0 @@ -module Model.UUID - ( generateUUID - ) where - -import Data.Text (Text, pack) -import Data.UUID (toString) -import Data.UUID.V4 (nextRandom) - -generateUUID :: IO Text -generateUUID = pack . toString <$> nextRandom diff --git a/server/src/Payer.hs b/server/src/Payer.hs deleted file mode 100644 index ab8312e..0000000 --- a/server/src/Payer.hs +++ /dev/null @@ -1,87 +0,0 @@ -module Payer - ( getExceedingPayers - ) where - -import Data.Map (Map) -import qualified Data.Map as M - -import Common.Model (ExceedingPayer (..), User (..), UserId) - -data Payer = Payer - { _payer_userId :: UserId - , _payer_preIncomePayments :: Int - , _payer_postIncomePayments :: Int - , _payer_income :: Int - } - -data PostPaymentPayer = PostPaymentPayer - { _postPaymentPayer_userId :: UserId - , _postPaymentPayer_preIncomePayments :: Int - , _postPaymentPayer_cumulativeIncome :: Int - , _postPaymentPayer_ratio :: Float - } - -getExceedingPayers :: [User] -> Map UserId Int -> Map UserId Int -> Map UserId Int -> [ExceedingPayer] -getExceedingPayers users cumulativeIncome preIncomeRepartition postIncomeRepartition = - let userIds = map _user_id users - payers = getPayers userIds cumulativeIncome preIncomeRepartition postIncomeRepartition - postPaymentPayers = map getPostPaymentPayer payers - mbMaxRatio = safeMaximum . map _postPaymentPayer_ratio $ postPaymentPayers - in case mbMaxRatio of - Just maxRatio -> - exceedingPayersFromAmounts - . map (\p -> (_postPaymentPayer_userId p, getFinalDiff maxRatio p)) - $ postPaymentPayers - Nothing -> - exceedingPayersFromAmounts - . map (\p -> (_payer_userId p, _payer_preIncomePayments p)) - $ payers - -getPayers :: [UserId] -> Map UserId Int -> Map UserId Int -> Map UserId Int -> [Payer] -getPayers userIds cumulativeIncome preIncomeRepartition postIncomeRepartition = - flip map userIds (\userId -> Payer - { _payer_userId = userId - , _payer_preIncomePayments = M.findWithDefault 0 userId preIncomeRepartition - , _payer_postIncomePayments = M.findWithDefault 0 userId postIncomeRepartition - , _payer_income = M.findWithDefault 0 userId cumulativeIncome - } - ) - -exceedingPayersFromAmounts :: [(UserId, Int)] -> [ExceedingPayer] -exceedingPayersFromAmounts userAmounts = - case mbMinAmount of - Nothing -> - [] - Just minAmount -> - filter (\payer -> _exceedingPayer_amount payer > 0) - . map (\userAmount -> - ExceedingPayer - { _exceedingPayer_userId = fst userAmount - , _exceedingPayer_amount = snd userAmount - minAmount - } - ) - $ userAmounts - where mbMinAmount = safeMinimum . map snd $ userAmounts - -getPostPaymentPayer :: Payer -> PostPaymentPayer -getPostPaymentPayer payer = - PostPaymentPayer - { _postPaymentPayer_userId = _payer_userId payer - , _postPaymentPayer_preIncomePayments = _payer_preIncomePayments payer - , _postPaymentPayer_cumulativeIncome = _payer_income payer - , _postPaymentPayer_ratio = (fromIntegral . _payer_postIncomePayments $ payer) / (fromIntegral $ _payer_income payer) - } - -getFinalDiff :: Float -> PostPaymentPayer -> Int -getFinalDiff maxRatio payer = - let postIncomeDiff = - truncate $ -1.0 * (maxRatio - _postPaymentPayer_ratio payer) * (fromIntegral . _postPaymentPayer_cumulativeIncome $ payer) - in postIncomeDiff + _postPaymentPayer_preIncomePayments payer - -safeMinimum :: (Ord a) => [a] -> Maybe a -safeMinimum [] = Nothing -safeMinimum xs = Just . minimum $ xs - -safeMaximum :: (Ord a) => [a] -> Maybe a -safeMaximum [] = Nothing -safeMaximum xs = Just . maximum $ xs diff --git a/server/src/Persistence/Category.hs b/server/src/Persistence/Category.hs deleted file mode 100644 index b0a6fca..0000000 --- a/server/src/Persistence/Category.hs +++ /dev/null @@ -1,123 +0,0 @@ -module Persistence.Category - ( count - , list - , listAll - , create - , edit - , delete - ) where - -import qualified Data.Maybe as Maybe -import Data.Text (Text) -import Data.Time.Clock (getCurrentTime) -import Database.SQLite.Simple (FromRow (fromRow), NamedParam ((:=))) -import qualified Database.SQLite.Simple as SQLite -import Prelude hiding (id) - -import Common.Model (Category (..), CategoryId) - -import Model.Query (Query (Query)) - -newtype Row = Row Category - -instance FromRow Row where - fromRow = Row <$> (Category <$> - SQLite.field <*> - SQLite.field <*> - SQLite.field <*> - SQLite.field <*> - SQLite.field <*> - SQLite.field) - -data CountRow = CountRow Int - -instance FromRow CountRow where - fromRow = CountRow <$> SQLite.field - -count :: Query Int -count = - Query (\conn -> - (Maybe.fromMaybe 0 . fmap (\(CountRow n) -> n) . Maybe.listToMaybe) <$> - SQLite.query_ conn "SELECT COUNT(*) FROM category WHERE deleted_at IS NULL" - ) - - -list :: Int -> Int -> Query [Category] -list page perPage = - Query (\conn -> - map (\(Row c) -> c) <$> - SQLite.queryNamed - conn - "SELECT * FROM category WHERE deleted_at IS NULL ORDER BY name LIMIT :limit OFFSET :offset" - [ ":limit" := perPage - , ":offset" := (page - 1) * perPage - ] - ) - -listAll :: Query [Category] -listAll = - Query (\conn -> - map (\(Row c) -> c) <$> - SQLite.query_ conn "SELECT * FROM category WHERE deleted_at IS NULL" - ) - -create :: Text -> Text -> Query () -create name color = - Query (\conn -> do - currentTime <- getCurrentTime - SQLite.executeNamed - conn - "INSERT INTO category (name, color, created_at) VALUES (:name, :color, :created_at)" - [ ":name" := name - , ":color" := color - , ":created_at" := currentTime - ] - ) - -edit :: CategoryId -> Text -> Text -> Query Bool -edit id name color = - Query (\conn -> do - mbCategory <- fmap (\(Row c) -> c) . Maybe.listToMaybe <$> - (SQLite.queryNamed conn "SELECT * FROM category WHERE id = :id" [ ":id" := id ]) - if Maybe.isJust mbCategory - then do - currentTime <- getCurrentTime - SQLite.executeNamed - conn - "UPDATE category SET edited_at = :editedAt, name = :name, color = :color WHERE id = :id" - [ ":editedAt" := currentTime - , ":name" := name - , ":color" := color - , ":id" := id - ] - return True - else - return False - ) - -data BoolRow = BoolRow Int - -instance FromRow BoolRow where - fromRow = BoolRow <$> SQLite.field - -delete :: CategoryId -> Query Bool -delete id = - Query (\conn -> do - mbPayment <- (fmap (\(BoolRow b) -> b) . Maybe.listToMaybe) <$> - (SQLite.queryNamed - conn - "SELECT true FROM payment WHERE category = :id AND deleted_at IS NULL" - [ ":id" := id ]) - if Maybe.isNothing mbPayment - then do - currentTime <- getCurrentTime - SQLite.executeNamed - conn - "UPDATE category SET deleted_at = :deletedAt WHERE id = :id AND deleted_at IS NULL" - [ ":deletedAt" := currentTime - , ":id" := id - ] - return True - else - return False - ) diff --git a/server/src/Persistence/Frequency.hs b/server/src/Persistence/Frequency.hs deleted file mode 100644 index edaa844..0000000 --- a/server/src/Persistence/Frequency.hs +++ /dev/null @@ -1,23 +0,0 @@ -module Persistence.Frequency - ( FrequencyField(..) - ) where - -import qualified Data.Text as T -import Database.SQLite.Simple (SQLData (SQLText)) -import Database.SQLite.Simple.FromField (FromField (fromField), - fieldData) -import Database.SQLite.Simple.Ok (Ok (Errors, Ok)) -import Database.SQLite.Simple.ToField (ToField (toField)) - -import Common.Model (Frequency) - -newtype FrequencyField = FrequencyField Frequency - -instance FromField FrequencyField where - fromField field = - case fieldData field of - SQLText text -> Ok (FrequencyField (read (T.unpack text) :: Frequency)) - _ -> Errors [error "SQLText field required for frequency"] - -instance ToField FrequencyField where - toField (FrequencyField f) = SQLText . T.pack . show $ f diff --git a/server/src/Persistence/Income.hs b/server/src/Persistence/Income.hs deleted file mode 100644 index 1b5364c..0000000 --- a/server/src/Persistence/Income.hs +++ /dev/null @@ -1,201 +0,0 @@ -module Persistence.Income - ( listAll - , count - , list - , listModifiedSince - , create - , edit - , delete - , definedForAll - , getCumulativeIncome - ) where - -import qualified Data.List as L -import Data.Map (Map) -import qualified Data.Map as M -import qualified Data.Maybe as Maybe -import qualified Data.Text as T -import Data.Time.Calendar (Day) -import Data.Time.Clock (UTCTime) -import Data.Time.Clock (getCurrentTime) -import Database.SQLite.Simple (FromRow (fromRow), NamedParam ((:=))) -import qualified Database.SQLite.Simple as SQLite -import Prelude hiding (id, until) - -import Common.Model (Income (..), IncomeId, PaymentId, - UserId) - -import Model.Query (Query (Query)) - -newtype Row = Row Income - -instance FromRow Row where - fromRow = Row <$> (Income <$> - SQLite.field <*> - SQLite.field <*> - SQLite.field <*> - SQLite.field <*> - SQLite.field <*> - SQLite.field <*> - SQLite.field) - -data CountRow = CountRow Int - -instance FromRow CountRow where - fromRow = CountRow <$> SQLite.field - -listAll :: Query [Income] -listAll = - Query (\conn -> - map (\(Row i) -> i) <$> - SQLite.query_ - conn - "SELECT * FROM income WHERE deleted_at IS NULL ORDER BY date DESC" - ) - - -count :: Query Int -count = - Query (\conn -> - (Maybe.fromMaybe 0 . fmap (\(CountRow n) -> n) . Maybe.listToMaybe) <$> - SQLite.query_ conn "SELECT COUNT(*) FROM income WHERE deleted_at IS NULL" - ) - -list :: Int -> Int -> Query [Income] -list page perPage = - Query (\conn -> - map (\(Row i) -> i) <$> - SQLite.queryNamed - conn - "SELECT * FROM income WHERE deleted_at IS NULL ORDER BY date DESC LIMIT :limit OFFSET :offset" - [ ":limit" := perPage - , ":offset" := (page - 1) * perPage - ] - ) - -listModifiedSince :: UTCTime -> Query [Income] -listModifiedSince since = - Query (\conn -> - map (\(Row i) -> i) <$> - SQLite.queryNamed - conn - (SQLite.Query . T.intercalate " " $ - [ "SELECT *" - , "FROM income" - , "WHERE" - , "created_at >= :since" - , "OR edited_at >= :since" - , "OR deleted_at >= :since" - ]) - [ ":since" := since ] - ) - -create :: UserId -> Day -> Int -> Query () -create userId date amount = - Query (\conn -> do - createdAt <- getCurrentTime - SQLite.executeNamed - conn - "INSERT INTO income (user_id, date, amount, created_at) VALUES (:userId, :date, :amount, :createdAt)" - [ ":userId" := userId - , ":date" := date - , ":amount" := amount - , ":createdAt" := createdAt - ] - ) - -edit :: UserId -> IncomeId -> Day -> Int -> Query Bool -edit userId id date amount = - Query (\conn -> do - income <- fmap (\(Row i) -> i) . Maybe.listToMaybe <$> - SQLite.queryNamed conn "SELECT * FROM income WHERE id = :id" [ ":id" := id ] - if Maybe.isJust income then - do - currentTime <- getCurrentTime - SQLite.executeNamed - conn - "UPDATE income SET edited_at = :editedAt, date = :date, amount = :amount WHERE id = :id AND user_id = :userId" - [ ":editedAt" := currentTime - , ":date" := date - , ":amount" := amount - , ":id" := id - , ":userId" := userId - ] - return True - else - return False - ) - -delete :: UserId -> PaymentId -> Query () -delete userId id = - Query (\conn -> - SQLite.executeNamed - conn - "UPDATE income SET deleted_at = datetime('now') WHERE id = :id AND user_id = :userId" - [ ":id" := id - , ":userId" := userId - ] - ) - -data UserDayRow = UserDayRow (UserId, Day) - -instance FromRow UserDayRow where - fromRow = do - user <- SQLite.field - day <- SQLite.field - return $ UserDayRow (user, day) - -definedForAll :: [UserId] -> Query (Maybe Day) -definedForAll users = - Query (\conn -> - (fromRows . fmap (\(UserDayRow (user, day)) -> (user, day))) <$> - SQLite.query_ - conn - "SELECT user_id, MIN(date) FROM income WHERE deleted_at IS NULL GROUP BY user_id;" - ) - where - fromRows rows = - if L.sort users == L.sort (map fst rows) then - Maybe.listToMaybe . reverse . L.sort . map snd $ rows - else - Nothing - -getCumulativeIncome :: Day -> Day -> Query (Map UserId Int) -getCumulativeIncome start end = - Query (\conn -> M.fromList <$> SQLite.queryNamed conn (SQLite.Query query) parameters) - where - query = - T.intercalate "\n" $ - [ "SELECT user_id, CAST(ROUND(SUM(count)) AS INTEGER) FROM (" - , " SELECT" - , " I1.user_id," - , " ((JULIANDAY(MIN(I2.date)) - JULIANDAY(I1.date)) * I1.amount * 12 / 365) AS count" - , " FROM (" <> (selectBoundedIncomes ">" ":start") <> ") AS I1" - , " INNER JOIN (" <> (selectBoundedIncomes "<" ":end") <> ") AS I2" - , " ON I2.date > I1.date AND I2.user_id == I1.user_id" - , " GROUP BY I1.date, I1.user_id" - , ") GROUP BY user_id" - ] - - selectBoundedIncomes op param = - T.intercalate "\n" $ - [ " SELECT user_id, date, amount FROM (" - , " SELECT" - , " i.user_id, " <> param <> " AS date, i.amount" - , " FROM" - , " (SELECT id, MAX(date) AS max_date" - , " FROM income" - , " WHERE date <= " <> param <> " AND deleted_at IS NULL" - , " GROUP BY user_id) AS m" - , " INNER JOIN income AS i" - , " ON i.id = m.id AND i.date = m.max_date" - , " ) UNION" - , " SELECT user_id, date, amount" - , " FROM income" - , " WHERE date " <> op <> " " <> param <> " AND deleted_at IS NULL" - ] - - parameters = - [ ":start" := start - , ":end" := end - ] diff --git a/server/src/Persistence/Payment.hs b/server/src/Persistence/Payment.hs deleted file mode 100644 index 573d57f..0000000 --- a/server/src/Persistence/Payment.hs +++ /dev/null @@ -1,389 +0,0 @@ -module Persistence.Payment - ( count - , find - , getRange - , listAllPunctual - , listActivePage - , listModifiedPunctualSince - , listActiveMonthlyOrderedByName - , create - , createMany - , edit - , delete - , searchCategory - , repartition - , getPreAndPostPaymentRepartition - , usedCategories - ) where - -import Data.Map (Map) -import qualified Data.Map as M -import qualified Data.Maybe as Maybe -import Data.Text (Text) -import qualified Data.Text as T -import Data.Time.Calendar (Day) -import qualified Data.Time.Calendar as Calendar -import Data.Time.Clock (UTCTime) -import Data.Time.Clock (getCurrentTime) -import Database.SQLite.Simple (FromRow (fromRow), - NamedParam ((:=)), ToRow) -import qualified Database.SQLite.Simple as SQLite -import Database.SQLite.Simple.ToField (ToField (toField)) -import Prelude hiding (id, until) - -import Common.Model (CategoryId, Frequency (..), - Payment (..), PaymentId, - User (..), UserId) -import qualified Common.Util.Text as TextUtil - -import Model.Query (Query (Query)) -import Persistence.Frequency (FrequencyField (..)) -import qualified Persistence.Income as IncomePersistence -import qualified Persistence.Util as PersistenceUtil - - -fields :: Text -fields = T.intercalate "," $ - [ "id" - , "user_id" - , "name" - , "cost" - , "date" - , "category" - , "frequency" - , "created_at" - , "edited_at" - , "deleted_at" - ] - -newtype Row = Row Payment - -instance FromRow Row where - fromRow = Row <$> (Payment <$> - SQLite.field <*> - SQLite.field <*> - SQLite.field <*> - SQLite.field <*> - SQLite.field <*> - SQLite.field <*> - (fmap (\(FrequencyField f) -> f) $ SQLite.field) <*> - SQLite.field <*> - SQLite.field <*> - SQLite.field) - -newtype InsertRow = InsertRow Payment - -instance ToRow InsertRow where - toRow (InsertRow p) = - [ toField (_payment_user p) - , toField (_payment_name p) - , toField (_payment_cost p) - , toField (_payment_date p) - , toField (_payment_category p) - , toField (FrequencyField (_payment_frequency p)) - , toField (_payment_createdAt p) - ] - -data Count = Count Int - -instance FromRow Count where - fromRow = Count <$> SQLite.field - -count :: Frequency -> Text -> Query Int -count frequency search = - Query (\conn -> - (\[Count n] -> n) <$> - SQLite.queryNamed - conn - (SQLite.Query $ T.intercalate " " - [ "SELECT COUNT(*)" - , "FROM payment" - , "WHERE" - , "deleted_at IS NULL" - , "AND frequency = :frequency" - , "AND (" <> PersistenceUtil.formatKeyForSearch "name" <> " LIKE :search OR cost LIKE :search)" - ]) - [ ":frequency" := FrequencyField frequency - , ":search" := "%" <> TextUtil.formatSearch search <> "%" - ] - ) - -find :: PaymentId -> Query (Maybe Payment) -find paymentId = - Query (\conn -> do - fmap (\(Row p) -> p) . Maybe.listToMaybe <$> - SQLite.queryNamed - conn - (SQLite.Query $ "SELECT " <> fields <> " FROM payment WHERE id = :id") - [ "id" := paymentId - ] - ) - -data RangeRow = RangeRow (Day, Day) - -instance FromRow RangeRow where - fromRow = (\f t -> RangeRow (f, t)) <$> SQLite.field <*> SQLite.field - -getRange :: Query (Maybe (Day, Day)) -getRange = - Query (\conn -> do - fmap (\(RangeRow (f, t)) -> (f, t)) . Maybe.listToMaybe <$> - SQLite.queryNamed - conn - (SQLite.Query $ T.intercalate " " - [ "SELECT MIN(date), MAX(date)" - , "FROM payment" - , "WHERE" - , "frequency = :frequency" - , "AND deleted_at IS NULL" - ]) - [ ":frequency" := FrequencyField Punctual - ] - ) - -listAllPunctual :: Query [Payment] -listAllPunctual = - Query (\conn -> - map (\(Row p) -> p) <$> - SQLite.queryNamed - conn - (SQLite.Query $ T.intercalate " " - [ "SELECT" - , fields - , "FROM payment" - , "WHERE deleted_at IS NULL AND frequency = :frequency" - , "ORDER BY date" - ]) - [ ":frequency" := FrequencyField Punctual - ] - ) - - -listActivePage :: Frequency -> Int -> Int -> Text -> Query [Payment] -listActivePage frequency page perPage search = - Query (\conn -> - map (\(Row p) -> p) <$> - SQLite.queryNamed - conn - (SQLite.Query $ T.intercalate " " - [ "SELECT" - , fields - , "FROM payment" - , "WHERE" - , "deleted_at IS NULL" - , "AND frequency = :frequency" - , "AND (" <> PersistenceUtil.formatKeyForSearch "name" <> " LIKE :search OR cost LIKE :search)" - , "ORDER BY date DESC" - , "LIMIT :limit" - , "OFFSET :offset" - ] - ) - [ ":frequency" := FrequencyField frequency - , ":search" := "%" <> TextUtil.formatSearch search <> "%" - , ":limit" := perPage - , ":offset" := (page - 1) * perPage - ] - ) - -listModifiedPunctualSince :: UTCTime -> Query [Payment] -listModifiedPunctualSince since = - Query (\conn -> - map (\(Row i) -> i) <$> - SQLite.queryNamed - conn - (SQLite.Query . T.intercalate " " $ - [ "SELECT " <> fields - , "FROM payment" - , "WHERE" - , "frequency = :frequency" - , "AND (created_at >= :since OR edited_at >= :since OR deleted_at >= :since)" - ]) - [ ":frequency" := FrequencyField Punctual - , ":since" := since - ] - ) - - -listActiveMonthlyOrderedByName :: Query [Payment] -listActiveMonthlyOrderedByName = - Query (\conn -> do - map (\(Row p) -> p) <$> - SQLite.queryNamed - conn - (SQLite.Query $ T.intercalate " " - [ "SELECT" - , fields - , "FROM payment" - , "WHERE deleted_at IS NULL AND frequency = :frequency" - , "ORDER BY name DESC" - ]) - [ ":frequency" := FrequencyField Monthly - ] - ) - -create :: UserId -> Text -> Int -> Day -> CategoryId -> Frequency -> Query () -create userId name cost date category frequency = - Query (\conn -> do - currentTime <- getCurrentTime - SQLite.executeNamed - conn - (SQLite.Query $ T.intercalate " " - [ "INSERT INTO payment (user_id, name, cost, date, category, frequency, created_at)" - , "VALUES (:userId, :name, :cost, :date, :category, :frequency, :currentTime)" - ]) - [ ":userId" := userId - , ":name" := name - , ":cost" := cost - , ":date" := date - , ":category" := category - , ":frequency" := FrequencyField frequency - , ":currentTime" := currentTime - ] - ) - -createMany :: [Payment] -> Query () -createMany payments = - Query (\conn -> - SQLite.executeMany - conn - (SQLite.Query $ T.intercalate "" - [ "INSERT INTO payment (user_id, name, cost, date, category, frequency, created_at)" - , "VALUES (?, ?, ?, ?, ?, ?, ?)" - ]) - (map InsertRow payments) - ) - -edit :: UserId -> PaymentId -> Text -> Int -> Day -> CategoryId -> Frequency -> Query Bool -edit userId paymentId name cost date category frequency = - Query (\conn -> do - payment <- fmap (\(Row p) -> p) . Maybe.listToMaybe <$> - SQLite.queryNamed - conn - (SQLite.Query $ - "SELECT " <> fields <> " FROM payment WHERE id = :paymentId and user_id = :userId") - [ ":paymentId" := paymentId - , ":userId" := userId - ] - if Maybe.isJust payment then - do - currentTime <- getCurrentTime - SQLite.executeNamed - conn - (SQLite.Query $ T.intercalate " " - [ "UPDATE" - , " payment" - , "SET" - , " edited_at = :editedAt," - , " name = :name," - , " cost = :cost," - , " date = :date," - , " category = :category," - , " frequency = :frequency" - , "WHERE" - , " id = :id" - , " AND user_id = :userId" - ]) - [ ":editedAt" := currentTime - , ":name" := name - , ":cost" := cost - , ":date" := date - , ":category" := category - , ":frequency" := FrequencyField frequency - , ":id" := paymentId - , ":userId" := userId - ] - return True - else - return False - ) - -delete :: UserId -> PaymentId -> Query () -delete userId paymentId = - Query (\conn -> - SQLite.executeNamed - conn - "UPDATE payment SET deleted_at = datetime('now') WHERE id = :id AND user_id = :userId" - [ ":id" := paymentId - , ":userId" := userId - ] - ) - -data CategoryIdRow = CategoryIdRow CategoryId - -instance FromRow CategoryIdRow where - fromRow = CategoryIdRow <$> SQLite.field - -searchCategory :: Text -> Query (Maybe CategoryId) -searchCategory paymentName = - Query (\conn -> - fmap (\(CategoryIdRow d) -> d) . Maybe.listToMaybe <$> - SQLite.queryNamed - conn - (SQLite.Query . T.intercalate " " $ - [ "SELECT category" - , "FROM payment" - , "WHERE deleted_at is NULL AND name LIKE :name" - , "ORDER BY edited_at, created_at" - , "LIMIT 1" - ]) - [ ":name" := "%" <> paymentName <> "%" - ] - ) - -usedCategories :: Query [CategoryId] -usedCategories = - Query (\conn -> do - map (\(CategoryIdRow p) -> p) <$> - SQLite.query_ - conn - (SQLite.Query $ T.intercalate " " - [ "SELECT DISTINCT category" - , "FROM payment" - , "WHERE deleted_at IS NULL" - ]) - ) - -data UserCostRow = UserCostRow (UserId, Int) - -instance FromRow UserCostRow where - fromRow = do - user <- SQLite.field - cost <- SQLite.field - return $ UserCostRow (user, cost) - -repartition :: Frequency -> Text -> Day -> Day -> Query (Map UserId Int) -repartition frequency search from to = - Query (\conn -> - M.fromList . fmap (\(UserCostRow r) -> r) <$> SQLite.queryNamed - conn - (SQLite.Query . T.intercalate " " $ - [ "SELECT user_id, SUM(cost)" - , "FROM payment" - , "WHERE" - , "deleted_at IS NULL" - , "AND frequency = :frequency" - , "AND (" <> PersistenceUtil.formatKeyForSearch "name" <> " LIKE :search OR cost LIKE :search)" - , "AND date >= :from" - , "AND date < :to" - , "GROUP BY user_id" - ]) - [ ":frequency" := FrequencyField frequency - , ":search" := "%" <> TextUtil.formatSearch search <> "%" - , ":from" := from - , ":to" := to - ] - ) - -getPreAndPostPaymentRepartition :: Maybe (Day, Day) -> [User] -> Query (Map UserId Int, Map UserId Int) -getPreAndPostPaymentRepartition paymentRange users = do - case paymentRange of - Just (from, to) -> do - incomeDefinedForAll <- IncomePersistence.definedForAll (_user_id <$> users) - (,) - <$> (repartition Punctual "" from (Maybe.fromMaybe (Calendar.addDays 1 to) incomeDefinedForAll)) - <*> (case incomeDefinedForAll of - Just d -> repartition Punctual "" d (Calendar.addDays 1 to) - Nothing -> return M.empty) - - Nothing -> - return (M.empty, M.empty) diff --git a/server/src/Persistence/User.hs b/server/src/Persistence/User.hs deleted file mode 100644 index 12145ac..0000000 --- a/server/src/Persistence/User.hs +++ /dev/null @@ -1,78 +0,0 @@ -module Persistence.User - ( list - , get - , checkPassword - , createSignInToken - ) where - -import qualified Data.Maybe as Maybe -import Data.Text (Text) -import Database.SQLite.Simple (FromRow (fromRow), NamedParam ((:=))) -import qualified Database.SQLite.Simple as SQLite - -import Common.Model (Email (..), Password (..), User (..)) - -import Model.HashedPassword (HashedPassword (..)) -import qualified Model.HashedPassword as HashedPassword -import Model.Query (Query (Query)) -import qualified Model.UUID as UUID - -newtype Row = Row User - -instance FromRow Row where - fromRow = Row <$> (User <$> - SQLite.field <*> - SQLite.field <*> - SQLite.field <*> - SQLite.field) - -list :: Query [User] -list = - Query (\conn -> do - map (\(Row u) -> u) <$> - SQLite.query_ conn "SELECT id, creation, email, name from user ORDER BY creation DESC" - ) - -get :: Text -> Query (Maybe User) -get token = - Query (\conn -> do - fmap (\(Row u) -> u) . Maybe.listToMaybe <$> - SQLite.queryNamed - conn - "SELECT id, creation, email, name FROM user WHERE sign_in_token = :sign_in_token LIMIT 1" - [ ":sign_in_token" := token ] - ) - -data HashedPasswordRow = HashedPasswordRow HashedPassword - -instance FromRow HashedPasswordRow where - fromRow = HashedPasswordRow <$> (HashedPassword <$> SQLite.field) - -checkPassword :: Email -> Password -> Query Bool -checkPassword (Email email) password = - Query (\conn -> do - hashedPassword <- fmap (\(HashedPasswordRow p) -> p) . Maybe.listToMaybe <$> - SQLite.queryNamed - conn - "SELECT password FROM user WHERE email = :email LIMIT 1" - [ ":email" := email ] - case hashedPassword of - Just h -> - return (HashedPassword.check password h) - - Nothing -> - return False - ) - -createSignInToken :: Email -> Query Text -createSignInToken (Email email) = - Query (\conn -> do - token <- UUID.generateUUID - SQLite.executeNamed - conn - "UPDATE user SET sign_in_token = :sign_in_token WHERE email = :email" - [ ":sign_in_token" := token - , ":email" := email - ] - return token - ) diff --git a/server/src/Persistence/Util.hs b/server/src/Persistence/Util.hs deleted file mode 100644 index b7496c6..0000000 --- a/server/src/Persistence/Util.hs +++ /dev/null @@ -1,11 +0,0 @@ -module Persistence.Util - ( formatKeyForSearch - ) where - -import Data.Text (Text) - -formatKeyForSearch :: Text -> Text -formatKeyForSearch key = - "replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(lower(" - <> key - <> "), 'à', 'a'), 'â', 'a'), 'ç', 'c'), 'è', 'e'), 'é', 'e'), 'ê', 'e'), 'ë', 'e'), 'î', 'i'), 'ï', 'i'), 'ô', 'o'), 'ù', 'u'), 'û', 'u'), 'ü', 'u')" diff --git a/server/src/Resource.hs b/server/src/Resource.hs deleted file mode 100644 index a12a0f2..0000000 --- a/server/src/Resource.hs +++ /dev/null @@ -1,54 +0,0 @@ -module Resource - ( Resource - , resourceCreatedAt - , resourceEditedAt - , resourceDeletedAt - , Status(..) - , statuses - , groupByStatus - , statusDuring - ) where - -import Data.Map (Map) -import qualified Data.Map as M -import Data.Maybe (fromMaybe) -import Data.Time.Clock (UTCTime) - -class Resource a where - resourceCreatedAt :: a -> UTCTime - resourceEditedAt :: a -> Maybe UTCTime - resourceDeletedAt :: a -> Maybe UTCTime - -data Status = - Created - | Edited - | Deleted - deriving (Eq, Show, Read, Ord, Enum, Bounded) - -statuses :: [Status] -statuses = [minBound..] - -groupByStatus :: Resource a => UTCTime -> UTCTime -> [a] -> Map Status [a] -groupByStatus start end resources = - foldl - (\m resource -> - case statusDuring start end resource of - Just status -> M.insertWith (++) status [resource] m - Nothing -> m - ) - M.empty - resources - -statusDuring :: Resource a => UTCTime -> UTCTime -> a -> Maybe Status -statusDuring start end resource - | created && not deleted = Just Created - | not created && edited && not deleted = Just Edited - | not created && deleted = Just Deleted - | otherwise = Nothing - where - created = belongs (resourceCreatedAt resource) start end - edited = fromMaybe False (fmap (\t -> belongs t start end) $ resourceEditedAt resource) - deleted = fromMaybe False (fmap (\t -> belongs t start end) $ resourceDeletedAt resource) - -belongs :: UTCTime -> UTCTime -> UTCTime -> Bool -belongs time start end = time >= start && time < end diff --git a/server/src/Secure.hs b/server/src/Secure.hs deleted file mode 100644 index a30941f..0000000 --- a/server/src/Secure.hs +++ /dev/null @@ -1,31 +0,0 @@ -module Secure - ( loggedAction - ) where - -import Control.Monad.IO.Class (liftIO) -import qualified Data.Text.Lazy as TL -import qualified Network.HTTP.Types.Status as HTTP -import Web.Scotty - -import Common.Model (User) -import qualified Common.Msg as Msg - -import qualified LoginSession -import qualified Model.Query as Query -import qualified Persistence.User as UserPersistence - -loggedAction :: (User -> ActionM ()) -> ActionM () -loggedAction action = do - maybeToken <- LoginSession.get - case maybeToken of - Just token -> do - maybeUser <- liftIO . Query.run . UserPersistence.get $ token - case maybeUser of - Just user -> - action user - Nothing -> do - status HTTP.forbidden403 - html . TL.fromStrict . Msg.get $ Msg.Secure_Unauthorized - Nothing -> do - status HTTP.forbidden403 - html . TL.fromStrict . Msg.get $ Msg.Secure_Forbidden diff --git a/server/src/SendMail.hs b/server/src/SendMail.hs deleted file mode 100644 index 13d4072..0000000 --- a/server/src/SendMail.hs +++ /dev/null @@ -1,66 +0,0 @@ -module SendMail - ( sendMail - ) where - -import Control.Arrow (left) -import Control.Exception (SomeException, try) -import Data.Either (isLeft) -import qualified Network.Mail.Mime as M - -import Data.Text (Text) -import qualified Data.Text as T -import qualified Data.Text.IO as T -import qualified Data.Text.Lazy as LT -import Data.Text.Lazy.Builder (fromText, toLazyText) - -import Conf (Conf) -import qualified Conf -import Model.Mail (Mail (..)) - -sendMail :: Conf -> Mail -> IO (Either Text ()) -sendMail conf mail = - if Conf.devMode conf - then - do - T.putStrLn . mockMailMessage $ mail - return (Right ()) - else - do - result <- left (T.pack . show) <$> (try (M.renderSendMail . getMimeMail $ mail) :: IO (Either SomeException ())) - if isLeft result - then putStrLn ("Error sending the following email:" ++ (show mail) ++ "\n" ++ (show result)) - else return () - return result - -mockMailMessage :: Mail -> Text -mockMailMessage mail = T.concat $ - [ "[MOCK MAIL] " - , subject mail - , " (from: " - , from mail - , ") (to: " - , T.intercalate ", " $ to mail - , ")" - , "\n" - , body mail - , "\n" - ] - -getMimeMail :: Mail -> M.Mail -getMimeMail (Mail mailFrom mailTo mailSubject mailPlainBody) = - let fromMail = M.emptyMail (address mailFrom) - in fromMail - { M.mailTo = map address mailTo - , M.mailParts = [ [ M.plainPart . strictToLazy $ mailPlainBody ] ] - , M.mailHeaders = [("Subject", mailSubject)] - } - -address :: Text -> M.Address -address addressEmail = - M.Address - { M.addressName = Nothing - , M.addressEmail = addressEmail - } - -strictToLazy :: Text -> LT.Text -strictToLazy = toLazyText . fromText diff --git a/server/src/Statistics.hs b/server/src/Statistics.hs deleted file mode 100644 index e463aac..0000000 --- a/server/src/Statistics.hs +++ /dev/null @@ -1,59 +0,0 @@ -module Statistics - ( paymentsAndIncomes - ) where - -import Control.Arrow ((&&&)) -import qualified Data.List as L -import Data.Map (Map) -import qualified Data.Map as M -import qualified Data.Maybe as Maybe -import qualified Data.Time.Calendar as Calendar - -import Common.Model (Income (..), MonthStats (..), Payment (..), - Stats) - -paymentsAndIncomes :: [Payment] -> [Income] -> Stats -paymentsAndIncomes payments incomes = - - map toMonthStat . M.toList $ foldl - (\m p -> M.alter (alter p) (startOfMonth $ _payment_date p) m) - M.empty - payments - - where - - toMonthStat (start, paymentsByCategory) = - MonthStats start paymentsByCategory (incomesAt start) - - incomesAt day = - M.map (incomeAt day) lastToFirstIncomesByUser - - incomeAt day lastToFirstIncome = - Maybe.maybe 0 _income_amount - . Maybe.listToMaybe - . dropWhile (\i -> _income_date i > day) - $ lastToFirstIncome - - lastToFirstIncomesByUser = - M.map (reverse . L.sortOn _income_date) - . groupBy _income_userId - $ incomes - - initMonthStats = - M.fromList - . map (\category -> (category, 0)) - . L.nub - $ map _payment_category payments - - alter p Nothing = Just (addPayment p initMonthStats) - alter p (Just monthStats) = Just (addPayment p monthStats) - - addPayment p monthStats = M.adjust ((+) (_payment_cost p)) (_payment_category p) monthStats - - startOfMonth day = - let (y, m, _) = Calendar.toGregorian day - in Calendar.fromGregorian y m 1 - -groupBy :: Ord k => (a -> k) -> [a] -> Map k [a] -groupBy key = - M.fromListWith (++) . map (key &&& pure) diff --git a/server/src/Util/Time.hs b/server/src/Util/Time.hs deleted file mode 100644 index 4a29fcc..0000000 --- a/server/src/Util/Time.hs +++ /dev/null @@ -1,22 +0,0 @@ -module Util.Time - ( belongToCurrentMonth - , belongToCurrentWeek - ) where - -import Data.Time.Calendar (toGregorian) -import Data.Time.Calendar.WeekDate (toWeekDate) -import Data.Time.Clock (UTCTime, getCurrentTime) - -import qualified Common.Util.Time as Time - -belongToCurrentMonth :: UTCTime -> IO Bool -belongToCurrentMonth time = do - (timeYear, timeMonth, _) <- toGregorian <$> Time.timeToDay time - (actualYear, actualMonth, _) <- toGregorian <$> (getCurrentTime >>= Time.timeToDay) - return (actualYear == timeYear && actualMonth == timeMonth) - -belongToCurrentWeek :: UTCTime -> IO Bool -belongToCurrentWeek time = do - (timeYear, timeWeek, _) <- toWeekDate <$> Time.timeToDay time - (actualYear, actualWeek, _) <- toWeekDate <$> (getCurrentTime >>= Time.timeToDay) - return (actualYear == timeYear && actualWeek == timeWeek) diff --git a/server/src/Validation/Category.hs b/server/src/Validation/Category.hs deleted file mode 100644 index 12f2117..0000000 --- a/server/src/Validation/Category.hs +++ /dev/null @@ -1,27 +0,0 @@ -module Validation.Category - ( createCategory - , editCategory - ) where - -import Data.Text (Text) -import Data.Validation (Validation) -import qualified Data.Validation as V - -import Common.Model (CreateCategoryForm (..), - EditCategoryForm (..)) -import qualified Common.Validation.Category as CategoryValidation -import Model.CreateCategory (CreateCategory (..)) -import Model.EditCategory (EditCategory (..)) - -createCategory :: CreateCategoryForm -> Validation Text CreateCategory -createCategory form = - CreateCategory - <$> CategoryValidation.name (_createCategoryForm_name form) - <*> CategoryValidation.color (_createCategoryForm_color form) - -editCategory :: EditCategoryForm -> Validation Text EditCategory -editCategory form = - EditCategory - <$> V.Success (_editCategoryForm_id form) - <*> CategoryValidation.name (_editCategoryForm_name form) - <*> CategoryValidation.color (_editCategoryForm_color form) diff --git a/server/src/Validation/Income.hs b/server/src/Validation/Income.hs deleted file mode 100644 index 5e034d1..0000000 --- a/server/src/Validation/Income.hs +++ /dev/null @@ -1,27 +0,0 @@ -module Validation.Income - ( createIncome - , editIncome - ) where - -import Data.Text (Text) -import Data.Validation (Validation) -import qualified Data.Validation as V - -import Common.Model (CreateIncomeForm (..), - EditIncomeForm (..)) -import qualified Common.Validation.Income as IncomeValidation -import Model.CreateIncome (CreateIncome (..)) -import Model.EditIncome (EditIncome (..)) - -createIncome :: CreateIncomeForm -> Validation Text CreateIncome -createIncome form = - CreateIncome - <$> IncomeValidation.amount (_createIncomeForm_amount form) - <*> IncomeValidation.date (_createIncomeForm_date form) - -editIncome :: EditIncomeForm -> Validation Text EditIncome -editIncome form = - EditIncome - <$> V.Success (_editIncomeForm_id form) - <*> IncomeValidation.amount (_editIncomeForm_amount form) - <*> IncomeValidation.date (_editIncomeForm_date form) diff --git a/server/src/Validation/Payment.hs b/server/src/Validation/Payment.hs deleted file mode 100644 index 20e370e..0000000 --- a/server/src/Validation/Payment.hs +++ /dev/null @@ -1,33 +0,0 @@ -module Validation.Payment - ( createPayment - , editPayment - ) where - -import Data.Text (Text) -import Data.Validation (Validation) -import qualified Data.Validation as V - -import Common.Model (CategoryId, CreatePaymentForm (..), - EditPaymentForm (..)) -import qualified Common.Validation.Payment as PaymentValidation -import Model.CreatePayment (CreatePayment (..)) -import Model.EditPayment (EditPayment (..)) - -createPayment :: [CategoryId] -> CreatePaymentForm -> Validation Text CreatePayment -createPayment categories form = - CreatePayment - <$> PaymentValidation.name (_createPaymentForm_name form) - <*> PaymentValidation.cost (_createPaymentForm_cost form) - <*> PaymentValidation.date (_createPaymentForm_date form) - <*> PaymentValidation.category categories (_createPaymentForm_category form) - <*> V.Success (_createPaymentForm_frequency form) - -editPayment :: [CategoryId] -> EditPaymentForm -> Validation Text EditPayment -editPayment categories form = - EditPayment - <$> V.Success (_editPaymentForm_id form) - <*> PaymentValidation.name (_editPaymentForm_name form) - <*> PaymentValidation.cost (_editPaymentForm_cost form) - <*> PaymentValidation.date (_editPaymentForm_date form) - <*> PaymentValidation.category categories (_editPaymentForm_category form) - <*> V.Success (_editPaymentForm_frequency form) diff --git a/server/src/Validation/SignIn.hs b/server/src/Validation/SignIn.hs deleted file mode 100644 index dc86122..0000000 --- a/server/src/Validation/SignIn.hs +++ /dev/null @@ -1,16 +0,0 @@ -module Validation.SignIn - ( signIn - ) where - -import Data.Text (Text) -import Data.Validation (Validation) - -import Common.Model (SignInForm (..)) -import qualified Common.Validation.SignIn as SignInValidation -import Model.SignIn (SignIn (..)) - -signIn :: SignInForm -> Validation Text SignIn -signIn form = - SignIn - <$> SignInValidation.email (_signInForm_email form) - <*> SignInValidation.password (_signInForm_password form) diff --git a/server/src/View/Mail/WeeklyReport.hs b/server/src/View/Mail/WeeklyReport.hs deleted file mode 100644 index 3fe224f..0000000 --- a/server/src/View/Mail/WeeklyReport.hs +++ /dev/null @@ -1,124 +0,0 @@ -module View.Mail.WeeklyReport - ( mail - ) where - -import Data.List (sortOn) -import Data.Map (Map) -import qualified Data.Map as M -import Data.Maybe (catMaybes, fromMaybe) -import Data.Monoid ((<>)) -import Data.Text (Text) -import qualified Data.Text as T -import Data.Time.Clock (UTCTime) - -import Common.Model (ExceedingPayer (..), Income (..), - Payment (..), User (..), UserId) -import qualified Common.Model as CM -import qualified Common.Msg as Msg -import qualified Common.View.Format as Format - -import Conf (Conf) -import qualified Conf as Conf -import Model.IncomeResource (IncomeResource (..)) -import Model.Mail (Mail (Mail)) -import qualified Model.Mail as M -import Model.PaymentResource (PaymentResource (..)) -import qualified Payer as Payer -import Resource (Status (..), groupByStatus, statuses) - -mail :: Conf -> [User] -> [Income] -> [Payment] -> Map UserId Int -> Map UserId Int -> Map UserId Int -> UTCTime -> UTCTime -> Mail -mail conf users weekIncomes weekPayments cumulativeIncome preIncomeRepartition postIncomeRepartition start end = - Mail - { M.from = Conf.noReplyMail conf - , M.to = map _user_email users - , M.subject = T.concat - [ Msg.get Msg.App_Title - , " − " - , Msg.get Msg.WeeklyReport_Title - ] - , M.body = body conf users weekIncomes weekPayments cumulativeIncome preIncomeRepartition postIncomeRepartition start end - } - -body :: Conf -> [User] -> [Income] -> [Payment] -> Map UserId Int -> Map UserId Int -> Map UserId Int -> UTCTime -> UTCTime -> Text -body conf users weekIncomes weekPayments cumulativeIncome preIncomeRepartition postIncomeRepartition start end = - T.intercalate "\n" $ - [ exceedingPayers conf users cumulativeIncome preIncomeRepartition postIncomeRepartition - , operations conf users paymentsGroupedByStatus incomesGroupedByStatus - ] - where - paymentsGroupedByStatus = groupByStatus start end . map PaymentResource $ weekPayments - incomesGroupedByStatus = groupByStatus start end . map IncomeResource $ weekIncomes - -exceedingPayers :: Conf -> [User] -> Map UserId Int -> Map UserId Int -> Map UserId Int -> Text -exceedingPayers conf users cumulativeIncome preIncomeRepartition postIncomeRepartition = - T.intercalate "\n" . map formatPayer $ payers - where - payers = Payer.getExceedingPayers users cumulativeIncome preIncomeRepartition postIncomeRepartition - formatPayer p = T.concat - [ " * " - , fromMaybe "" $ _user_name <$> CM.findUser (_exceedingPayer_userId p) users - , " + " - , Format.price (Conf.currency conf) $ _exceedingPayer_amount p - , "\n" - ] - -operations :: Conf -> [User] -> Map Status [PaymentResource] -> Map Status [IncomeResource] -> Text -operations conf users paymentsByStatus incomesByStatus = - if M.null paymentsByStatus && M.null incomesByStatus - then - Msg.get Msg.WeeklyReport_Empty - else - T.intercalate "\n" . catMaybes . concat $ - [ map (\s -> paymentSection s conf users <$> M.lookup s paymentsByStatus) statuses - , map (\s -> incomeSection s conf users <$> M.lookup s incomesByStatus) statuses - ] - -paymentSection :: Status -> Conf -> [User] -> [PaymentResource] -> Text -paymentSection status conf users payments = - section sectionTitle sectionItems - where count = length payments - sectionTitle = Msg.get $ case status of - Created -> if count > 1 then Msg.WeeklyReport_PaymentsCreated count else Msg.WeeklyReport_PaymentCreated count - Edited -> if count > 1 then Msg.WeeklyReport_PaymentsEdited count else Msg.WeeklyReport_PaymentEdited count - Deleted -> if count > 1 then Msg.WeeklyReport_PaymentsDeleted count else Msg.WeeklyReport_PaymentDeleted count - sectionItems = map (payedFor status conf users) . sortOn _payment_date . map (\(PaymentResource p) -> p) $ payments - -payedFor :: Status -> Conf -> [User] -> Payment -> Text -payedFor status conf users payment = - case status of - Deleted -> Msg.get (Msg.WeeklyReport_PayedForNot name amount for at) - _ -> Msg.get (Msg.WeeklyReport_PayedFor name amount for at) - where name = formatUserName (_payment_user payment) users - amount = Format.price (Conf.currency conf) . _payment_cost $ payment - for = _payment_name payment - at = Format.longDay $ _payment_date payment - -incomeSection :: Status -> Conf -> [User] -> [IncomeResource] -> Text -incomeSection status conf users incomes = - section sectionTitle sectionItems - where count = length incomes - sectionTitle = Msg.get $ case status of - Created -> if count > 1 then Msg.WeeklyReport_IncomesCreated count else Msg.WeeklyReport_IncomeCreated count - Edited -> if count > 1 then Msg.WeeklyReport_IncomesEdited count else Msg.WeeklyReport_IncomeEdited count - Deleted -> if count > 1 then Msg.WeeklyReport_IncomesDeleted count else Msg.WeeklyReport_IncomeDeleted count - sectionItems = map (isPayedFrom status conf users) . sortOn _income_date . map (\(IncomeResource i) -> i) $ incomes - -isPayedFrom :: Status -> Conf -> [User] -> Income -> Text -isPayedFrom status conf users income = - case status of - Deleted -> Msg.get (Msg.WeeklyReport_PayedFromNot name amount for) - _ -> Msg.get (Msg.WeeklyReport_PayedFrom name amount for) - where name = formatUserName (_income_userId income) users - amount = Format.price (Conf.currency conf) . _income_amount $ income - for = Format.longDay $ _income_date income - -formatUserName :: UserId -> [User] -> Text -formatUserName userId = fromMaybe "−" . fmap _user_name . CM.findUser userId - -section :: Text -> [Text] -> Text -section title items = - T.concat - [ title - , "\n\n" - , T.unlines . map (" * " <>) $ items - ] diff --git a/server/src/View/Page.hs b/server/src/View/Page.hs deleted file mode 100644 index ae7a266..0000000 --- a/server/src/View/Page.hs +++ /dev/null @@ -1,43 +0,0 @@ -module View.Page - ( page - ) where - -import Data.Aeson (encode) -import qualified Data.Aeson.Types as Json -import Data.Text.Internal.Lazy (Text) -import Data.Text.Lazy.Encoding (decodeUtf8) -import Prelude hiding (init) - -import Text.Blaze.Html -import Text.Blaze.Html.Renderer.Text (renderHtml) -import Text.Blaze.Html5 -import qualified Text.Blaze.Html5 as H -import Text.Blaze.Html5.Attributes -import qualified Text.Blaze.Html5.Attributes as A - -import Common.Model (Init) -import qualified Common.Msg as Msg - -page :: Maybe Init -> Text -page init = - renderHtml . docTypeHtml $ do - H.head $ do - meta ! charset "UTF-8" - meta ! name "viewport" ! content "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" - H.title (toHtml $ Msg.get Msg.App_Title) - script ! src "/javascript/main.js" $ "" - script ! src "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.js" $ "" - jsonScript "init" init - link ! rel "stylesheet" ! type_ "text/css" ! href "/css/reset.css" - link ! rel "stylesheet" ! type_ "text/css" ! href "/css/main.css" - link ! rel "icon" ! type_ "image/png" ! href "/images/icon.png" - H.body $ do - H.div ! A.class_ "spinner" $ "" - - -jsonScript :: Json.ToJSON a => Text -> a -> Html -jsonScript scriptId json = - script - ! A.id (toValue scriptId) - ! type_ "application/json" - $ toHtml . decodeUtf8 . encode $ json |