From 414692240a2c4f92de5dcb9e610b4b7340f6472b Mon Sep 17 00:00:00 2001 From: aitzol Date: Sat, 9 Apr 2022 17:38:00 +0200 Subject: [PATCH] first release --- .editorconfig | 3 + .gitignore | 3 + LICENSE | 674 ++++++ README.md | 84 + app.py | 981 +++++++++ change_pwd.tpl | 46 + data/__pycache__/flist.cpython-39.pyc | Bin 0 -> 21988 bytes data/__pycache__/fnlist.cpython-39.pyc | Bin 0 -> 29645 bytes data/__pycache__/slist.cpython-39.pyc | Bin 0 -> 8243 bytes data/flist.py | 2739 ++++++++++++++++++++++++ data/invite-codes.db | Bin 0 -> 8192 bytes data/screenshot.png | Bin 0 -> 24897 bytes data/slist.py | 1001 +++++++++ delete.tpl | 38 + edit_email.tpl | 38 + edit_fullname.tpl | 41 + index.tpl | 37 + locales/base.pot | 215 ++ locales/en/LC_MESSAGES/base.mo | Bin 0 -> 3855 bytes locales/en/LC_MESSAGES/base.po | 215 ++ locales/eu/LC_MESSAGES/base.mo | Bin 0 -> 3967 bytes locales/eu/LC_MESSAGES/base.po | 215 ++ requirements.txt | 2 + settings.ini.example | 42 + signup.tpl | 55 + static/style.css | 203 ++ user.tpl | 74 + 27 files changed, 6706 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.py create mode 100644 change_pwd.tpl create mode 100644 data/__pycache__/flist.cpython-39.pyc create mode 100644 data/__pycache__/fnlist.cpython-39.pyc create mode 100644 data/__pycache__/slist.cpython-39.pyc create mode 100644 data/flist.py create mode 100644 data/invite-codes.db create mode 100644 data/screenshot.png create mode 100644 data/slist.py create mode 100644 delete.tpl create mode 100644 edit_email.tpl create mode 100644 edit_fullname.tpl create mode 100644 index.tpl create mode 100644 locales/base.pot create mode 100644 locales/en/LC_MESSAGES/base.mo create mode 100644 locales/en/LC_MESSAGES/base.po create mode 100644 locales/eu/LC_MESSAGES/base.mo create mode 100644 locales/eu/LC_MESSAGES/base.po create mode 100644 requirements.txt create mode 100644 settings.ini.example create mode 100644 signup.tpl create mode 100644 static/style.css create mode 100644 user.tpl diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..805bb57 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +/settings.ini +/settings.ini.example.original +session \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..805bb57 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/settings.ini +/settings.ini.example.original +session \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..375f1e2 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# LDAP kudeaketarako Web Interfazea +[base]: https://github.com/jirutka/ldap-passwd-webui +[proiektu-izena]: ldap-python-webui +[git-izena]: aitzol/[proiektu-izena] +[pypi-bottle]: https://pypi.python.org/pypi/bottle/ +[pypi-ldap3]: https://pypi.python.org/pypi/ldap3 +[settings]: https://git.lainoa.eus/aitzol/ldap-python-webui/src/branch/master/settings.ini.example +[LICENSE]: https://www.gnu.org/licenses/gpl-3.0.txt +[GPL3]: https://www.gnu.org/licenses/licenses.html +[wsgiref]: https://docs.python.org/3/library/wsgiref.html#module-wsgiref.simple_server +[WSGI]: https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface +[LDAP]: https://eu.wikipedia.org/wiki/LDAP +[fork]: https://github.com/jirutka/ldap-passwd-webui + +Proiektu honen helburua erabiltzaileei [LDAP][LDAP] protokoloa erabiltzen duten zerbitzuetan norberaren kontuaren kudeaketarako tresna bat eskaintzea da, kontua sortu, pasahitza aldatu eta oinarrizko beste eragiketa batzuk burutzeko aukera emanez. [Bottle](http://bottlepy.org), Python-en WSGI web-framework-a erabiliz dago eraikia, [@jirutka][fork]-ren _ldap-passwd-webui_ proiektuan oinarritua. + +## Instalakuntza + +#### Baldintzak + +* Python 3.x +* [bottle][pypi-bottle] +* [ldap3][pypi-ldap3] 2.x + +### Urratsak + +Biltegi honetako edukiak klonatu eta menpekotasunak instalatu: + + git clone https://git.lainoa.eus/aitzol/ldap-python-webui + cd ldap-python-webui + pip install -r requirements.txt + +## Abian jarri + +#### Konfiguraketa + +Konfiguraketa [settings.ini][settings] fitxategian ezartzen da. Fitxategi honen kokapena `CONF_FILE` ingurumen-aldagaia erabiliz zehaztu daiteke. + +#### Ingurunea + +`LDAP_ADMIN_PASSWORD` eta `LDAP_READONLY_PASSWORD` _environment_ edo ingurumen-aldagaiak sisteman ezarri. + +#### Abiarazteko aukerak + +* [WSGI][WSGI] zerbitzariaren bidez, [wsgiref][wsgiref]-en oinarritua: + +``` + uwsgi --http :8080 --enable-threads --wsgi-file app.py +``` + +* Berezko Bottle zerbitzariaren bidez zuzenean `app.py` exekutatuz: + +``` + cd ldap-python-webui + python3 app.py +``` + +* Ondoren nabigatzailean http://localhost:8080 helbidea ireki + +## Ezaugarriak +* Saioa hasi + > Erabiltzaile izena eta pasahitzaz LDAP zerbitzarian saioa hasi. +* Izen-abizenak(aukerakoa) editatu +* Email helbidea editatu +* Pasahitza aldatu +* kontua ezabatu +* Kontua sortu + > Gonbidapen kodea erabiliz +* Lokalizazioa/Hizkuntza egokitzeko aukera + +## Egiteke + +* Erabiltzaileari ePosta bidez kontua aktibatzeko eskatzea. +* Pasahitza berrezartzen denean erabiltzaileari ePosta bidez jakinaraztea. + +## Screenshot + +![alt text](data/screenshot.png "Screenshot") + + +## Lizentzia + +Lan hau [GPL License][GPL3] lizentziapean aurkitzen da. +Lizentziaren textu osoa eskuratzeko ikusi ondorengo [esteka][LICENSE]. \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..605ea97 --- /dev/null +++ b/app.py @@ -0,0 +1,981 @@ +#!/usr/bin/env python3 + +''' +ldap-python-webui :: LDAP kudeaketarako Web Interfazea - Web UI for LDAP management. +Copyright (C) 2022 Aitzol Berasategi - Wproject + +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 . +''' + +import bottle +from bottle import get, post, static_file, request, route, template +from bottle import SimpleTemplate +from bottle.ext import beaker +from configparser import ConfigParser +from ldap3 import Server, Connection, ALL +from ldap3 import SIMPLE, SUBTREE, MODIFY_REPLACE, ALL_ATTRIBUTES +from ldap3.core.exceptions import LDAPBindError, LDAPConstraintViolationResult, \ + LDAPInvalidCredentialsResult, LDAPUserNameIsMandatoryError, \ + LDAPSocketOpenError, LDAPExceptionError +import logging +from os import getenv, environ, path +import re +import sqlite3 +from data import flist, slist +import random +import gettext + +BASE_DIR = path.dirname(__file__) +LOG = logging.getLogger(__name__) +LOG_FORMAT = '%(asctime)s %(levelname)s: %(message)s' +VERSION = '0.0.1' + +@get('/') +def get_index(): + try: + return user_tpl(data=newSession().get(), str=i18n.str) + except Exception as e: + return index_tpl(str=i18n.str) + +@get('/user') +def get_index(): + try: + return user_tpl(data=newSession().get(), str=i18n.str) + except Exception as e: + return index_tpl(str=i18n.str) + +@get('/signup') +def get_index(): + newSession() + return signup_tpl(str=i18n.str) + +@get('/change_pwd') +def get_index(): + try: + return change_pwd_tpl(data=newSession().get(), str=i18n.str) + except Exception as e: + return index_tpl(str=i18n.str) + +@get('/edit_fullname') +def get_index(): + try: + return edit_fullname_tpl(data=newSession().get(), str=i18n.str) + except Exception as e: + return index_tpl(str=i18n.str) + +@get('/edit_email') +def get_index(): + try: + return edit_email_tpl(data=newSession().get(), str=i18n.str) + except Exception as e: + return index_tpl(str=i18n.str) + +@get('/delete') +def get_index(): + try: + return delete_tpl(data=newSession().get(), str=i18n.str) + except Exception as e: + return index_tpl(str=i18n.str) + +@get('/logout') +def get_index(): + + def error(msg): + return index_tpl(alerts=[('error', msg, 'fadeOut')], str=i18n.str) + + try: + username = newSession().get()['username'] + if(username is not None): + logout(username) + except Error as e: + LOG.warning("Unsuccessful attempt to log out: %s" % e) + return error(str(e)) + + return index_tpl(alerts=[('success', i18n.msg[0], 'fadeOut')], str=i18n.str) + +@post('/user') +def post_user(): + form = request.forms.getunicode + + def error(msg): + return index_tpl(alerts=[('error', msg, 'fadeOut')], str=i18n.str) + + if len(form('username')) < 3: + return error(i18n.msg[1]) + + if len(form('password')) < 1: + return error(i18n.msg[2]) + + try: + login(form('username'), form('password')) + except Error as e: + LOG.warning("Unsuccessful attempt to login %s: %s" % (form('username'), e)) + return error(str(e)) + + return user_tpl(alerts=[('success', '%s %s' % (i18n.msg[3], form('username').capitalize()), 'fadeOut' )], data=newSession().get(), str=i18n.str) + +@post('/signup') +def post_signup(): + #ensure that i18n exists + if 'i18n' not in globals(): + newSession() + + form = request.forms.getunicode + isFake = False + + def username_validation(e): + regex = r'^\w+$' + return(bool(re.fullmatch(regex, e))) + + def email_validation(e): + regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' + return(bool(re.fullmatch(regex, e))) + + def code_is_valid(code): + con = sqlite3.connect('data/invite-codes.db') + cur = con.cursor() + + codes=[] + for row in cur.execute('SELECT * FROM codes WHERE valid = 1'): + codes.append(row[0]) + + return(bool(code in codes)) + + def mark_code_as_used(code): + con = sqlite3.connect('data/invite-codes.db') + cur = con.cursor() + + cur.execute('''UPDATE codes SET valid=? WHERE code==?''',(0, code)) + con.commit() + + def auto_complete(arg): + if arg == 'firstname': + result = random.choice(flist.firstname) + elif arg == 'surname': + result = random.choice(slist.surname) + return(result.lower()) + + def error(msg): + return signup_tpl(alerts=[('error', msg, 'fadeOut')], str=i18n.str) + + if not code_is_valid(form('invite_code')): + return(error(i18n.msg[4])) + + if len(form('username')) < 3: + return error(i18n.msg[5]) + + username = form('username').lower() + if not username_validation(username): + return error(i18n.msg[6]) + + if len(form('firstname')) == 0: + firstname = auto_complete('firstname') + isFake = True + else: + firstname = form('firstname').lower() + + if len(form('surname')) == 0: + surname = auto_complete('surname') + isFake = True + else: + surname = form('surname').lower() + + email = form('email').lower() + + if form('password') != form('confirm-password'): + return error(i18n.msg[7]) + + if len(form('password')) < 8: + return error(i18n.msg[8]) + + try: + account_request(username, firstname, surname, form('password'), email, isFake) + except Error as e: + LOG.warning("Unsuccessful attempt to create an account for %s: %s" % (form('username'), e)) + return error(str(e)) + + try: + mark_code_as_used(form('invite_code')) + except Error as e: + LOG.warning("There was a problem verifying the invitation code, please try again later.", e) + return error(str(e)) + + LOG.info("New account successfully created for %s" % form('username')) + + return index_tpl(alerts=[('success', i18n.msg[9], 'fadeOut')], str=i18n.str) + +@post('/edit_fullname') +def post_edit_fullname(): + form = request.forms.getunicode + + try: + username = newSession().get()['username'] + old_firstname = newSession().get()['firstname'] + old_surname = newSession().get()['surname'] + except Error as e: + return index_tpl(alerts=[('error', str(e), 'fadeOut')], str=i18n.str) + + def error(msg): + return edit_fullname_tpl(alerts=[('error', msg, 'fadeOut')], data=newSession().get(), str=i18n.str) + + if len(form('firstname')) < 3: + return error(i18n.msg[10]) + + if len(form('surname')) < 3: + return error(i18n.msg[11]) + + try: + edit_fullname(username, old_firstname, old_surname, form('firstname').lower(), form('surname').lower()) + except Error as e: + LOG.warning("Unsuccessful attempt to edit fullname for %s: %s" % (username, e)) + return error(str(e)) + + return user_tpl(alerts=[('success', i18n.msg[12], 'fadeOut' )], data=newSession().get(), str=i18n.str) + +@post('/edit_email') +def post_edit_email(): + form = request.forms.getunicode + + try: + username = newSession().get()['username'] + old_email = newSession().get()['mail'] + except Error as e: + return index_tpl(alerts=[('error', str(e), 'fadeOut')], str=i18n.str) + + def email_is_valid(e): + regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' + return(bool(re.fullmatch(regex, e))) + + def error(msg): + return edit_email_tpl(alerts=[('error', msg, 'fadeOut')], data=newSession().get(), str=i18n.str) + + if not email_is_valid(form('email')): + return(error(i18n.msg[13])) + + try: + edit_email(username, old_email, form('email').lower()) + except Error as e: + LOG.warning("Unsuccessful attempt to change email addres for %s: %s" % (username, e)) + return error(str(e)) + + return user_tpl(alerts=[('success', i18n.msg[14], 'fadeOut' )], data=newSession().get(), str=i18n.str) + +@post('/change_pwd') +def post_change_pwd(): + form = request.forms.getunicode + try: + username=newSession().get()['username'] + except Error as e: + return index_tpl(alerts=[('error', str(e), 'fadeOut')], str=i18n.str) + + def error(msg): + return change_pwd_tpl(username=username, alerts=[('error', msg, 'fadeOut')], str=i18n.str) + + if form('new-password') != form('confirm-password'): + return error(i18n.msg[7]) + + if len(form('new-password')) < 8: + return error(i18n.msg[8]) + + if form('old-password') == form('confirm-password'): + return error(i18n.msg[15]) + + try: + change_passwords(username, form('old-password'), form('new-password')) + logout(username) + except Error as e: + LOG.warning("Unsuccessful attempt to change password for %s: %s" % (username, e)) + return error(str(e)) + + LOG.info("Password successfully changed for: %s" % username) + + return index_tpl(alerts=[('success', i18n.msg[16], 'fadeOut')], username=username, str=i18n.str) + +@post('/delete') +def post_delete(): + form = request.forms.getunicode + + def error(msg): + return delete_tpl(alerts=[('error', msg, 'fadeOut')], str=i18n.str) + + try: + username = newSession().get()['username'] + if(form('username').lower() == username): + del_user(username) + else: + return(error(i18n.msg[17])) + except Error as e: + LOG.warning("Unsuccessful attempt to delete the account: %s" % e) + return error(str(e)) + + LOG.info("Account successfully deleted") + + return index_tpl(alerts=[('success', i18n.msg[18], 'fadeOut')], str=i18n.str) + +@route('/static/', name='static') +def serve_static(filename): + return static_file(filename, root=path.join(BASE_DIR, 'static')) + +def index_tpl(**kwargs): + return template('index', **kwargs) + +def user_tpl(**kwargs): + return template('user', **kwargs) + +def signup_tpl(**kwargs): + return template('signup', **kwargs) + +def change_pwd_tpl(**kwargs): + return template('change_pwd', **kwargs) + +def edit_email_tpl(**kwargs): + return template('edit_email', **kwargs) + +def edit_fullname_tpl(**kwargs): + return template('edit_fullname', **kwargs) + +def delete_tpl(**kwargs): + return template('delete', **kwargs) + +def connect_ldap(conf, **kwargs): + server = Server(host=conf['host'], + port=conf.getint('port', None), + use_ssl=conf.getboolean('use_ssl', False), + connect_timeout=5) + + return Connection(server, raise_exceptions=True, **kwargs) + +#LOGIN +def login(username, password): + n = N + for key in (key for key in CONF.sections() + if key == 'ldap' or key.startswith('ldap:')): + + LOG.debug("%s is trying to logging in %s" % (username, key)) + n -= 1 + try: + login_user(CONF[key], username, password) + except Error as e: + if n >=1: + e = [] + continue + else: + raise e + + break + +def login_user(conf, *args): + try: + login_user_ldap(conf, *args) + + except (LDAPBindError, LDAPInvalidCredentialsResult, LDAPUserNameIsMandatoryError): + raise Error(i18n.msg[19]) + + except LDAPSocketOpenError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[20]) + + except LDAPExceptionError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[21]) + +def login_user_ldap(conf, username, password): + #set current LDAP + superUser = SuperUsers(conf) + + #with connect_ldap(conf) as c: + with connect_ldap(conf, user=superUser.readonly_dn, password=superUser.readonly_pwd) as c: + user_dn = find_user_dn(conf, c, username) + + # Note: raises LDAPUserNameIsMandatoryError when user_dn is None. + with connect_ldap(conf, authentication=SIMPLE, user=user_dn, password=password) as c: + c.bind() + newSession().set(get_user_data(user_dn, c)) + LOG.debug("%s logged in to %s" % (username, conf['base'])) + +#LOGOUT +def logout(username): + n = N + for key in (key for key in CONF.sections() + if key == 'ldap' or key.startswith('ldap:')): + + LOG.debug("Logging out %s from %s" % (username, key)) + n -= 1 + try: + logout_user(CONF[key], username) + except Error as e: + if n >=1: + e = [] + continue + else: + raise e + + break + +def logout_user(conf, *args): + try: + logout_user_ldap(conf, *args) + + except (LDAPBindError, LDAPInvalidCredentialsResult, LDAPUserNameIsMandatoryError): + raise Error(i18n.msg[19]) + + except LDAPSocketOpenError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[20]) + + except LDAPExceptionError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[21]) + +def logout_user_ldap(conf, username): + #set current LDAP + superUser = SuperUsers(conf) + + #with connect_ldap(conf) as c: + with connect_ldap(conf, user=superUser.readonly_dn, password=superUser.readonly_pwd) as c: + user_dn = find_user_dn(conf, c, username) + c.unbind() + #newSession().close() + newSession().delete() + LOG.info("%s LOGED OUT" % (username)) + +#SIGN UP +def account_request(username, firstname, surname, password, email, isFake): + created = [] + for key in (key for key in CONF.sections() + if key == 'ldap' or key.startswith('ldap:')): + + LOG.debug("Creating account for %s on %s server" % (username, key)) + try: + new_user_account(CONF[key], username, firstname, surname, password, email, isFake) + created.append(key) + except Error as e: + for key in reversed(created): + LOG.info("Reverting account creation in %s for %s" % (key, username)) + try: + #Akatsen bat gertatzen bada LDAP instantzia guztietan kontua ezabatu + del_account(CONF[key], username) + except Error as e2: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e2)) + raise e + +def new_user_account(conf, *args): + try: + register(conf, *args) + + except (LDAPBindError, LDAPInvalidCredentialsResult, LDAPUserNameIsMandatoryError): + raise Error(i18n.msg[19]) + + except LDAPSocketOpenError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[20]) + + except LDAPExceptionError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[21]) + +def register(conf, username, firstname, surname, password, email, isFake): + + def to_ascii(str): + ascii_str="" + for c in str: + if 0 <= ord(c) <= 127: + ascii_str=ascii_str+c + else: + ascii_str=ascii_str+"X" + return(ascii_str) + + #set current LDAP + superUser = SuperUsers(conf) + + with connect_ldap(conf, user=superUser.admin_dn, password=superUser.admin_pwd) as c: + + try: + if (find_user_dn(conf,c,username) is not None): + raise Error(i18n.msg[22]) + + if (find_email(conf,c,email)): + raise Error(i18n.msg[23]) + + except Error as e: + raise e + + else: + #create new account + uidNumber = find_uid_number(conf,c)+1 + directory = 'home/user/'+to_ascii(username) + OBJECT_CLASS = ['top', 'inetOrgPerson', 'posixAccount', 'accountsManagement'] + attributes = {'gidNumber': '501', 'uidNumber': uidNumber, 'homeDirectory': directory, 'givenName': firstname, 'sn': surname, 'uid' : username, 'mail': email, 'active': False, 'fakeCn': isFake} + new_user_dn = "cn="+firstname+" "+ surname+",cn=users,"+conf['base'] + c.add(dn=new_user_dn,object_class=OBJECT_CLASS, attributes=attributes) + #create/change user password + c.extend.standard.modify_password(new_user_dn, '', password) + LOG.info("%s has registered on %s" % (username, conf)) + +#EDIT FULLNAME +def edit_fullname(username, old_firstname, old_surname, firstname, surname,): + changed = [] + + for key in (key for key in CONF.sections() + if key == 'ldap' or key.startswith('ldap:')): + + LOG.debug("Changing fullname in %s for %s" % (key, username)) + try: + new_fullname(CONF[key], username, firstname, surname) + changed.append(key) + LOG.debug("%s changed fullname on %s" % (username, key)) + except Error as e: + for key in reversed(changed): + LOG.info("Reverting fullname change in %s for %s" % (key, username)) + try: + new_fullname(CONF[key], username, old_firstname, old_surname) + except Error as e2: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e2)) + raise e + +def new_fullname(conf, *args): + try: + update_fullname(conf, *args) + + except (LDAPBindError, LDAPInvalidCredentialsResult, LDAPUserNameIsMandatoryError): + raise Error(i18n.msg[24]) + + except LDAPConstraintViolationResult as e: + # Extract useful part of the error message (for Samba 4 / AD). + msg = e.message.split('check_password_restrictions: ')[-1].capitalize() + raise Error(msg) + + except LDAPSocketOpenError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[20]) + + except LDAPExceptionError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[21]) + +def update_fullname(conf, username, firstname, surname): + #set current LDAP + superUser = SuperUsers(conf) + + with connect_ldap(conf, user=superUser.admin_dn, password=superUser.admin_pwd) as c: + #with connect_ldap(conf) as c: + user_dn = find_user_dn(conf, c, username) + c.modify(user_dn, {'givenName': [( MODIFY_REPLACE, firstname )], 'sn': [( MODIFY_REPLACE, surname )]}) + + new_cn = "cn="+firstname+" "+ surname + c.modify_dn(user_dn, new_cn) + new_user_dn = new_cn+",cn=users,"+conf['base'] + + base = ",cn=users," + conf['base'] + fakeFullName = user_dn[3:-len(base)].split(" ") + + if(user_dn == new_user_dn): + raise Error('Izen-abizenak ez dira aldatu.') + + c.modify(new_user_dn, {'fakeCn': [(MODIFY_REPLACE, 'false' )]}) + newSession().set(get_user_data(new_user_dn, c)) + +#EDIT EMAIL +def edit_email(username, old_email, new_email): + changed = [] + + for key in (key for key in CONF.sections() + if key == 'ldap' or key.startswith('ldap:')): + + LOG.debug("Changing email in %s for %s" % (key, username)) + try: + new_email_address(CONF[key], username, old_email, new_email) + changed.append(key) + LOG.debug("%s changed email address on %s" % (username, key)) + except Error as e: + for key in reversed(changed): + LOG.info("Reverting email change in %s for %s" % (key, username)) + try: + new_email_address(CONF[key], username, new_email, old_email) + except Error as e2: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e2)) + raise e + +def new_email_address(conf, *args): + try: + update_email_address(conf, *args) + + except (LDAPBindError, LDAPInvalidCredentialsResult, LDAPUserNameIsMandatoryError): + raise Error(i18n.msg[24]) + + except LDAPConstraintViolationResult as e: + # Extract useful part of the error message (for Samba 4 / AD). + msg = e.message.split('check_password_restrictions: ')[-1].capitalize() + raise Error(msg) + + except LDAPSocketOpenError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[20]) + + except LDAPExceptionError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[21]) + + +def update_email_address(conf, username, old_email, new_email): + if(old_email == new_email): + raise Error('Email helbidea ez da aldatu.') + + #set current LDAP + superUser = SuperUsers(conf) + + with connect_ldap(conf, user=superUser.admin_dn, password=superUser.admin_pwd) as c: + user_dn = find_user_dn(conf, c, username) + new_email_addresses = get_user_email_array(user_dn, c, old_email, new_email) + c.modify(user_dn, {'mail': [( MODIFY_REPLACE, new_email_addresses )]}) + newSession().set(get_user_data(user_dn, c)) + +#CHANGE PASSWORD +def change_passwords(username, old_pass, new_pass): + changed = [] + + for key in (key for key in CONF.sections() + if key == 'ldap' or key.startswith('ldap:')): + + LOG.debug("Changing password in %s for %s" % (key, username)) + try: + change_password(CONF[key], username, old_pass, new_pass) + changed.append(key) + LOG.debug("%s changed pwd on %s" % (username, key)) + except Error as e: + for key in reversed(changed): + LOG.info("Reverting password change in %s for %s" % (key, username)) + try: + change_password(CONF[key], username, new_pass, old_pass) + except Error as e2: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e2)) + raise e + +def change_password(conf, *args): + try: + if conf.get('type') == 'ad': + change_password_ad(conf, *args) + else: + change_password_ldap(conf, *args) + + except (LDAPBindError, LDAPInvalidCredentialsResult, LDAPUserNameIsMandatoryError): + raise Error(i18n.msg[24]) + + except LDAPConstraintViolationResult as e: + # Extract useful part of the error message (for Samba 4 / AD). + msg = e.message.split('check_password_restrictions: ')[-1].capitalize() + raise Error(msg) + + except LDAPSocketOpenError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[20]) + + except LDAPExceptionError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[21]) + + +def change_password_ldap(conf, username, old_pass, new_pass): + #set current LDAP + superUser = SuperUsers(conf) + + with connect_ldap(conf, user=superUser.readonly_dn, password=superUser.readonly_pwd) as c: + #with connect_ldap(conf) as c: + user_dn = find_user_dn(conf, c, username) + + # Note: raises LDAPUserNameIsMandatoryError when user_dn is None. + with connect_ldap(conf, authentication=SIMPLE, user=user_dn, password=old_pass) as c: + c.bind() + c.extend.standard.modify_password(user_dn, old_pass, new_pass) + +def change_password_ad(conf, username, old_pass, new_pass): + user = username + '@' + conf['ad_domain'] + + with connect_ldap(conf, authentication=SIMPLE, user=user, password=old_pass) as c: + c.bind() + user_dn = find_user_dn(conf, c, username) + c.extend.microsoft.modify_password(user_dn, new_pass, old_pass) + +#DELETE ACCOUNT +def del_user(username): + n = N + for key in (key for key in CONF.sections() + if key == 'ldap' or key.startswith('ldap:')): + LOG.debug("Deleting account for %s from %s" % (username, key)) + n -= 1 + try: + del_account(CONF[key], username) + LOG.debug("Account for %s deleted on -> %s" % (username, CONF[key])) + if(n == 0 and newSession().get()['username'] is not None): + newSession().delete() + + except Error as e: + raise e + +def del_account(conf, *args): + try: + delete(conf, *args) + + except LDAPSocketOpenError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[20]) + + except LDAPExceptionError as e: + LOG.error('{}: {!s}'.format(e.__class__.__name__, e)) + raise Error(i18n.msg[21]) + +def delete(conf, username): + #set current LDAP + superUser = SuperUsers(conf) + + with connect_ldap(conf, user=superUser.admin_dn, password=superUser.admin_pwd) as c: + try: + user_dn = find_user_dn(conf, c, username) + c.delete(user_dn) + + except Error as e: + raise e + +#AUXILIARY FUNCTIONS +#find user +def find_user_dn(conf, conn, uid): + search_filter = conf['search_filter'].replace('{uid}', uid) + conn.search(conf['base'], "(%s)" % search_filter, SUBTREE) + + return conn.response[0]['dn'] if conn.response else None + +#find email +def find_email(conf, conn, email): + search_filter = '(uid=*)' + if conn.search(conf['base'], search_filter, attributes=['mail']): + for i in conn.response: + for j in i['attributes']['mail']: + if(j == email): + return True + + return False + +#find highest uidNumber +def find_uid_number(conf, conn): + search_filter = '(uid=*)' + if conn.search(conf['base'], search_filter, attributes=['uidNumber']): + + uidNumbersList=[] + for i in conn.response: + uidNumbersList.append(i['attributes']['uidNumber']) + + return max(uidNumbersList) + + else: + return(999) + +def get_user_email_array(user_dn, conn, old_email, new_email): + search_filter = '(objectClass=*)' + conn.search(user_dn, search_filter, attributes=['mail']) + emails = conn.entries[0].mail.values + for i in range(len(emails)): + if(emails[i] == old_email): + emails[i] = new_email + return(emails) + +def get_user_data(user_dn, conn): + search_filter = '(objectClass=*)' + conn.search(user_dn, search_filter, attributes=['active','fakeCn','givenName','sn','uid','mail']) + data = [] + data.append(conn.entries[0].active.values[0]) + data.append(conn.entries[0].fakeCn.values[0]) + data.append(conn.entries[0].givenName.values[0]) + data.append(conn.entries[0].sn.values[0]) + data.append(conn.entries[0].uid.values[0]) + data.append(conn.entries[0].mail.values[0]) + return(data) + +def read_config(): + config = ConfigParser() + config.read([path.join(BASE_DIR, 'settings.ini'), getenv('CONF_FILE', '')]) + return config + +CONF = read_config() + +def ldaps_count(): + keys = [] + for i, key in enumerate(CONF.sections()): + if key == 'ldap' or key.startswith('ldap:'): + keys.append(key) + #n=len(keys) + return(len(keys)) + +N = ldaps_count() + +class Error(Exception): + pass + +#SESSIONS MANAGEMENT +def newSession(): + + class Session(object): + """docstring for Session""" + def __init__(self): + super(Session, self).__init__() + self.data = bottle.request.environ.get('beaker.session') + self.lang = self.get_lang() + #localization + global i18n + i18n = LocalizeTo(self.lang) + + def get_lang(self): + if 'HTTP_ACCEPT_LANGUAGE' in bottle.request.environ: + lang = bottle.request.get('HTTP_ACCEPT_LANGUAGE') + return str(lang[:2]) + else: + return CONF['locale']['lang'] + + def get(self): + if 'username' in self.data: + return(self.data) + else: + raise Error(i18n.msg[25]) + + def set(self, data): + self.active = data[0] + self.fakeCn = data[1] + self.firstname = data[2] + self.surname = data[3] + self.username = data[4] + self.mail = data[5] + + self.data['active'] = self.active + self.data['fakeCn'] = self.fakeCn + self.data['firstname'] = self.firstname + self.data['surname'] = self.surname + self.data['username'] = self.username + self.data['mail'] = self.mail + + def close(self): + self.data.pop('username') + + def delete(self): + self.data.delete() + + s=Session() + return s + +class LocalizeTo(object): + """docstring for Session""" + def __init__(self, lang): + super(LocalizeTo, self).__init__() + translate = gettext.translation('base', localedir=CONF['locale']['dir'], languages=[lang]) + translate.install() + _ = translate.gettext + + #generic strings + str_00 = _("User") + str_01 = _("Username") + str_02 = _("Firstname") + str_03 = _("Surname") + str_04 = _("Password") + str_05 = _("Old password") + str_06 = _("New password") + str_07 = _("Confirm password") + str_08 = _("Email") + str_09 = _("edit") + str_10 = _("Login") + str_11 = _("Logout") + str_12 = _("Delete") + str_13 = _("Sign Up") + str_14 = _("Back") + str_15 = _("Update") + str_16 = _("Or Sign In") + str_17 = _("Or Sign Up") + str_18 = _("Invite code") + str_19 = _("Edit your fullname") + str_20 = _("Edit your email") + str_21 = _("Change your password") + str_22 = _("Delete your account") + str_23 = _("Welcome") + + #messages + msg_00 = _("The session was closed.") + msg_01 = _("Username must be at least 3 characters long!") + msg_02 = _("Please enter your password!") + msg_03 = _("Welcome") + msg_04 = _("The code is invalid or has expired.") + msg_05 = _("A bit short, don't you think?!") + msg_06 = _("Not allowed characters for the username field.") + msg_07 = _("Passwords do not match!") + msg_08 = _("Password must be at least 8 characters long!") + msg_09 = _("Congratulations, your account has been created!") + msg_10 = _("Your firstname is a bit short, don't you think?") + msg_11 = _("Your surname is a bit short, don't you think?") + msg_12 = _("Your first and last name have been successfully updated.") + msg_13 = _("Invalid email address. Please try again.") + msg_14 = _("Your email has been successfully updated.") + msg_15 = _("The password entered is the same as the current password.") + msg_16 = _("Password has been changed!") + msg_17 = _("Please, type your username for account deletion.") + msg_18 = _("Account successfully deleted!") + msg_19 = _("Username or password is incorrect!") + msg_20 = _("Unable to connect to the remote server.") + msg_21 = _("Encountered an unexpected error while communicating with the remote server.") + msg_22 = _("User already exists.") + msg_23 = _("Email already exists.") + msg_24 = _("Forgot your password? Please try again.") + msg_25 = _("The session has expired.") + + self.str = {'usr':str_00, 'usrn':str_01, 'fn':str_02, 'sn':str_03, 'pwd':str_04, 'old-pwd':str_05, 'new-pwd':str_06, 'conf-pwd':str_07, 'email':str_08, 'edit':str_09, 'login':str_10, 'log-out':str_11, 'del':str_12, 'sign-up':str_13, 'back':str_14, 'update':str_15, 'or-sign-in':str_16, 'or-sign-up':str_17, 'inv-code':str_18, 'edit-fn':str_19, 'edit-email':str_20, 'ch-pwd':str_21, 'del-account':str_22, 'welcome':str_23} + self.msg = (msg_00, msg_01, msg_02, msg_03, msg_04, msg_05, msg_06, msg_07, msg_08, msg_09, msg_10, msg_11, msg_12, msg_13, msg_14, msg_15, msg_16, msg_17, msg_18, msg_19, msg_20, msg_21, msg_22, msg_23, msg_24, msg_25) + +if environ.get('DEBUG'): + bottle.debug(True) + +# Set up logging. +logging.basicConfig(format=LOG_FORMAT) +LOG.setLevel(logging.INFO) +LOG.info("Starting ldap-python-webui %s" % VERSION) + +session_opts = { + 'session.type': CONF['session']['type'], + 'session.cookie_expires': CONF['session']['expire'], + 'session.data_dir': CONF['session']['data_dir'], + 'session.auto': CONF['session']['auto'] +} + +class SuperUsers(object): + """docstring for Session""" + def __init__(self, conf): + super(SuperUsers, self).__init__() + self.domain=conf['base'][conf['base'].find(","):] + self.admin_dn="cn=admin"+self.domain + self.admin_pwd=environ['LDAP_ADMIN_PASSWORD'] + self.readonly_dn="cn=readonly"+self.domain + self.readonly_pwd=environ['LDAP_READONLY_PASSWORD'] + +superUser = SuperUsers(CONF['ldap:0']) + +app = beaker.middleware.SessionMiddleware(bottle.app(), session_opts) + +bottle.TEMPLATE_PATH = [BASE_DIR] + +# Set default attributes to pass into templates. +#SimpleTemplate.defaults = dict(CONF['html']) +SimpleTemplate.defaults['url'] = bottle.url + + +# Run bottle internal server when invoked directly (mainly for development). +if __name__ == '__main__': + bottle.run(app, **CONF['server']) +# Run bottle in application mode (in production under uWSGI server). +else: + #application = bottle.default_app() + application = app diff --git a/change_pwd.tpl b/change_pwd.tpl new file mode 100644 index 0000000..5da670b --- /dev/null +++ b/change_pwd.tpl @@ -0,0 +1,46 @@ + + + + + + + + + {{ str['ch-pwd'] }} + + + + + +
+

{{ str['ch-pwd'] }}

+ +
+ + + + + + + + + + +
+ + +
+ +
+ + %for type, text, animation in get('alerts', []): +
+
{{ text }}
+
+ %end + +
+ + diff --git a/data/__pycache__/flist.cpython-39.pyc b/data/__pycache__/flist.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ff74a1146377d3a4c962da591bc4695d7518817 GIT binary patch literal 21988 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 A*Z=?k literal 0 HcmV?d00001 diff --git a/data/__pycache__/slist.cpython-39.pyc b/data/__pycache__/slist.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28a698f7e6305ff963e8dde4ab2edec2aafd1e3b GIT binary patch literal 8243 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 literal 0 HcmV?d00001 diff --git a/data/screenshot.png b/data/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..1c85e2166d8eaf735b9c062f397fd3b968db699e GIT binary patch literal 24897 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

aNhUOTmG>qxwTL)9&0JtDvIo1 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%0Do8V=0MQjfSb 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 literal 0 HcmV?d00001 diff --git a/data/slist.py b/data/slist.py new file mode 100644 index 0000000..1491a8a --- /dev/null +++ b/data/slist.py @@ -0,0 +1,1001 @@ +surname = [ + "Abbott", + "Acevedo", + "Acosta", + "Adams", + "Adkins", + "Agirre", + "Aguilar", + "Albert", + "Alexander", + "Alford", + "Allen", + "Allison", + "Alston", + "Alvarado", + "Alvarez", + "Anderson", + "Andrews", + "Anthony", + "Armstrong", + "Arnold", + "Ashley", + "Atkins", + "Atkinson", + "Austin", + "Avery", + "Avila", + "Ayala", + "Ayers", + "Bailey", + "Baird", + "Baker", + "Baldwin", + "Ball", + "Ballard", + "Banks", + "Barber", + "Barker", + "Barlow", + "Barnes", + "Barnett", + "Barr", + "Barrera", + "Barrett", + "Barron", + "Barry", + "Bartlett", + "Barton", + "Bass", + "Bates", + "Battle", + "Bauer", + "Baxter", + "Beach", + "Bean", + "Beard", + "Beasley", + "Beck", + "Becker", + "Bell", + "Bender", + "Benjamin", + "Bennett", + "Benson", + "Bentley", + "Benton", + "Berg", + "Berger", + "Bernard", + "Berry", + "Best", + "Bird", + "Bishop", + "Black", + "Blackburn", + "Blackwell", + "Blair", + "Blake", + "Blanchard", + "Blankenship", + "Blevins", + "Bolton", + "Bond", + "Bonner", + "Booker", + "Boone", + "Booth", + "Bowen", + "Bowers", + "Bowman", + "Boyd", + "Boyer", + "Boyle", + "Bradford", + "Bradley", + "Bradshaw", + "Brady", + "Branch", + "Bray", + "Brennan", + "Brewer", + "Bridges", + "Briggs", + "Bright", + "Britt", + "Brock", + "Brooks", + "Brown", + "Browning", + "Bruce", + "Bryan", + "Bryant", + "Buchanan", + "Buck", + "Buckley", + "Buckner", + "Bullock", + "Burch", + "Burgess", + "Burke", + "Burks", + "Burnett", + "Burns", + "Burris", + "Burt", + "Burton", + "Bush", + "Butler", + "Byers", + "Byrd", + "Cabrera", + "Cain", + "Calderon", + "Caldwell", + "Calhoun", + "Callahan", + "Camacho", + "Cameron", + "Campbell", + "Campos", + "Cannon", + "Cantrell", + "Cantu", + "Cardenas", + "Carey", + "Carlson", + "Carney", + "Carpenter", + "Carr", + "Carrillo", + "Carroll", + "Carson", + "Carter", + "Carver", + "Case", + "Casey", + "Cash", + "Castaneda", + "Castillo", + "Castro", + "Cervantes", + "Chambers", + "Chan", + "Chandler", + "Chaney", + "Chang", + "Chapman", + "Charles", + "Chase", + "Chavez", + "Chen", + "Cherry", + "Christensen", + "Christian", + "Church", + "Clark", + "Clarke", + "Clay", + "Clayton", + "Clements", + "Clemons", + "Cleveland", + "Cline", + "Cobb", + "Cochran", + "Coffey", + "Cohen", + "Cole", + "Coleman", + "Collier", + "Collins", + "Colon", + "Combs", + "Compton", + "Conley", + "Conner", + "Conrad", + "Contreras", + "Conway", + "Cook", + "Cooke", + "Cooley", + "Cooper", + "Copeland", + "Cortez", + "Cote", + "Cotton", + "Cox", + "Craft", + "Craig", + "Crane", + "Crawford", + "Crosby", + "Cross", + "Cruz", + "Cummings", + "Cunningham", + "Curry", + "Curtis", + "Dale", + "Dalton", + "Daniel", + "Daniels", + "Daugherty", + "Davenport", + "David", + "Davidson", + "Davis", + "Dawson", + "Day", + "Dean", + "Decker", + "Dejesus", + "Delacruz", + "Delaney", + "Deleon", + "Delgado", + "Dennis", + "Diaz", + "Dickerson", + "Dickson", + "Dillard", + "Dillon", + "Dixon", + "Dodson", + "Dominguez", + "Donaldson", + "Donovan", + "Dorsey", + "Dotson", + "Douglas", + "Downs", + "Doyle", + "Drake", + "Dudley", + "Duffy", + "Duke", + "Duncan", + "Dunlap", + "Dunn", + "Duran", + "Durham", + "Dyer", + "Eaton", + "Edwards", + "Elliott", + "Ellis", + "Ellison", + "Emerson", + "England", + "English", + "Erickson", + "Espinoza", + "Estes", + "Estrada", + "Evans", + "Everett", + "Ewing", + "Farley", + "Farmer", + "Farrell", + "Faulkner", + "Ferguson", + "Fernandez", + "Ferrell", + "Fields", + "Figueroa", + "Finch", + "Finley", + "Fischer", + "Fisher", + "Fitzgerald", + "Fitzpatrick", + "Fleming", + "Fletcher", + "Flores", + "Flowers", + "Floyd", + "Flynn", + "Foley", + "Forbes", + "Ford", + "Foreman", + "Foster", + "Fowler", + "Fox", + "Francis", + "Franco", + "Frank", + "Franklin", + "Franks", + "Frazier", + "Frederick", + "Freeman", + "French", + "Frost", + "Fry", + "Frye", + "Fuentes", + "Fuller", + "Fulton", + "Gaines", + "Gallagher", + "Gallegos", + "Galloway", + "Gamble", + "Garcia", + "Gardner", + "Garner", + "Garrett", + "Garrison", + "Garza", + "Gates", + "Gay", + "Gentry", + "George", + "Gibbs", + "Gibson", + "Gilbert", + "Giles", + "Gill", + "Gillespie", + "Gilliam", + "Gilmore", + "Glass", + "Glenn", + "Glover", + "Goff", + "Golden", + "Gomez", + "Gonzales", + "Gonzalez", + "Good", + "Goodman", + "Goodwin", + "Gordon", + "Gould", + "Graham", + "Grant", + "Graves", + "Gray", + "Green", + "Greene", + "Greer", + "Gregory", + "Griffin", + "Griffith", + "Grimes", + "Gross", + "Guerra", + "Guerrero", + "Guthrie", + "Gutierrez", + "Guy", + "Guzman", + "Hahn", + "Hale", + "Haley", + "Hall", + "Hamilton", + "Hammond", + "Hampton", + "Hancock", + "Haney", + "Hansen", + "Hanson", + "Hardin", + "Harding", + "Hardy", + "Harmon", + "Harper", + "Harrell", + "Harrington", + "Harris", + "Harrison", + "Hart", + "Hartman", + "Harvey", + "Hatfield", + "Hawkins", + "Hayden", + "Hayes", + "Haynes", + "Hays", + "Head", + "Heath", + "Hebert", + "Henderson", + "Hendricks", + "Hendrix", + "Henry", + "Hensley", + "Henson", + "Herman", + "Hernandez", + "Herrera", + "Herring", + "Hess", + "Hester", + "Hewitt", + "Hickman", + "Hicks", + "Higgins", + "Hill", + "Hines", + "Hinton", + "Hobbs", + "Hodge", + "Hodges", + "Hoffman", + "Hogan", + "Holcomb", + "Holden", + "Holder", + "Holland", + "Holloway", + "Holman", + "Holmes", + "Holt", + "Hood", + "Hooper", + "Hoover", + "Hopkins", + "Hopper", + "Horn", + "Horne", + "Horton", + "House", + "Houston", + "Howard", + "Howe", + "Howell", + "Hubbard", + "Huber", + "Hudson", + "Huff", + "Huffman", + "Hughes", + "Hull", + "Humphrey", + "Hunt", + "Hunter", + "Hurley", + "Hurst", + "Hutchinson", + "Hyde", + "Ingram", + "Irwin", + "Jackson", + "Jacobs", + "Jacobson", + "James", + "Jarvis", + "Jefferson", + "Jenkins", + "Jennings", + "Jensen", + "Jimenez", + "Johns", + "Johnson", + "Johnston", + "Jones", + "Jordan", + "Joseph", + "Joyce", + "Joyner", + "Juarez", + "Justice", + "Kane", + "Kaufman", + "Keith", + "Keller", + "Kelley", + "Kelly", + "Kemp", + "Kennedy", + "Kent", + "Kerr", + "Key", + "Kidd", + "Kim", + "King", + "Kinney", + "Kirby", + "Kirk", + "Kirkland", + "Klein", + "Kline", + "Knapp", + "Knight", + "Knowles", + "Knox", + "Koch", + "Kramer", + "Lamb", + "Lambert", + "Lancaster", + "Landry", + "Lane", + "Lang", + "Langley", + "Lara", + "Larsen", + "Larson", + "Lawrence", + "Lawson", + "Le", + "Leach", + "Leblanc", + "Lee", + "Leon", + "Leonard", + "Lester", + "Levine", + "Levy", + "Lewis", + "Lindsay", + "Lindsey", + "Little", + "Livingston", + "Lloyd", + "Logan", + "Long", + "Lopez", + "Lott", + "Love", + "Lowe", + "Lowery", + "Lucas", + "Luna", + "Lynch", + "Lynn", + "Lyons", + "Macdonald", + "Macias", + "Mack", + "Madden", + "Maddox", + "Maldonado", + "Malone", + "Mann", + "Manning", + "Marks", + "Marquez", + "Marsh", + "Marshall", + "Martin", + "Martinez", + "Mason", + "Massey", + "Mathews", + "Mathis", + "Matthews", + "Maxwell", + "May", + "Mayer", + "Maynard", + "Mayo", + "Mays", + "Mcbride", + "Mccall", + "Mccarthy", + "Mccarty", + "Mcclain", + "Mcclure", + "Mcconnell", + "Mccormick", + "Mccoy", + "Mccray", + "Mccullough", + "Mcdaniel", + "Mcdonald", + "Mcdowell", + "Mcfadden", + "Mcfarland", + "Mcgee", + "Mcgowan", + "Mcguire", + "Mcintosh", + "Mcintyre", + "Mckay", + "Mckee", + "Mckenzie", + "Mckinney", + "Mcknight", + "Mclaughlin", + "Mclean", + "Mcleod", + "Mcmahon", + "Mcmillan", + "Mcneil", + "Mcpherson", + "Meadows", + "Medina", + "Mejia", + "Melendez", + "Melton", + "Mendez", + "Mendoza", + "Mercado", + "Mercer", + "Merrill", + "Merritt", + "Meyer", + "Meyers", + "Michael", + "Middleton", + "Miles", + "Miller", + "Mills", + "Miranda", + "Mitchell", + "Molina", + "Monroe", + "Montgomery", + "Montoya", + "Moody", + "Moon", + "Mooney", + "Moore", + "Morales", + "Moran", + "Moreno", + "Morgan", + "Morin", + "Morris", + "Morrison", + "Morrow", + "Morse", + "Morton", + "Moses", + "Mosley", + "Moss", + "Mueller", + "Mullen", + "Mullins", + "Munoz", + "Murphy", + "Murray", + "Myers", + "Nash", + "Navarro", + "Neal", + "Nelson", + "Newman", + "Newton", + "Nguyen", + "Nichols", + "Nicholson", + "Nielsen", + "Nieves", + "Nixon", + "Noble", + "Noel", + "Nolan", + "Norman", + "Norris", + "Norton", + "Nunez", + "Obrien", + "Ochoa", + "Oconnor", + "Odom", + "Odonnell", + "Oliver", + "Olsen", + "Olson", + "Oneal", + "Oneil", + "Oneill", + "Orr", + "Ortega", + "Ortiz", + "Osborn", + "Osborne", + "Owen", + "Owens", + "Pace", + "Pacheco", + "Padilla", + "Page", + "Palmer", + "Park", + "Parker", + "Parks", + "Parrish", + "Parsons", + "Pate", + "Patel", + "Patrick", + "Patterson", + "Patton", + "Paul", + "Payne", + "Pearson", + "Peck", + "Pena", + "Pennington", + "Perez", + "Perkins", + "Perry", + "Peters", + "Petersen", + "Peterson", + "Petty", + "Phelps", + "Phillips", + "Pickett", + "Pierce", + "Pittman", + "Pitts", + "Pollard", + "Poole", + "Pope", + "Porter", + "Potter", + "Potts", + "Powell", + "Powers", + "Pratt", + "Preston", + "Price", + "Prince", + "Pruitt", + "Puckett", + "Pugh", + "Quinn", + "Ramirez", + "Ramos", + "Ramsey", + "Randall", + "Randolph", + "Rasmussen", + "Ratliff", + "Ray", + "Raymond", + "Reed", + "Reese", + "Reeves", + "Reid", + "Reilly", + "Reyes", + "Reynolds", + "Rhodes", + "Rice", + "Rich", + "Richard", + "Richards", + "Richardson", + "Richmond", + "Riddle", + "Riggs", + "Riley", + "Rios", + "Rivas", + "Rivera", + "Rivers", + "Roach", + "Robbins", + "Roberson", + "Roberts", + "Robertson", + "Robinson", + "Robles", + "Rocha", + "Rodgers", + "Rodriguez", + "Rodriquez", + "Rogers", + "Rojas", + "Rollins", + "Roman", + "Romero", + "Rosa", + "Rosales", + "Rosario", + "Rose", + "Ross", + "Roth", + "Rowe", + "Rowland", + "Roy", + "Ruiz", + "Rush", + "Russell", + "Russo", + "Rutledge", + "Ryan", + "Salas", + "Salazar", + "Salinas", + "Sampson", + "Sanchez", + "Sanders", + "Sandoval", + "Sanford", + "Santana", + "Santiago", + "Santos", + "Sargent", + "Saunders", + "Savage", + "Sawyer", + "Schmidt", + "Schneider", + "Schroeder", + "Schultz", + "Schwartz", + "Scott", + "Sears", + "Sellers", + "Serrano", + "Sexton", + "Shaffer", + "Shannon", + "Sharp", + "Sharpe", + "Shaw", + "Shelton", + "Shepard", + "Shepherd", + "Sheppard", + "Sherman", + "Shields", + "Short", + "Silva", + "Simmons", + "Simon", + "Simpson", + "Sims", + "Singleton", + "Skinner", + "Slater", + "Sloan", + "Small", + "Smith", + "Snider", + "Snow", + "Snyder", + "Solis", + "Solomon", + "Sosa", + "Soto", + "Sparks", + "Spears", + "Spence", + "Spencer", + "Stafford", + "Stanley", + "Stanton", + "Stark", + "Steele", + "Stein", + "Stephens", + "Stephenson", + "Stevens", + "Stevenson", + "Stewart", + "Stokes", + "Stone", + "Stout", + "Strickland", + "Strong", + "Stuart", + "Suarez", + "Sullivan", + "Summers", + "Sutton", + "Swanson", + "Sweeney", + "Sweet", + "Sykes", + "Talley", + "Tanner", + "Tate", + "Taylor", + "Terrell", + "Terry", + "Thomas", + "Thompson", + "Thornton", + "Tillman", + "Todd", + "Torres", + "Townsend", + "Tran", + "Travis", + "Trevino", + "Trujillo", + "Tucker", + "Turner", + "Tyler", + "Tyson", + "Underwood", + "Valdez", + "Valencia", + "Valentine", + "Valenzuela", + "Vance", + "Vang", + "Vargas", + "Vasquez", + "Vaughan", + "Vaughn", + "Vazquez", + "Vega", + "Velasquez", + "Velazquez", + "Velez", + "Villarreal", + "Vincent", + "Vinson", + "Wade", + "Wagner", + "Walker", + "Wall", + "Wallace", + "Waller", + "Walls", + "Walsh", + "Walter", + "Walters", + "Walton", + "Ward", + "Ware", + "Warner", + "Warren", + "Washington", + "Waters", + "Watkins", + "Watson", + "Watts", + "Weaver", + "Webb", + "Weber", + "Webster", + "Weeks", + "Weiss", + "Welch", + "Wells", + "West", + "Wheeler", + "Whitaker", + "White", + "Whitehead", + "Whitfield", + "Whitley", + "Whitney", + "Wiggins", + "Wilcox", + "Wilder", + "Wiley", + "Wilkerson", + "Wilkins", + "Wilkinson", + "William", + "Williams", + "Williamson", + "Willis", + "Wilson", + "Winters", + "Wise", + "Witt", + "Wolf", + "Wolfe", + "Wong", + "Wood", + "Woodard", + "Woods", + "Woodward", + "Wooten", + "Workman", + "Wright", + "Wyatt", + "Wynn", + "Yang", + "Yates", + "York", + "Young", + "Zamora", + "Zimmerman"] \ No newline at end of file diff --git a/delete.tpl b/delete.tpl new file mode 100644 index 0000000..685958f --- /dev/null +++ b/delete.tpl @@ -0,0 +1,38 @@ + + + + + + + + + {{ str['del-account'] }} + + + + + +

+

{{ str['del-account'] }}

+ +
+ + + + +
+ + +
+ +
+ + %for type, text, animation in get('alerts', []): +
+
{{ text }}
+
+ %end + +
+ + diff --git a/edit_email.tpl b/edit_email.tpl new file mode 100644 index 0000000..458a604 --- /dev/null +++ b/edit_email.tpl @@ -0,0 +1,38 @@ + + + + + + + + + {{ str['edit-email'] }} + + + + + +
+

{{ str['edit-email'] }}

+ +
+ + + + +
+ + +
+ +
+ + %for type, text, animation in get('alerts', []): +
+
{{ text }}
+
+ %end + +
+ + diff --git a/edit_fullname.tpl b/edit_fullname.tpl new file mode 100644 index 0000000..c544666 --- /dev/null +++ b/edit_fullname.tpl @@ -0,0 +1,41 @@ + + + + + + + + + {{ str['edit-fn'] }} + + + + + +
+

{{ str['edit-fn'] }}

+ +
+ + + + + + + +
+ + +
+ +
+ + %for type, text, animation in get('alerts', []): +
+
{{ text }}
+
+ %end + +
+ + diff --git a/index.tpl b/index.tpl new file mode 100644 index 0000000..c47460b --- /dev/null +++ b/index.tpl @@ -0,0 +1,37 @@ + + + + + + + + + {{ str['login'] }} + + + + + +
+

{{ str['login'] }}

+ +
+ + + + + + + + {{ str['or-sign-up'] }} +
+ + %for type, text, animation in get('alerts', []): +
+
{{ text }}
+
+ %end + +
+ + diff --git a/locales/base.pot b/locales/base.pot new file mode 100644 index 0000000..642cbd6 --- /dev/null +++ b/locales/base.pot @@ -0,0 +1,215 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , 2022. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 0.0.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-04-07 17:23+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Aitzol Berasategi \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: app.py:880 +msgid "User" +msgstr "" + +#: app.py:881 +msgid "Username" +msgstr "" + +#: app.py:882 +msgid "Firstname" +msgstr "" + +#: app.py:883 +msgid "Surname" +msgstr "" + +#: app.py:884 +msgid "Password" +msgstr "" + +#: app.py:885 +msgid "Old password" +msgstr "" + +#: app.py:886 +msgid "New password" +msgstr "" + +#: app.py:887 +msgid "Confirm password" +msgstr "" + +#: app.py:888 +msgid "Email" +msgstr "" + +#: app.py:889 +msgid "edit" +msgstr "" + +#: app.py:890 +msgid "Login" +msgstr "" + +#: app.py:891 +msgid "Logout" +msgstr "" + +#: app.py:892 +msgid "Delete" +msgstr "" + +#: app.py:893 +msgid "Sign Up" +msgstr "" + +#: app.py:894 +msgid "Back" +msgstr "" + +#: app.py:895 +msgid "Update" +msgstr "" + +#: app.py:896 +msgid "Or Sign In" +msgstr "" + +#: app.py:897 +msgid "Or Sign Up" +msgstr "" + +#: app.py:898 +msgid "Invite code" +msgstr "" + +#: app.py:899 +msgid "Edit your fullname" +msgstr "" + +#: app.py:900 +msgid "Edit your email" +msgstr "" + +#: app.py:901 +msgid "Change your password" +msgstr "" + +#: app.py:902 +msgid "Delete your account" +msgstr "" + +#: app.py:903 app.py:909 +msgid "Welcome" +msgstr "" + +#: app.py:906 +msgid "The session was closed." +msgstr "" + +#: app.py:907 +msgid "Username must be at least 3 characters long!" +msgstr "" + +#: app.py:908 +msgid "Please enter your password!" +msgstr "" + +#: app.py:910 +msgid "The code is invalid or has expired." +msgstr "" + +#: app.py:911 +msgid "A bit short, don't you think?!" +msgstr "" + +#: app.py:912 +msgid "Not allowed characters for the username field." +msgstr "" + +#: app.py:913 +msgid "Passwords do not match!" +msgstr "" + +#: app.py:914 +msgid "Password must be at least 8 characters long!" +msgstr "" + +#: app.py:915 +msgid "Congratulations, your account has been created!" +msgstr "" + +#: app.py:916 +msgid "Your firstname is a bit short, don't you think?" +msgstr "" + +#: app.py:917 +msgid "Your surname is a bit short, don't you think?" +msgstr "" + +#: app.py:918 +msgid "Your first and last name have been successfully updated." +msgstr "" + +#: app.py:919 +msgid "Invalid email address. Please try again." +msgstr "" + +#: app.py:920 +msgid "Your email has been successfully updated." +msgstr "" + +#: app.py:921 +msgid "The password entered is the same as the current password." +msgstr "" + +#: app.py:922 +msgid "Password has been changed!" +msgstr "" + +#: app.py:923 +msgid "Please, type your username for account deletion." +msgstr "" + +#: app.py:924 +msgid "Account successfully deleted!" +msgstr "" + +#: app.py:925 +msgid "Username or password is incorrect!" +msgstr "" + +#: app.py:926 +msgid "Unable to connect to the remote server." +msgstr "" + +#: app.py:927 +msgid "" +"Encountered an unexpected error while communicating with the remote server." +msgstr "" + +#: app.py:928 +msgid "User already exists." +msgstr "" + +#: app.py:929 +msgid "Email already exists." +msgstr "" + +#: app.py:930 +msgid "Forgot your password? Please try again." +msgstr "" + +#: app.py:931 +msgid "The session has expired." +msgstr "" \ No newline at end of file diff --git a/locales/en/LC_MESSAGES/base.mo b/locales/en/LC_MESSAGES/base.mo new file mode 100644 index 0000000000000000000000000000000000000000..ddd17cca49433b49725f30e2bc246a07fc353825 GIT binary patch literal 3855 zcmeH|O^g&p6vqoi#T5ln6i}g7#YNd}7A0{RP#D+&R@o0>SA|>AT|3j#Q$6XAT~-qk zViHE)9fW&tq@f&HsnljT3Lk(C$WOgab2oegu7&Tz9qP1@cpW(8&6K!PW3@=)+ZX z%KMYB4~}#=0ax*U5^jP49E4}#z3?M=AN&Fi!JptQa4n5+1IgtQ)%Orr)deW=`4!6km!a&tf|m!O59w0R!AIc2QNWcZxg}Uh&EfG z=&>6oMUNMue4mA~-z!k`coWJxA3(9|8z_4G07Z{WQ1rM0C9Z1-vgolMiXPjc=rIQe z;Hz*O{1Ea}-_ppu3sBbo4NClfhoZ;j&ieqJY@v2_xDVozdIpLf$KfD!P~!P66g@tL zqQ}=z;-qeGa}^Xl)m!KBQO4*VNVnQfeT*vWcZ>Kg_IAsiod>Z^{KLJnh%I7= z#8irCEa!16RpKGOVXEqJALg>pPI&119RMAfSmt!sYaRXI~_#%F7 zp^D$4iNt#&^;*TJQ{o}HBKaq=eweDN`c#zbtdXSopbir^kn8!R(0Lp1 z_Dq!JnXhWDbjhYR)W+$;*)uI0u&K6bnxuNJ5ye&qNwZnFD8O7-*K<+cASPC)wwdHs zXEvR+sjm)2X_l8e9!k=B(lM(Wg?)N5wkESWPv^C%o5=ZU)XkbWVz0JsVVDwD|Jnm1 zb_$Zvs-sCga?&P6uEyQRb*D$2>OM}l)MQt}?t|`$Q!0(_3!j=rmLr@txfV}zJ$$tlapLN|!5QhL9riX& z9yI80$HqD=UYtGMZ-Y9YZ*?-O=LxQL)22*B(#cn)BUpdDDE(DaI4_~oQKq9#$eAdv zAa{iJemSPq@m7~+5JbjH;4%psgXK9W(v;5&fospq(2cE^F4_Gs$MhghGTCa{nNuWH zo?w)7WScz7+W*R@=~hVkt7&8_nl4#y62y-TRQQPvh# z^tt|M%k7{h)8r-G@)hwTJ@MWHsOs_^^u9n*y#3?6XUhMqj>2}rN(7kWJy}l)hK^CiS+@Snv8k% zNbfae*Z!FEo*3U2+0#sO^}^JxH)Tz;qK{U`4^LMQ*Yw`n^nP3T+#S6!STi;{R_htQ z(;w>NKF!HNZ;As^(YZa7?`X*edrGl0n?E%@b;uj;jS~}RZ0gBv7jlJF^e`8F-|~IG WeBUqM_bLCs^0%{m-~UhU`+opH@EO7Y literal 0 HcmV?d00001 diff --git a/locales/en/LC_MESSAGES/base.po b/locales/en/LC_MESSAGES/base.po new file mode 100644 index 0000000..0dc32d1 --- /dev/null +++ b/locales/en/LC_MESSAGES/base.po @@ -0,0 +1,215 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , 2022. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 0.0.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-04-07 17:23+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Aitzol Berasategi \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: app.py:880 +msgid "User" +msgstr "User" + +#: app.py:881 +msgid "Username" +msgstr "Username" + +#: app.py:882 +msgid "Firstname" +msgstr "Firstname" + +#: app.py:883 +msgid "Surname" +msgstr "Surname" + +#: app.py:884 +msgid "Password" +msgstr "Password" + +#: app.py:885 +msgid "Old password" +msgstr "Old password" + +#: app.py:886 +msgid "New password" +msgstr "New password" + +#: app.py:887 +msgid "Confirm password" +msgstr "Confirm password" + +#: app.py:888 +msgid "Email" +msgstr "Email" + +#: app.py:889 +msgid "edit" +msgstr "edit" + +#: app.py:890 +msgid "Login" +msgstr "Login" + +#: app.py:891 +msgid "Logout" +msgstr "Logout" + +#: app.py:892 +msgid "Delete" +msgstr "Delete" + +#: app.py:893 +msgid "Sign Up" +msgstr "Sign Up" + +#: app.py:894 +msgid "Back" +msgstr "Back" + +#: app.py:895 +msgid "Update" +msgstr "Update" + +#: app.py:896 +msgid "Or Sign In" +msgstr "Or Sign In" + +#: app.py:897 +msgid "Or Sign Up" +msgstr "Or Sign Up" + +#: app.py:898 +msgid "Invite code" +msgstr "Invite code" + +#: app.py:899 +msgid "Edit your fullname" +msgstr "Edit your fullname" + +#: app.py:900 +msgid "Edit your email" +msgstr "Edit your email" + +#: app.py:901 +msgid "Change your password" +msgstr "Change your password" + +#: app.py:902 +msgid "Delete your account" +msgstr "Delete your account" + +#: app.py:903 app.py:909 +msgid "Welcome" +msgstr "Welcome" + +#: app.py:906 +msgid "The session was closed." +msgstr "The session was closed." + +#: app.py:907 +msgid "Username must be at least 3 characters long!" +msgstr "Username must be at least 3 characters long!" + +#: app.py:908 +msgid "Please enter your password!" +msgstr "Please enter your password!" + +#: app.py:910 +msgid "The code is invalid or has expired." +msgstr "The code is invalid or has expired." + +#: app.py:911 +msgid "A bit short, don't you think?!" +msgstr "A bit short, don't you think?!" + +#: app.py:912 +msgid "Not allowed characters for the username field." +msgstr "Not allowed characters for the username field." + +#: app.py:913 +msgid "Passwords do not match!" +msgstr "Passwords do not match!" + +#: app.py:914 +msgid "Password must be at least 8 characters long!" +msgstr "Password must be at least 8 characters long!" + +#: app.py:915 +msgid "Congratulations, your account has been created!" +msgstr "Congratulations, your account has been created!" + +#: app.py:916 +msgid "Your firstname is a bit short, don't you think?" +msgstr "Your firstname is a bit short, don't you think?" + +#: app.py:917 +msgid "Your surname is a bit short, don't you think?" +msgstr "Your surname is a bit short, don't you think?" + +#: app.py:918 +msgid "Your first and last name have been successfully updated." +msgstr "Your first and last name have been successfully updated." + +#: app.py:919 +msgid "Invalid email address. Please try again." +msgstr "Invalid email address. Please try again." + +#: app.py:920 +msgid "Your email has been successfully updated." +msgstr "Your email has been successfully updated." + +#: app.py:921 +msgid "The password entered is the same as the current password." +msgstr "The password entered is the same as the current password." + +#: app.py:922 +msgid "Password has been changed!" +msgstr "Password has been changed!" + +#: app.py:923 +msgid "Please, type your username for account deletion." +msgstr "Please, type your username for account deletion." + +#: app.py:924 +msgid "Account successfully deleted!" +msgstr "Account successfully deleted!" + +#: app.py:925 +msgid "Username or password is incorrect!" +msgstr "Username or password is incorrect!" + +#: app.py:926 +msgid "Unable to connect to the remote server." +msgstr "Unable to connect to the remote server." + +#: app.py:927 +msgid "" +"Encountered an unexpected error while communicating with the remote server." +msgstr "Encountered an unexpected error while communicating with the remote server." + +#: app.py:928 +msgid "User already exists." +msgstr "User already exists." + +#: app.py:929 +msgid "Email already exists." +msgstr "Email already exists." + +#: app.py:930 +msgid "Forgot your password? Please try again." +msgstr "Forgot your password? Please try again." + +#: app.py:931 +msgid "The session has expired." +msgstr "The session has expired." diff --git a/locales/eu/LC_MESSAGES/base.mo b/locales/eu/LC_MESSAGES/base.mo new file mode 100644 index 0000000000000000000000000000000000000000..0f92fda0108e1c8cdf5478c1dab23720e8231c8e GIT binary patch literal 3967 zcmai$PmCN#6^9E5Nmv4b0Ex|?xE2Ub$adSt0xg5VS+B>=67L^*cLDnXHPf{{#q@NK zs=K|rBSlCYA`llizy(F(76~CFZb-R-xN+iuI6#Csfg&z&h(I9tUUkpRuGdOh+x_)) zSJiv(_g=l;*B*P|hl=AfoFC=<+1r#_g_qvJh2!}LlzI}b!H>ZK9`TaS#09RXXz=!#L3w{{(;1YZhJ^_CUKLURR&%@W?L-0{9!Y8llOM`{Z`1HT5B;kTja z_$rLx>rmD^jX0;t&67~_v`mm{4h!QEIb8e+y)f8UvGcsQ1bjO zDE0JRDC@rhMb|$=skeW@4m|aqCa>F2;`j!98V=w^_yZ{M{054hKf+JL*P+;XoQK>h z4K<9R=(7oB{T__r%a9P%Z=u-#13U}gfU@2hf?I-LhT_LQBpr(|d4^N=h{PwgaK9tw;WZz z%l*P9YPHDsoF5%~ka|%O-pEPXZDPz{a^R^0@TgE>1g zE<5sD+{?|#s&4Z_uG(a*nH*|+;L560-8@8Oy-l>qb)DOTvF&41Tki|6_XjSsx?hY& zb?*9jo2Po;RRbik+S^f4SzTIx$NE@ZbH1#C!E1$2i`J{D2$%I%W=&~z zt#MS`(|gWlNvt-rWIlJ(>us0jdM#Je+ugC+nr3i%qvyE^i6-r{SEIVD$eT8mmPjkT zeAJ7q$kX}YlIqeq{*Fx5AMm`<#^!i@ym+SXmUMMEZVPK33877^Ce$I-6sr&j-f!0- zUbRE;BAs@ncCF-0lu$^wBlmF_Gj5~R!3{)Yya-o{XbhJ7e(gQ4XTmizOY(;4lS@ut z>@&Te6{VQk%}tM@stTOSbE-{lW$k zF$~8K^JYD$F;q5*mHfQXPV6 zotDu~(a{TAo6${s$H~a(Dk$ z9lheJNs;L*)|--fsneHCc=TMxelJXH>+(xX%hTGVHrlaf)X_IqHm>ikT<_{j-QDMG z9n(AJWAJ8uZM{2p{6c(wfp%I^gV7EfqN6K&P@Nl#fiHw)m$v%i?#{Jnc|J~D+_OHC zZkN!)I(nHtzo2fIUhSg;Gcw+rC2c1%s~CuUKtRg*e% zvhB@);Ec8@ZBx4Wk!L;Hr>#n-ZWkUI&2ULiYH#(hS+qpj!^3+;usA<`HV6&b} zEGEdK7+9OwJTg58%y9NJ^mj9zgTbb|;~d|R#KfvQk&uQ()6}U)T2yMiEXjM#mL>vT zPpn6miSf>OJ9IhjO5YzERx;AB#jd(7fv|Jh@Xf$XWF3^dthbFbEJL%NQ1#xs$&wyf zw!NLid>}Jdq^1`<2`zsbVBAimTJAGZtLsIM5N1p~LmHV;*eQ&(8`s{t>R_Ie#x#m} zw8{ubK4KyoX(CMW;e$4kaM~Y>^064wdaW78vn=Wfp-~uoQVa2KYEXW*#S!v2n_%X5 z^P(^q=F2LTGb@>)PHX(dp9vp{nQ>gpYI4v{=~>L9>#>RAGMf!`4UKA>K7YB+D|uub?f^NcT$_*IG4 zGq-{w>5&JbJ0-$?lyzi|D14u#!eitLqq-A^YC}eztXd=6%MK}P%9X00?aQNea8Fh6 zuinY+azdDsmBjX&=kVe@hfTKI)VYe}9W@?`u+eNTOfh7mupXsIk!%JbX{Vm%Av-O6 wf2MtC8dA_Ayq2<_w}hr!%}23h+$, 2022. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 0.0.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-04-07 17:23+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Aitzol Berasategi \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: app.py:880 +msgid "User" +msgstr "Erabiltzailea" + +#: app.py:881 +msgid "Username" +msgstr "Erabiltzaile izena" + +#: app.py:882 +msgid "Firstname" +msgstr "Izena" + +#: app.py:883 +msgid "Surname" +msgstr "Abizena" + +#: app.py:884 +msgid "Password" +msgstr "Pasahitza" + +#: app.py:885 +msgid "Old password" +msgstr "Pasahitz zaharra" + +#: app.py:886 +msgid "New password" +msgstr "Pasahitz berria" + +#: app.py:887 +msgid "Confirm password" +msgstr "Pasahitza berretsi" + +#: app.py:888 +msgid "Email" +msgstr "Emaila" + +#: app.py:889 +msgid "edit" +msgstr "editatu" + +#: app.py:890 +msgid "Login" +msgstr "Saioa hasi" + +#: app.py:891 +msgid "Logout" +msgstr "Saioa itxi" + +#: app.py:892 +msgid "Delete" +msgstr "Ezabatu" + +#: app.py:893 +msgid "Sign Up" +msgstr "Kontua sortu" + +#: app.py:894 +msgid "Back" +msgstr "Itzuli" + +#: app.py:895 +msgid "Update" +msgstr "Eguneratu" + +#: app.py:896 +msgid "Or Sign In" +msgstr "Edo Saioa hasi" + +#: app.py:897 +msgid "Or Sign Up" +msgstr "Edo Kontua sortu" + +#: app.py:898 +msgid "Invite code" +msgstr "Gonbidapen kodea" + +#: app.py:899 +msgid "Edit your fullname" +msgstr "Izen-abizenak editatu" + +#: app.py:900 +msgid "Edit your email" +msgstr "Email helbidea editatu" + +#: app.py:901 +msgid "Change your password" +msgstr "Pasahitza eguneratu" + +#: app.py:902 +msgid "Delete your account" +msgstr "Kontua ezabatu" + +#: app.py:903 app.py:909 +msgid "Welcome" +msgstr "Ongi etorri" + +#: app.py:906 +msgid "The session was closed." +msgstr "Saioa itxi da." + +#: app.py:907 +msgid "Username must be at least 3 characters long!" +msgstr "Erabiltzaile izenak gutxienez 3 karaktere izan behar ditu!" + +#: app.py:908 +msgid "Please enter your password!" +msgstr "Sartu zure pasahitza, mesedez!" + +#: app.py:910 +msgid "The code is invalid or has expired." +msgstr "Kodea baliogabea da edo iraungi egin da." + +#: app.py:911 +msgid "A bit short, don't you think?!" +msgstr "Labur-xamarra, ez duzu uste?" + +#: app.py:912 +msgid "Not allowed characters for the username field." +msgstr "Erabiltzaile izenerako onartzen ez diren karaktereak." + +#: app.py:913 +msgid "Passwords do not match!" +msgstr "Pasahitzak ez datoz bat!" + +#: app.py:914 +msgid "Password must be at least 8 characters long!" +msgstr "Pasahitzak gutxienez 8 karaktereko luzera izan behar du!" + +#: app.py:915 +msgid "Congratulations, your account has been created!" +msgstr "Zorionak, zure kontua sortu da!" + +#: app.py:916 +msgid "Your firstname is a bit short, don't you think?" +msgstr "Zure izena labur-xamarra da, ez duzu uste?" + +#: app.py:917 +msgid "Your surname is a bit short, don't you think?" +msgstr "Zure abizena labur-xamarra da, ez duzu uste?" + +#: app.py:918 +msgid "Your first and last name have been successfully updated." +msgstr "Zure izen-abizenak ongi eguneratu dira." + +#: app.py:919 +msgid "Invalid email address. Please try again." +msgstr "Baliogabeko email helbidea. Saia zaitez berriz, mesedez. " + +#: app.py:920 +msgid "Your email has been successfully updated." +msgstr "Zure emaila ongi eguneratu da." + +#: app.py:921 +msgid "The password entered is the same as the current password." +msgstr "Sartutako pasahitza egungo pasahitzaren berdina da." + +#: app.py:922 +msgid "Password has been changed!" +msgstr "Pasahitza eguneratua izan da!" + +#: app.py:923 +msgid "Please, type your username for account deletion." +msgstr "Kontua ezabatzeko idatzi zure erabiltzaile izena, mesedez." + +#: app.py:924 +msgid "Account successfully deleted!" +msgstr "Kontua ongi ezabatu da!" + +#: app.py:925 +msgid "Username or password is incorrect!" +msgstr "Erabiltzaile izena edo pasahitza okerrak dira!" + +#: app.py:926 +msgid "Unable to connect to the remote server." +msgstr "Ezinezkoa urruneko zerbitzara konektatzea." + +#: app.py:927 +msgid "" +"Encountered an unexpected error while communicating with the remote server." +msgstr "Ezusteko errore bat gertatu da urruneko zerbitzariarekin komunikatzean." + +#: app.py:928 +msgid "User already exists." +msgstr "Erabiltzaile hori existitzen da." + +#: app.py:929 +msgid "Email already exists" +msgstr "Email hori existitzen da." + +#: app.py:930 +msgid "Forgot your password? Please try again." +msgstr "Zure pasahitza ahaztu duzu? Saia zeitez berriz, mesedez." + +#: app.py:931 +msgid "The session has expired." +msgstr "Saioa iraungi egin da." diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..207ea11 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +bottle>=0.12.19 +ldap3>=2.9.1 \ No newline at end of file diff --git a/settings.ini.example b/settings.ini.example new file mode 100644 index 0000000..2a54054 --- /dev/null +++ b/settings.ini.example @@ -0,0 +1,42 @@ +#[html] +#page_title = Login + +[ldap:0] +host = localhost +port = 636 +use_ssl = True +base = ou=groups,dc=example,dc=org +search_filter = uid={uid} + +# LDAP zerbitzariak gehitu nahi badira... +#[ldap:1] +#host = localhost +#base = ou=People,dc=example,dc=org +#search_filter = uid={uid} + +[uwsgi] +plugins = python3 +socket = /run/uwsgi/main.sock +chdir = /var/www/scripts +logger = file:/var/log/uwsgi/main.log +processes = 1 +threads = 2 +# map URI paths to applications +mount = /admin/ldap-python-webui=ldap-python-webui/app.py +manage-script-name = true + +[server] +server = auto +host = localhost +port = 8080 + +[session] +type = file +expire = 300 +data_dir = ./session +auto = true + +[locale] +dir = ./locales +#default lang +lang = en \ No newline at end of file diff --git a/signup.tpl b/signup.tpl new file mode 100644 index 0000000..a110e8b --- /dev/null +++ b/signup.tpl @@ -0,0 +1,55 @@ + + + + + + + + + {{ str['sign-up'] }} + + + + + +
+

{{ str['sign-up'] }}

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + {{ str['or-sign-in'] }} + +
+ + %for type, text, animation in get('alerts', []): +
+
{{ text }}
+
+ %end + +
+ + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..02b49ca --- /dev/null +++ b/static/style.css @@ -0,0 +1,203 @@ +body { + font-family: sans-serif; + color: #333; +} + +main { + margin: 0 auto; +} + +h1 { + font-size: 2em; + margin-bottom: 2.5em; + margin-top: 2em; + text-align: center; +} + +h1 span{ + text-transform: capitalize; +} + +a:link{ + text-decoration: none; +} + +a:active { + color: #1F1F1F; +} + +.user p{ + text-transform: capitalize; +} +.edit { + display: none; +} + +form[name="fullNameForm"] input{ + text-transform: capitalize; +} + + +form { +/* border-radius: 0.2rem; + border: 1px solid #CCC;*/ + margin: 0 auto; + max-width: 16rem; + padding: 2rem 2.5rem 1.5rem 2.5rem; +} + +input { + background-color: #FAFAFA; + border-radius: 0.2rem; + border: 1px solid #CCC; + box-shadow: inset 0 1px 3px #DDD; + box-sizing: border-box; + display: block; + font-size: 1em; + padding: 0.4em 0.6em; + vertical-align: middle; + width: 100%; +} + +.form-buttons{ + display:flex; + flex-direction:row; +} + +.form-buttons > a, .form-buttons > button{ + flex-grow:1; +} + +input:focus { + background-color: #FFF; + border-color: #51A7E8; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075) inset, 0 0 5px rgba(81, 167, 232, 0.5); + outline: 0; +} + +input.not-required { + background-color: #E0E0E0; +} + +label { + color: #666; + display: block; + font-size: 0.9em; + font-weight: bold; + margin: 1em 0 0.25em 0; +} + +button { + border-radius: 0.2rem; + border: 1px solid; + box-sizing: border-box; + color: #fff; + cursor: pointer; + display: block; + font-size: 0.9em; + font-weight: bold; + margin: 2em 0 0.5em 0; + padding: 0.5em 0.7em; + text-align: center; + text-decoration: none; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); + user-select: none; + vertical-align: middle; + white-space: nowrap; +} + +button.green { + background-color: #60B044; + background-image: linear-gradient(#8ADD6D, #60B044); + border-color: #5CA941; +} +button.green:focus, +button.green:hover { + background-color: #569E3D; + background-image: linear-gradient(#79D858, #569E3D); + border-color: #4A993E; +} + +button.red { + background-color: #E74C3C; + background-image: linear-gradient(#CF0B04, #E74C3C); + border-color: #C0392B; +} +button.red:focus, +button.red:hover{ + background-color: #D13E2E; + background-image: linear-gradient(#CF0000, #D13E2E); + border-color: #C92F1E; +} + + +/*alerts*/ +.alerts { + margin: 2rem auto 0 auto; + max-width: 30rem; + animation-duration: 5s; + animation-fill-mode: both; + -webkit-animation-duration: 5s; + -webkit-animation-fill-mode: both; +} + +.alert { + border-radius: 0.2rem; + border: 1px solid; + color: #fff; + padding: 0.7em 1.5em; +} + +.alert.error { + background-color: #E74C3C; + border-color: #C0392B; +} + +.alert.success { + background-color: #60B044; + border-color: #5CA941; +} + +@-webkit-keyframes fadeOut { + 0% {opacity: 1;} + 70% {opacity: 1;} + 100% {opacity: 0;} +} + +@keyframes fadeOut { + 0% {opacity: 1;} + 60% {opacity: 1;} + 100% {opacity: 0;} +} + +.fadeOut { + -webkit-animation-name: fadeOut; + animation-name: fadeOut; +} + +/**/ +.grid-container { + display: grid; + grid-template-columns: 90% 10%; + margin: 0 auto; + max-width: 16rem; + padding: 0 2.5rem 0 2.5rem; + align-items: first baseline; +} +.grid-item a { + color: #222; +} +.grid-item a:visited { + color: #888; +} +.grid-item h5 { + text-transform: capitalize; +} + +/**/ +@media only screen and (max-width: 480px) { + + form { + border: 0; + } +} diff --git a/user.tpl b/user.tpl new file mode 100644 index 0000000..d46c1d3 --- /dev/null +++ b/user.tpl @@ -0,0 +1,74 @@ + + + + + + + + + {{ str['welcome'] }} + + + + + +
+

{{ str['welcome'] }} {{ data['username'] }}

+ +
+
+ +
+ + + +
+ +
+ + + +
+ +
+ + + + + +
+ + + %for type, text, animation in get('alerts', []): +
+
{{ text }}
+
+ %end + +
+ + + +