mirror of
https://github.com/hyzendust/freedoms4-backend-public.git
synced 2026-06-30 23:12:18 +02:00
Init
This commit is contained in:
674
LICENSE
Normal file
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
208
admin.php
Normal file
208
admin.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
/**
|
||||
* admin.php — User management backend for freedoms4.org
|
||||
* Hyzen-only. All actions require an active hyzen session.
|
||||
*
|
||||
* Actions:
|
||||
* GET ?action=list_users — list all users
|
||||
* POST { action: "block_user", user_id } — block a user
|
||||
* POST { action: "unblock_user", user_id } — unblock a user
|
||||
* POST { action: "delete_user", user_id } — permanently delete a user
|
||||
*/
|
||||
|
||||
// ── Credentials from env file ──
|
||||
$env_file = '/etc/freedoms4/auth.env';
|
||||
if (!is_readable($env_file)) {
|
||||
http_response_code(503);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['success' => false, 'message' => 'Server configuration error.']);
|
||||
exit;
|
||||
}
|
||||
$env = [];
|
||||
foreach (file($env_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) continue;
|
||||
[$k, $v] = explode('=', $line, 2);
|
||||
$env[trim($k)] = trim($v);
|
||||
}
|
||||
|
||||
define('DB_HOST', $env['DB_HOST'] ?? '127.0.0.1');
|
||||
define('DB_PORT', $env['DB_PORT'] ?? '5432');
|
||||
define('DB_NAME', $env['DB_NAME'] ?? 'freedoms4');
|
||||
define('DB_USER', $env['DB_USER'] ?? 'freedoms4_user');
|
||||
define('DB_PASS', $env['DB_PASS'] ?? '');
|
||||
|
||||
define('SESSION_NAME', 'f4_session');
|
||||
define('SESSION_SECURE', true);
|
||||
define('SESSION_SAMESITE', 'None');
|
||||
define('MAX_BODY_BYTES', 4096);
|
||||
define('ADMIN_USER', 'hyzen');
|
||||
|
||||
// ── CORS ──
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
$allowed_origins = ['https://freedoms4.org', 'https://www.freedoms4.org'];
|
||||
|
||||
if ($origin && !in_array($origin, $allowed_origins, true)) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['success' => false, 'message' => 'Forbidden.']);
|
||||
exit;
|
||||
}
|
||||
if ($origin) {
|
||||
header('Access-Control-Allow-Origin: ' . $origin);
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
}
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
function json_out(array $data, int $status = 200): never {
|
||||
http_response_code($status);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
function db_connect(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo !== null) return $pdo;
|
||||
$dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', DB_HOST, DB_PORT, DB_NAME);
|
||||
try {
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
error_log('admin.php DB error: ' . $e->getMessage());
|
||||
json_out(['success' => false, 'message' => 'Database unavailable.'], 503);
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
// ── Session + admin check ──
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_name(SESSION_NAME);
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'secure' => SESSION_SECURE,
|
||||
'httponly' => true,
|
||||
'samesite' => SESSION_SAMESITE,
|
||||
]);
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (empty($_SESSION['username']) || $_SESSION['username'] !== ADMIN_USER) {
|
||||
json_out(['success' => false, 'message' => 'Unauthorized.'], 403);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// GET: list users
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$pdo = db_connect();
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT id, username, email, blocked, created_at
|
||||
FROM users
|
||||
ORDER BY CASE WHEN username = :admin THEN 0 ELSE 1 END,
|
||||
LOWER(username) ASC,
|
||||
username ASC"
|
||||
);
|
||||
$stmt->execute([':admin' => ADMIN_USER]);
|
||||
json_out(['success' => true, 'users' => $stmt->fetchAll()]);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
json_out(['success' => false, 'message' => 'Method not allowed.'], 405);
|
||||
}
|
||||
|
||||
$content_length = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
|
||||
if ($content_length > MAX_BODY_BYTES) {
|
||||
json_out(['success' => false, 'message' => 'Request too large.'], 413);
|
||||
}
|
||||
$body = json_decode(fread(fopen('php://input', 'r'), MAX_BODY_BYTES), true);
|
||||
if (!is_array($body)) {
|
||||
json_out(['success' => false, 'message' => 'Invalid request body.'], 400);
|
||||
}
|
||||
|
||||
$action = $body['action'] ?? '';
|
||||
$user_id = (int)($body['user_id'] ?? 0);
|
||||
|
||||
if ($user_id === 0) {
|
||||
json_out(['success' => false, 'message' => 'user_id is required.']);
|
||||
}
|
||||
|
||||
$pdo = db_connect();
|
||||
|
||||
// Prevent admin from acting on themselves
|
||||
$stmt = $pdo->prepare('SELECT username FROM users WHERE id = :id LIMIT 1');
|
||||
$stmt->execute([':id' => $user_id]);
|
||||
$target = $stmt->fetch();
|
||||
if (!$target) {
|
||||
json_out(['success' => false, 'message' => 'User not found.'], 404);
|
||||
}
|
||||
if ($target['username'] === ADMIN_USER) {
|
||||
json_out(['success' => false, 'message' => 'Cannot modify the admin account.'], 403);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Block user
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
if ($action === 'block_user') {
|
||||
$pdo->prepare('UPDATE users SET blocked = TRUE WHERE id = :id')
|
||||
->execute([':id' => $user_id]);
|
||||
|
||||
// Backup virtual mail entry so user can no longer receive mail
|
||||
$safe_user = escapeshellarg($target['username']);
|
||||
shell_exec("sudo /usr/local/bin/email-block block {$safe_user} 2>&1");
|
||||
|
||||
json_out(['success' => true]);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Unblock user
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
if ($action === 'unblock_user') {
|
||||
$pdo->prepare('UPDATE users SET blocked = FALSE WHERE id = :id')
|
||||
->execute([':id' => $user_id]);
|
||||
|
||||
// Restore virtual mail entry
|
||||
$safe_user = escapeshellarg($target['username']);
|
||||
shell_exec("sudo /usr/local/bin/email-block unblock {$safe_user} 2>&1");
|
||||
|
||||
json_out(['success' => true]);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Delete user
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
if ($action === 'delete_user') {
|
||||
$safe_user = escapeshellarg($target['username']);
|
||||
|
||||
// Run the same message cleanup as the block action before removing the
|
||||
// mailbox and account configuration permanently.
|
||||
$block_output = [];
|
||||
$block_status = 0;
|
||||
exec("sudo /usr/local/bin/email-block block {$safe_user} 2>&1", $block_output, $block_status);
|
||||
if ($block_status !== 0) {
|
||||
error_log("email-block failed during delete for {$target['username']}: " . implode("\n", $block_output));
|
||||
json_out(['success' => false, 'message' => 'Failed to clear the user email history.'], 500);
|
||||
}
|
||||
|
||||
$delete_output = [];
|
||||
$delete_status = 0;
|
||||
exec("sudo /usr/local/bin/email-delete {$safe_user} 2>&1", $delete_output, $delete_status);
|
||||
if ($delete_status !== 0) {
|
||||
error_log("email-delete failed for {$target['username']}: " . implode("\n", $delete_output));
|
||||
}
|
||||
|
||||
$pdo->prepare('DELETE FROM users WHERE id = :id')
|
||||
->execute([':id' => $user_id]);
|
||||
json_out(['success' => true]);
|
||||
}
|
||||
|
||||
json_out(['success' => false, 'message' => 'Unknown action.'], 400);
|
||||
9
auth-passwdfile.conf.ext
Normal file
9
auth-passwdfile.conf.ext
Normal file
@@ -0,0 +1,9 @@
|
||||
passdb {
|
||||
driver = passwd-file
|
||||
args = scheme=SHA512-CRYPT username_format=%n /var/dovecot/users
|
||||
}
|
||||
userdb {
|
||||
driver = passwd-file
|
||||
args = username_format=%n /var/dovecot/users
|
||||
default_fields = uid=vmail gid=mail home=/var/vmail/%n@freedoms4.org/maildir
|
||||
}
|
||||
600
auth.php
Normal file
600
auth.php
Normal file
@@ -0,0 +1,600 @@
|
||||
<?php
|
||||
/**
|
||||
* auth.php — Login, Sign-Up, OTP/Welcome emails, Prosody XMPP account backend for freedoms4.org
|
||||
*
|
||||
* Sign-up flow:
|
||||
* 1. POST { action: "send_otp", email }
|
||||
* 2. POST { action: "signup", username, email, password, otp }
|
||||
*/
|
||||
|
||||
// ── Credentials from env file ─────────────────────────
|
||||
$env_file = '/etc/freedoms4/auth.env';
|
||||
if (!is_readable($env_file)) {
|
||||
error_log('auth.php: env file not readable: ' . $env_file);
|
||||
http_response_code(503);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['success' => false, 'message' => 'Server configuration error.']);
|
||||
exit;
|
||||
}
|
||||
$env = [];
|
||||
foreach (file($env_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) continue;
|
||||
[$k, $v] = explode('=', $line, 2);
|
||||
$env[trim($k)] = trim($v);
|
||||
}
|
||||
|
||||
define('DB_HOST', $env['DB_HOST'] ?? '127.0.0.1');
|
||||
define('DB_PORT', $env['DB_PORT'] ?? '5432');
|
||||
define('DB_NAME', $env['DB_NAME'] ?? 'freedoms4');
|
||||
define('DB_USER', $env['DB_USER'] ?? 'freedoms4_user');
|
||||
define('DB_PASS', $env['DB_PASS'] ?? '');
|
||||
define('PROSODY_DB_NAME', $env['PROSODY_DB_NAME'] ?? 'prosody');
|
||||
define('PROSODY_DB_USER', $env['PROSODY_DB_USER'] ?? 'prosody');
|
||||
define('PROSODY_DB_PASS', $env['PROSODY_DB_PASS'] ?? '');
|
||||
define('PROSODY_HOST', $env['PROSODY_HOST'] ?? 'freedoms4.org');
|
||||
|
||||
// ── Constants ──
|
||||
define('SESSION_NAME', 'f4_session');
|
||||
define('SESSION_SECURE', true);
|
||||
define('SESSION_SAMESITE', 'None');
|
||||
define('SESSION_TTL', 86400); // 24 hours
|
||||
|
||||
define('OTP_FROM', 'no-reply@freedoms4.org');
|
||||
define('OTP_TTL', 600); // 10 minutes
|
||||
define('OTP_MAX_DAY', 5); // max OTPs per email per 24 h
|
||||
define('OTP_MAX_FAILS', 10); // max failed OTP attempts per IP before lockout
|
||||
define('MAX_BODY_BYTES', 4096);
|
||||
|
||||
// ── CORS ──
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
$allowed_origins = ['https://freedoms4.org', 'https://www.freedoms4.org'];
|
||||
|
||||
if ($origin && !in_array($origin, $allowed_origins, true)) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['success' => false, 'message' => 'Forbidden.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($origin) {
|
||||
header('Access-Control-Allow-Origin: ' . $origin);
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
function json_out(array $data, int $status = 200): never {
|
||||
http_response_code($status);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
function start_session(): void {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_name(SESSION_NAME);
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'secure' => SESSION_SECURE,
|
||||
'httponly' => true,
|
||||
'samesite' => SESSION_SAMESITE,
|
||||
]);
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
function db_connect(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo !== null) return $pdo;
|
||||
$dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', DB_HOST, DB_PORT, DB_NAME);
|
||||
try {
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
error_log('DB connection failed: ' . $e->getMessage());
|
||||
json_out(['success' => false, 'message' => 'Database unavailable.'], 503);
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
function prosody_db_connect(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo !== null) return $pdo;
|
||||
$dsn = sprintf('pgsql:host=127.0.0.1;port=5432;dbname=%s', PROSODY_DB_NAME);
|
||||
// Throws on failure — caller must catch and handle
|
||||
$pdo = new PDO($dsn, PROSODY_DB_USER, PROSODY_DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
// Rate limiting via APCu (per-IP, persistent across requests within the window)
|
||||
// Falls back to session-based if APCu is unavailable.
|
||||
function rate_limit(string $ip, int $max, int $window): bool {
|
||||
$key = 'rl_' . hash('sha256', $ip);
|
||||
if (function_exists('apcu_fetch')) {
|
||||
$count = apcu_fetch($key, $ok);
|
||||
if (!$ok) {
|
||||
apcu_store($key, 1, $window);
|
||||
return true;
|
||||
}
|
||||
if ($count >= $max) return false;
|
||||
apcu_inc($key);
|
||||
return true;
|
||||
}
|
||||
// Session fallback
|
||||
$now = time();
|
||||
$rl = $_SESSION[$key] ?? ['count' => 0, 'window_start' => $now];
|
||||
if ($now - $rl['window_start'] > $window) {
|
||||
$rl = ['count' => 0, 'window_start' => $now];
|
||||
}
|
||||
$rl['count']++;
|
||||
$_SESSION[$key] = $rl;
|
||||
return $rl['count'] <= $max;
|
||||
}
|
||||
|
||||
// OTP failure tracking via APCu (per-IP lockout after OTP_MAX_FAILS attempts)
|
||||
function otp_fail_count(string $ip): int {
|
||||
$key = 'otpfail_' . hash('sha256', $ip);
|
||||
if (function_exists('apcu_fetch')) {
|
||||
$count = apcu_fetch($key, $ok);
|
||||
return $ok ? (int)$count : 0;
|
||||
}
|
||||
return $_SESSION[$key] ?? 0;
|
||||
}
|
||||
|
||||
function otp_fail_increment(string $ip): void {
|
||||
$key = 'otpfail_' . hash('sha256', $ip);
|
||||
if (function_exists('apcu_inc')) {
|
||||
if (!apcu_fetch($key)) {
|
||||
apcu_store($key, 1, 3600);
|
||||
} else {
|
||||
apcu_inc($key);
|
||||
}
|
||||
return;
|
||||
}
|
||||
$_SESSION[$key] = ($_SESSION[$key] ?? 0) + 1;
|
||||
}
|
||||
|
||||
function otp_fail_reset(string $ip): void {
|
||||
$key = 'otpfail_' . hash('sha256', $ip);
|
||||
if (function_exists('apcu_delete')) {
|
||||
apcu_delete($key);
|
||||
return;
|
||||
}
|
||||
unset($_SESSION[$key]);
|
||||
}
|
||||
|
||||
function create_xmpp_account(string $username, string $password): bool {
|
||||
try {
|
||||
$pdo = prosody_db_connect();
|
||||
$host = PROSODY_HOST;
|
||||
$now = time();
|
||||
|
||||
// Never overwrite an existing account
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT 1 FROM prosody WHERE host = :h AND \"user\" = :u AND store = 'accounts' LIMIT 1"
|
||||
);
|
||||
$stmt->execute([':h' => $host, ':u' => $username]);
|
||||
if ($stmt->fetch()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Derive SCRAM-SHA-1 keys
|
||||
$salt = sprintf(
|
||||
'%08x-%04x-%04x-%04x-%012x',
|
||||
random_int(0, 0xffffffff),
|
||||
random_int(0, 0xffff),
|
||||
random_int(0x4000, 0x4fff),
|
||||
random_int(0x8000, 0xbfff),
|
||||
random_int(0, 0xffffffffffff)
|
||||
);
|
||||
$iterations = 10000;
|
||||
$salted_pw = hash_pbkdf2('sha1', $password, $salt, $iterations, 0, true);
|
||||
$client_key = hash_hmac('sha1', 'Client Key', $salted_pw, true);
|
||||
$stored_key = sha1($client_key);
|
||||
$server_key = hash_hmac('sha1', 'Server Key', $salted_pw, false);
|
||||
|
||||
$insert = $pdo->prepare(
|
||||
"INSERT INTO prosody (host, \"user\", store, key, type, value)
|
||||
VALUES (:h, :u, 'accounts', :k, :t, :v)
|
||||
ON CONFLICT (host, \"user\", store, key) DO UPDATE SET type = EXCLUDED.type, value = EXCLUDED.value"
|
||||
);
|
||||
|
||||
$rows = [
|
||||
['salt', 'string', $salt],
|
||||
['iteration_count', 'number', (string)$iterations],
|
||||
['stored_key', 'string', $stored_key],
|
||||
['server_key', 'string', $server_key],
|
||||
['created', 'number', (string)$now],
|
||||
['updated', 'number', (string)$now],
|
||||
];
|
||||
|
||||
$pdo->beginTransaction();
|
||||
foreach ($rows as [$key, $type, $value]) {
|
||||
$insert->execute([':h' => $host, ':u' => $username, ':k' => $key, ':t' => $type, ':v' => $value]);
|
||||
}
|
||||
$pdo->commit();
|
||||
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
error_log("create_xmpp_account failed for {$username}: " . $e->getMessage());
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Only accept POST ──
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
json_out(['success' => false, 'message' => 'Method not allowed.'], 405);
|
||||
}
|
||||
|
||||
// ── Request body size cap ──
|
||||
$content_length = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
|
||||
if ($content_length > MAX_BODY_BYTES) {
|
||||
json_out(['success' => false, 'message' => 'Request too large.'], 413);
|
||||
}
|
||||
|
||||
$raw = fread(fopen('php://input', 'r'), MAX_BODY_BYTES + 1);
|
||||
if (strlen($raw) > MAX_BODY_BYTES) {
|
||||
json_out(['success' => false, 'message' => 'Request too large.'], 413);
|
||||
}
|
||||
|
||||
$body = json_decode($raw, true);
|
||||
if (!is_array($body)) {
|
||||
json_out(['success' => false, 'message' => 'Invalid request body.'], 400);
|
||||
}
|
||||
$action = $body['action'] ?? '';
|
||||
|
||||
// ── Session + rate limiting ──
|
||||
start_session();
|
||||
$now = time();
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
|
||||
if (!rate_limit($ip, 20, 900)) {
|
||||
json_out(['success' => false, 'message' => 'Too many requests. Please wait a few minutes.'], 429);
|
||||
}
|
||||
|
||||
// ── 24 h session expiry ──
|
||||
if (!empty($_SESSION['user_id'])) {
|
||||
$last_seen = $_SESSION['last_seen'] ?? 0;
|
||||
if ($now - $last_seen > SESSION_TTL) {
|
||||
if (!empty($_SESSION['db_session_id'])) {
|
||||
try {
|
||||
db_connect()->prepare(
|
||||
"UPDATE user_sessions SET logged_out_at = NOW() WHERE id = :sid AND logged_out_at IS NULL"
|
||||
)->execute([':sid' => $_SESSION['db_session_id']]);
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
session_destroy();
|
||||
start_session();
|
||||
json_out(['success' => false, 'message' => 'Session expired. Please log in again.'], 401);
|
||||
}
|
||||
if ($now - $last_seen > 60) {
|
||||
$_SESSION['last_seen'] = $now;
|
||||
if (!empty($_SESSION['db_session_id'])) {
|
||||
try {
|
||||
db_connect()->prepare(
|
||||
"UPDATE user_sessions SET last_seen_at = NOW() WHERE id = :sid"
|
||||
)->execute([':sid' => $_SESSION['db_session_id']]);
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Send OTP
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
if ($action === 'send_otp') {
|
||||
$username = trim($body['username'] ?? '');
|
||||
$email = trim($body['email'] ?? '');
|
||||
if ($username !== '' && !preg_match('/^[a-zA-Z0-9_\-]{3,32}$/', $username)) {
|
||||
json_out(['success' => false, 'message' => 'Username must be 3-32 characters: letters, numbers, _ or -.']);
|
||||
}
|
||||
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
json_out(['success' => false, 'message' => 'A valid email address is required.']);
|
||||
}
|
||||
|
||||
$pdo = db_connect();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT blocked FROM users WHERE username = :u OR email = :e ORDER BY blocked DESC LIMIT 1');
|
||||
$stmt->execute([':u' => $username, ':e' => $email]);
|
||||
$user = $stmt->fetch();
|
||||
if ($user && ($user['blocked'] === true || $user['blocked'] === 't')) {
|
||||
json_out(['success' => false, 'message' => 'This account has been blocked.']);
|
||||
}
|
||||
if ($user) {
|
||||
json_out(['success' => false, 'message' => 'Username or email is already taken.']);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT COUNT(*) FROM email_otps WHERE email = :e AND created_at > NOW() - INTERVAL '24 hours'"
|
||||
);
|
||||
$stmt->execute([':e' => $email]);
|
||||
if ((int)$stmt->fetchColumn() >= OTP_MAX_DAY) {
|
||||
json_out(['success' => false, 'message' => 'Too many OTP requests for this email. Please try again tomorrow.'], 429);
|
||||
}
|
||||
|
||||
$pdo->prepare("DELETE FROM email_otps WHERE email = :e AND used = FALSE")->execute([':e' => $email]);
|
||||
|
||||
$otp = str_pad((string)random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
$otp_hash = password_hash($otp, PASSWORD_BCRYPT, ['cost' => 10]);
|
||||
|
||||
$pdo->prepare(
|
||||
"INSERT INTO email_otps (email, otp_hash, expires_at, used)
|
||||
VALUES (:e, :h, NOW() + INTERVAL '10 minutes', FALSE)"
|
||||
)->execute([':e' => $email, ':h' => $otp_hash]);
|
||||
|
||||
$subject = 'Freedoms4 sign up OTP';
|
||||
$message =
|
||||
"Hello,\n\n" .
|
||||
"Your OTP to create a freedoms4.org account is:\n\n" .
|
||||
"{$otp}\n\n" .
|
||||
"This code expires in 10 minutes. Do not share it with anyone.\n\n" .
|
||||
"If you did not request this, you can safely ignore this email.\n\n" .
|
||||
"freedoms4.org";
|
||||
|
||||
$headers = implode("\r\n", [
|
||||
'From: freedoms4.org <' . OTP_FROM . '>',
|
||||
'Reply-To: ' . OTP_FROM,
|
||||
'Cc: hyzen@freedoms4.org',
|
||||
'X-Mailer: PHP/' . PHP_VERSION,
|
||||
'MIME-Version: 1.0',
|
||||
'Content-Type: text/plain; charset=UTF-8',
|
||||
]);
|
||||
|
||||
if (!mail($email, $subject, $message, $headers)) {
|
||||
error_log("OTP mail() failed for: {$email}");
|
||||
json_out(['success' => false, 'message' => 'Failed to send OTP email. Please try again.'], 500);
|
||||
}
|
||||
|
||||
json_out(['success' => true, 'message' => 'OTP sent. Please check your inbox (and spam folder).']);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Login
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
if ($action === 'login') {
|
||||
$username = trim($body['username'] ?? '');
|
||||
$password = $body['password'] ?? '';
|
||||
|
||||
if ($username === '' || $password === '') {
|
||||
json_out(['success' => false, 'message' => 'Username and password are required.']);
|
||||
}
|
||||
|
||||
$pdo = db_connect();
|
||||
$stmt = $pdo->prepare('SELECT id, username, password_hash, blocked FROM users WHERE username = :u LIMIT 1');
|
||||
$stmt->execute([':u' => $username]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
$hash = $user['password_hash'] ?? '$2y$12$invalidhashpadding000000000000000000000000000000000000000';
|
||||
if (!$user || !password_verify($password, $hash)) {
|
||||
json_out(['success' => false, 'message' => 'Invalid username or password.']);
|
||||
}
|
||||
|
||||
if ($user && ($user['blocked'] === true || $user['blocked'] === 't')) {
|
||||
json_out(['success' => false, 'message' => 'This account has been blocked.']);
|
||||
}
|
||||
|
||||
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['last_seen'] = $now;
|
||||
|
||||
$ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512);
|
||||
$session_hash = hash('sha256', session_id());
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO user_sessions (user_id, session_id, logged_in_at, last_seen_at, ip_address, user_agent)
|
||||
VALUES (:uid, :sid, NOW(), NOW(), :ip, :ua)"
|
||||
);
|
||||
$stmt->execute([':uid' => $user['id'], ':sid' => $session_hash, ':ip' => $ip, ':ua' => $ua]);
|
||||
$_SESSION['db_session_id'] = $pdo->lastInsertId();
|
||||
|
||||
json_out(['success' => true, 'redirect' => '/']);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Signup + XMPP + Email accounts creation
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
if ($action === 'signup') {
|
||||
$username = trim($body['username'] ?? '');
|
||||
$email = trim($body['email'] ?? '');
|
||||
$password = $body['password'] ?? '';
|
||||
$otp = trim($body['otp'] ?? '');
|
||||
$terms_agreed = !empty($body['terms_agreed']);
|
||||
|
||||
if ($username === '') {
|
||||
json_out(['success' => false, 'message' => 'Username is required.']);
|
||||
}
|
||||
if (!preg_match('/^[a-zA-Z0-9_\-]{3,32}$/', $username)) {
|
||||
json_out(['success' => false, 'message' => 'Username must be 3-32 characters: letters, numbers, _ or -.']);
|
||||
}
|
||||
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
json_out(['success' => false, 'message' => 'A valid email address is required.']);
|
||||
}
|
||||
if (strlen($password) < 8) {
|
||||
json_out(['success' => false, 'message' => 'Password must be at least 8 characters.']);
|
||||
}
|
||||
if ($otp === '' || !preg_match('/^\d{6}$/', $otp)) {
|
||||
json_out(['success' => false, 'message' => 'A 6-digit verification code is required.']);
|
||||
}
|
||||
if (!$terms_agreed) {
|
||||
json_out(['success' => false, 'message' => 'You must agree to the terms and conditions.']);
|
||||
}
|
||||
|
||||
// OTP brute-force lockout
|
||||
if (otp_fail_count($ip) >= OTP_MAX_FAILS) {
|
||||
json_out(['success' => false, 'message' => 'Too many failed attempts. Please request a new code.'], 429);
|
||||
}
|
||||
|
||||
$pdo = db_connect();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT blocked FROM users WHERE username = :u OR email = :e ORDER BY blocked DESC LIMIT 1');
|
||||
$stmt->execute([':u' => $username, ':e' => $email]);
|
||||
$user = $stmt->fetch();
|
||||
if ($user && ($user['blocked'] === true || $user['blocked'] === 't')) {
|
||||
json_out(['success' => false, 'message' => 'This account has been blocked.']);
|
||||
}
|
||||
if ($user) {
|
||||
json_out(['success' => false, 'message' => 'Username or email is already taken.']);
|
||||
}
|
||||
|
||||
// Verify OTP
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT id, otp_hash FROM email_otps
|
||||
WHERE email = :e AND used = FALSE AND expires_at > NOW()
|
||||
ORDER BY created_at DESC LIMIT 1"
|
||||
);
|
||||
$stmt->execute([':e' => $email]);
|
||||
$otp_row = $stmt->fetch();
|
||||
|
||||
if (!$otp_row || !password_verify($otp, $otp_row['otp_hash'])) {
|
||||
otp_fail_increment($ip);
|
||||
json_out(['success' => false, 'message' => 'Invalid or expired verification code.']);
|
||||
}
|
||||
|
||||
// Valid OTP — reset failure counter
|
||||
otp_fail_reset($ip);
|
||||
|
||||
// Check uniqueness
|
||||
$stmt = $pdo->prepare('SELECT 1 FROM users WHERE username = :u OR email = :e LIMIT 1');
|
||||
$stmt->execute([':u' => $username, ':e' => $email]);
|
||||
if ($stmt->fetch()) {
|
||||
json_out(['success' => false, 'message' => 'Username or email is already taken.']);
|
||||
}
|
||||
|
||||
// Create freedoms4 account + mark OTP used
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
|
||||
$pdo->prepare(
|
||||
'INSERT INTO users (username, email, password_hash, terms_agreed, created_at) VALUES (:u, :e, :h, :t, NOW())'
|
||||
)->execute([':u' => $username, ':e' => $email, ':h' => $hash, ':t' => $terms_agreed ? 'true' : 'false']);
|
||||
|
||||
$pdo->prepare("UPDATE email_otps SET used = TRUE WHERE id = :id")
|
||||
->execute([':id' => $otp_row['id']]);
|
||||
|
||||
$pdo->commit();
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
error_log('signup transaction failed: ' . $e->getMessage());
|
||||
json_out(['success' => false, 'message' => 'Account creation failed. Please try again.'], 500);
|
||||
}
|
||||
|
||||
// Regenerate session after successful signup (#5)
|
||||
session_regenerate_id(true);
|
||||
|
||||
// Create XMPP account
|
||||
if (!create_xmpp_account($username, $password)) {
|
||||
error_log("XMPP account creation failed for new user: {$username}");
|
||||
// Don't fail the signup — user account exists, XMPP can be fixed manually
|
||||
}
|
||||
|
||||
// ── Create Dovecot virtual email account ──
|
||||
// Sudoers: www-data ALL=(root) NOPASSWD: /usr/local/bin/email-account-create
|
||||
$safe_user = escapeshellarg($username);
|
||||
$safe_pw = escapeshellarg($password);
|
||||
$email_out = shell_exec("sudo /usr/local/bin/email-account-create {$safe_user} {$safe_pw} 2>&1");
|
||||
if (!in_array(trim($email_out ?? ''), ['created', 'exists', 'system-user'])) {
|
||||
error_log("email-account-create failed for {$username}: " . ($email_out ?? 'null'));
|
||||
// Don't fail the signup — email can be fixed manually
|
||||
}
|
||||
|
||||
// Welcome email
|
||||
$welcome_headers = implode("\r\n", [
|
||||
'From: hyzen <hyzen@freedoms4.org>',
|
||||
'Reply-To: hyzen@freedoms4.org',
|
||||
'Cc: hyzen@freedoms4.org',
|
||||
'X-Mailer: PHP/' . PHP_VERSION,
|
||||
'MIME-Version: 1.0',
|
||||
'Content-Type: text/plain; charset=UTF-8',
|
||||
]);
|
||||
|
||||
$welcome_message =
|
||||
"Hi {$username},\n\n" .
|
||||
"Welcome aboard!\n\n" .
|
||||
"Thank you for signing up.\n\n" .
|
||||
"Your credentials:\n\n" .
|
||||
"XMPP JID: {$username}@freedoms4.org\n" .
|
||||
"Email ID: {$username}@freedoms4.org\n\n" .
|
||||
"Passwords: Use the same password that you used during registration.\n\n" .
|
||||
"If you have any questions, I'm here to help:\n" .
|
||||
"Email <mailto:hyzen@freedoms4.org> and XMPP <xmpp:hyzen@freedoms4.org>: hyzen@freedoms4.org\n" .
|
||||
"IRC/Liberachat: hyzen, #freedoms4\n\n" .
|
||||
"Best regards,\n" .
|
||||
"hyzen, freedoms4.org.";
|
||||
|
||||
if (!mail($email, "Welcome to freedoms4.org", $welcome_message, $welcome_headers)) {
|
||||
error_log("Welcome mail() failed for: {$email}");
|
||||
}
|
||||
|
||||
json_out(['success' => true]);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Check session
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
if ($action === 'check_session') {
|
||||
if (empty($_SESSION['user_id'])) {
|
||||
json_out(['valid' => false]);
|
||||
}
|
||||
|
||||
// Verify user still exists in DB
|
||||
try {
|
||||
$pdo = db_connect();
|
||||
$stmt = $pdo->prepare('SELECT 1 FROM users WHERE id = :id LIMIT 1');
|
||||
$stmt->execute([':id' => $_SESSION['user_id']]);
|
||||
if (!$stmt->fetch()) {
|
||||
// User deleted — destroy session
|
||||
$_SESSION = [];
|
||||
session_destroy();
|
||||
json_out(['valid' => false]);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// DB unavailable — don't force logout, just report invalid so frontend can retry
|
||||
json_out(['valid' => false, 'db_error' => true]);
|
||||
}
|
||||
|
||||
json_out(['valid' => true, 'username' => $_SESSION['username']]);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Logout
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Logout
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
if ($action === 'logout') {
|
||||
if (!empty($_SESSION['db_session_id'])) {
|
||||
try {
|
||||
db_connect()->prepare(
|
||||
"UPDATE user_sessions SET logged_out_at = NOW() WHERE id = :sid AND logged_out_at IS NULL"
|
||||
)->execute([':sid' => $_SESSION['db_session_id']]);
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
|
||||
$_SESSION = [];
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$p = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000,
|
||||
$p['path'], $p['domain'], $p['secure'], $p['httponly']);
|
||||
}
|
||||
session_destroy();
|
||||
json_out(['success' => true, 'redirect' => '/']);
|
||||
}
|
||||
|
||||
json_out(['success' => false, 'message' => 'Unknown action.'], 400);
|
||||
58
backend.freedoms4.org
Normal file
58
backend.freedoms4.org
Normal file
@@ -0,0 +1,58 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name backend.freedoms4.org;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/html;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name backend.freedoms4.org;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/backend.freedoms4.org/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/backend.freedoms4.org/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
root /var/www/freedoms4/api;
|
||||
|
||||
location = /auth.php {
|
||||
limit_except POST OPTIONS {
|
||||
deny all;
|
||||
}
|
||||
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
|
||||
location = /comments.php {
|
||||
limit_except GET POST OPTIONS {
|
||||
deny all;
|
||||
}
|
||||
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
|
||||
location = /admin.php {
|
||||
limit_except GET POST OPTIONS {
|
||||
deny all;
|
||||
}
|
||||
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
|
||||
|
||||
location / {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
378
comments.php
Normal file
378
comments.php
Normal file
@@ -0,0 +1,378 @@
|
||||
<?php
|
||||
/**
|
||||
* comments.php — Blog comment backend for freedoms4.org
|
||||
*
|
||||
* Actions:
|
||||
* GET ?action=get&post_id=... — fetch comments for a post
|
||||
* POST { action: "post", post_id, body } — add a top-level comment
|
||||
* POST { action: "reply", post_id, parent_id, body } — reply to a comment
|
||||
* POST { action: "delete", comment_id } — delete own comment
|
||||
*/
|
||||
|
||||
// ── Credentials from env file ──
|
||||
$env_file = '/etc/freedoms4/auth.env';
|
||||
if (!is_readable($env_file)) {
|
||||
http_response_code(503);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['success' => false, 'message' => 'Server configuration error.']);
|
||||
exit;
|
||||
}
|
||||
$env = [];
|
||||
foreach (file($env_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) continue;
|
||||
[$k, $v] = explode('=', $line, 2);
|
||||
$env[trim($k)] = trim($v);
|
||||
}
|
||||
|
||||
define('DB_HOST', $env['DB_HOST'] ?? '127.0.0.1');
|
||||
define('DB_PORT', $env['DB_PORT'] ?? '5432');
|
||||
define('DB_NAME', $env['DB_NAME'] ?? 'freedoms4');
|
||||
define('DB_USER', $env['DB_USER'] ?? 'freedoms4_user');
|
||||
define('DB_PASS', $env['DB_PASS'] ?? '');
|
||||
|
||||
define('SESSION_NAME', 'f4_session');
|
||||
define('SESSION_SECURE', true);
|
||||
define('SESSION_SAMESITE', 'None');
|
||||
define('MAX_BODY_BYTES', 8192);
|
||||
define('MAX_COMMENT_LEN', 2000);
|
||||
|
||||
// ── CORS ──
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
$allowed_origins = ['https://freedoms4.org', 'https://www.freedoms4.org'];
|
||||
|
||||
if ($origin && !in_array($origin, $allowed_origins, true)) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['success' => false, 'message' => 'Forbidden.']);
|
||||
exit;
|
||||
}
|
||||
if ($origin) {
|
||||
header('Access-Control-Allow-Origin: ' . $origin);
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
}
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
function json_out(array $data, int $status = 200): never {
|
||||
http_response_code($status);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
function db_connect(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo !== null) return $pdo;
|
||||
$dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', DB_HOST, DB_PORT, DB_NAME);
|
||||
try {
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
error_log('comments.php DB error: ' . $e->getMessage());
|
||||
json_out(['success' => false, 'message' => 'Database unavailable.'], 503);
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
function start_session(): void {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_name(SESSION_NAME);
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'secure' => SESSION_SECURE,
|
||||
'httponly' => true,
|
||||
'samesite' => SESSION_SAMESITE,
|
||||
]);
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send comment notification emails.
|
||||
*
|
||||
* $type 'new_comment' | 'new_reply'
|
||||
* $actor username of the person who wrote the comment/reply
|
||||
* $body the comment/reply text
|
||||
* $post_id the post slug/path (used as human-readable context)
|
||||
* $notify_user ['username' => ..., 'email' => ...] | null — commenter being replied to
|
||||
*/
|
||||
function send_notification(string $type, string $actor, string $body, string $post_id, ?array $notify_user): void {
|
||||
$from = 'no-reply@freedoms4.org';
|
||||
$headers = implode("\r\n", [
|
||||
'From: freedoms4.org <' . $from . '>',
|
||||
'Reply-To: ' . $from,
|
||||
'X-Mailer: PHP/' . PHP_VERSION,
|
||||
'MIME-Version: 1.0',
|
||||
'Content-Type: text/plain; charset=UTF-8',
|
||||
]);
|
||||
|
||||
$post_url = 'https://freedoms4.org' . trim($post_id, '"\'');
|
||||
|
||||
if ($type === 'new_reply') {
|
||||
// ── Reply notification ──
|
||||
// Always notify hyzen (unless hyzen is the one replying)
|
||||
$reply_subject = "You have a new reply from {$actor}";
|
||||
$reply_body =
|
||||
"You have a new reply from {$actor}:\n\n" .
|
||||
"{$body}\n\n" .
|
||||
"Post: {$post_url}\n\n" .
|
||||
"freedoms4.org";
|
||||
|
||||
// Notify the commenter being replied to (their registered email + @freedoms4.org),
|
||||
// unless they are hyzen (handled separately below)
|
||||
if ($notify_user && $notify_user['username'] !== 'hyzen') {
|
||||
// Send to registered email
|
||||
if (!empty($notify_user['email'])) {
|
||||
@mail($notify_user['email'], $reply_subject, $reply_body, $headers);
|
||||
}
|
||||
// Send to their @freedoms4.org address
|
||||
$site_email = $notify_user['username'] . '@freedoms4.org';
|
||||
@mail($site_email, $reply_subject, $reply_body, $headers);
|
||||
}
|
||||
|
||||
// Notify hyzen for all replies (unless hyzen is the replier)
|
||||
if ($actor !== 'hyzen') {
|
||||
// If hyzen is being replied to, use the reply subject; otherwise use new-comment subject
|
||||
if ($notify_user && $notify_user['username'] === 'hyzen') {
|
||||
@mail('hyzen@freedoms4.org', $reply_subject, $reply_body, $headers);
|
||||
} else {
|
||||
// hyzen gets a "new reply" notice even when it's not on their own comment
|
||||
$hyzen_subject = "A new comment from {$actor}";
|
||||
$hyzen_body =
|
||||
"A new comment from {$actor}:\n\n" .
|
||||
"{$body}\n\n" .
|
||||
"Post: {$post_url}\n\n" .
|
||||
"freedoms4.org";
|
||||
@mail('hyzen@freedoms4.org', $hyzen_subject, $hyzen_body, $headers);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// ── New top-level comment notification (hyzen only) ──
|
||||
if ($actor === 'hyzen') return; // hyzen commenting on their own site — skip
|
||||
$subject = "A new comment from {$actor}";
|
||||
$msg =
|
||||
"A new comment from {$actor}:\n\n" .
|
||||
"{$body}\n\n" .
|
||||
"Post: {$post_url}\n\n" .
|
||||
"freedoms4.org";
|
||||
@mail('hyzen@freedoms4.org', $subject, $msg, $headers);
|
||||
}
|
||||
}
|
||||
|
||||
function logged_in_user(): ?array {
|
||||
if (empty($_SESSION['user_id']) || empty($_SESSION['username'])) return null;
|
||||
// Verify the user still exists in the DB (handles deleted accounts / wiped DB)
|
||||
try {
|
||||
$pdo = db_connect();
|
||||
$stmt = $pdo->prepare('SELECT blocked FROM users WHERE id = :id LIMIT 1');
|
||||
$stmt->execute([':id' => (int)$_SESSION['user_id']]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) {
|
||||
$_SESSION = [];
|
||||
session_destroy();
|
||||
return null;
|
||||
}
|
||||
if ($row['blocked'] === true || $row['blocked'] === 't') {
|
||||
return null;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// DB unavailable — treat as logged-out to be safe
|
||||
return null;
|
||||
}
|
||||
return ['id' => (int)$_SESSION['user_id'], 'username' => $_SESSION['username']];
|
||||
}
|
||||
|
||||
// ── GET: fetch comments ──
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$post_id = trim($_GET['post_id'] ?? '');
|
||||
if ($post_id === '') {
|
||||
json_out(['success' => false, 'message' => 'post_id is required.'], 400);
|
||||
}
|
||||
|
||||
start_session();
|
||||
$viewer = logged_in_user();
|
||||
$is_admin = $viewer && $viewer['username'] === 'hyzen';
|
||||
|
||||
$pdo = db_connect();
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT c.id, c.post_id, c.parent_id, c.user_id, COALESCE(u.username, c.username, '[deleted user]') AS username,
|
||||
c.body, c.created_at, c.deleted, c.deleted_by
|
||||
FROM comments c
|
||||
LEFT JOIN users u ON u.id = c.user_id
|
||||
WHERE c.post_id = :pid
|
||||
ORDER BY c.created_at ASC"
|
||||
);
|
||||
$stmt->execute([':pid' => $post_id]);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
// Build tree: top-level comments with nested replies
|
||||
$top = [];
|
||||
$index = [];
|
||||
foreach ($rows as $row) {
|
||||
if ($row['deleted']) {
|
||||
$row['body'] = null;
|
||||
$row['deleted_label'] = $row['deleted_by'] === 'admin' ? 'deleted by admin' : 'deleted by user';
|
||||
} else {
|
||||
$row['deleted_label'] = null;
|
||||
}
|
||||
$row['replies'] = [];
|
||||
$row['is_own'] = $viewer && (int)$row['user_id'] === $viewer['id'];
|
||||
$index[$row['id']] = $row;
|
||||
}
|
||||
foreach ($index as $id => &$node) {
|
||||
if ($node['parent_id'] === null) {
|
||||
$top[$id] = &$node;
|
||||
} else {
|
||||
$index[$node['parent_id']]['replies'][$id] = &$node;
|
||||
}
|
||||
}
|
||||
unset($node);
|
||||
|
||||
json_out(['success' => true, 'comments' => array_values($top), 'logged_in' => $viewer !== null, 'username' => $viewer['username'] ?? null, 'is_admin' => $is_admin]);
|
||||
}
|
||||
|
||||
// ── POST actions ──
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
json_out(['success' => false, 'message' => 'Method not allowed.'], 405);
|
||||
}
|
||||
|
||||
$content_length = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
|
||||
if ($content_length > MAX_BODY_BYTES) {
|
||||
json_out(['success' => false, 'message' => 'Request too large.'], 413);
|
||||
}
|
||||
$raw = fread(fopen('php://input', 'r'), MAX_BODY_BYTES + 1);
|
||||
$body = json_decode($raw, true);
|
||||
if (!is_array($body)) {
|
||||
json_out(['success' => false, 'message' => 'Invalid request body.'], 400);
|
||||
}
|
||||
|
||||
start_session();
|
||||
$user = logged_in_user();
|
||||
if (!$user) {
|
||||
json_out(['success' => false, 'message' => 'You must be logged in to comment.'], 401);
|
||||
}
|
||||
|
||||
$action = $body['action'] ?? '';
|
||||
|
||||
// ── POST: add comment or reply ──
|
||||
if ($action === 'post' || $action === 'reply') {
|
||||
$post_id = trim($body['post_id'] ?? '');
|
||||
$text = trim($body['body'] ?? '');
|
||||
$parent_id = isset($body['parent_id']) ? (int)$body['parent_id'] : null;
|
||||
|
||||
if ($post_id === '') {
|
||||
json_out(['success' => false, 'message' => 'post_id is required.']);
|
||||
}
|
||||
if ($text === '') {
|
||||
json_out(['success' => false, 'message' => 'Comment cannot be empty.']);
|
||||
}
|
||||
if (strlen($text) > MAX_COMMENT_LEN) {
|
||||
json_out(['success' => false, 'message' => 'Comment is too long (max 2000 characters).']);
|
||||
}
|
||||
|
||||
$pdo = db_connect();
|
||||
|
||||
// Rate limit: max 1 comment per user per minute (hyzen is exempt)
|
||||
if ($user['username'] !== 'hyzen') {
|
||||
$rate_stmt = $pdo->prepare(
|
||||
"SELECT COUNT(*) FROM comments
|
||||
WHERE user_id = :uid AND created_at > NOW() - INTERVAL '1 minute'"
|
||||
);
|
||||
$rate_stmt->execute([':uid' => $user['id']]);
|
||||
if ((int)$rate_stmt->fetchColumn() >= 1) {
|
||||
json_out(['success' => false, 'message' => 'You are posting too fast. Please wait a moment.'], 429);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate parent exists and belongs to same post
|
||||
if ($parent_id !== null) {
|
||||
$stmt = $pdo->prepare("SELECT id FROM comments WHERE id = :pid AND post_id = :post AND deleted = FALSE LIMIT 1");
|
||||
$stmt->execute([':pid' => $parent_id, ':post' => $post_id]);
|
||||
if (!$stmt->fetch()) {
|
||||
json_out(['success' => false, 'message' => 'Parent comment not found.'], 404);
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO comments (post_id, parent_id, user_id, username, body, created_at, deleted)
|
||||
VALUES (:post, :parent, :uid, :username, :body, NOW(), FALSE)
|
||||
RETURNING id, created_at"
|
||||
);
|
||||
$stmt->execute([
|
||||
':post' => $post_id,
|
||||
':parent' => $parent_id,
|
||||
':uid' => $user['id'],
|
||||
':username' => $user['username'],
|
||||
':body' => $text,
|
||||
]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
// ── Email notifications ──
|
||||
if ($parent_id !== null) {
|
||||
// It's a reply — find the parent comment's author for notification
|
||||
$parent_stmt = $pdo->prepare(
|
||||
"SELECT c.user_id, u.username, u.email
|
||||
FROM comments c JOIN users u ON u.id = c.user_id
|
||||
WHERE c.id = :pid LIMIT 1"
|
||||
);
|
||||
$parent_stmt->execute([':pid' => $parent_id]);
|
||||
$parent_author = $parent_stmt->fetch() ?: null;
|
||||
send_notification('new_reply', $user['username'], $text, $post_id, $parent_author);
|
||||
} else {
|
||||
// Top-level comment
|
||||
send_notification('new_comment', $user['username'], $text, $post_id, null);
|
||||
}
|
||||
|
||||
json_out(['success' => true, 'id' => $row['id'], 'created_at' => $row['created_at']]);
|
||||
}
|
||||
|
||||
// ── POST: delete comment (own) or any comment (admin) ──
|
||||
if ($action === 'delete') {
|
||||
$comment_id = (int)($body['comment_id'] ?? 0);
|
||||
if ($comment_id === 0) {
|
||||
json_out(['success' => false, 'message' => 'comment_id is required.']);
|
||||
}
|
||||
|
||||
$is_admin = $user['username'] === 'hyzen';
|
||||
|
||||
$pdo = db_connect();
|
||||
|
||||
// Check if the comment belongs to the deleter
|
||||
$owner_stmt = $pdo->prepare("SELECT user_id FROM comments WHERE id = :id LIMIT 1");
|
||||
$owner_stmt->execute([':id' => $comment_id]);
|
||||
$comment_row = $owner_stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$is_own = $comment_row && (int)$comment_row['user_id'] === (int)$user['id'];
|
||||
|
||||
$deleted_by = $is_own ? 'user' : 'admin';
|
||||
|
||||
if ($is_admin) {
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE comments SET deleted = TRUE, body = NULL, deleted_by = :by
|
||||
WHERE id = :id AND deleted = FALSE"
|
||||
);
|
||||
$stmt->execute([':id' => $comment_id, ':by' => $deleted_by]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE comments SET deleted = TRUE, body = NULL, deleted_by = :by
|
||||
WHERE id = :id AND user_id = :uid AND deleted = FALSE"
|
||||
);
|
||||
$stmt->execute([':id' => $comment_id, ':uid' => $user['id'], ':by' => $deleted_by]);
|
||||
}
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
json_out(['success' => false, 'message' => 'Comment not found or not yours.'], 403);
|
||||
}
|
||||
json_out(['success' => true]);
|
||||
}
|
||||
|
||||
json_out(['success' => false, 'message' => 'Unknown action.'], 400);
|
||||
58
email-account-create.sh
Executable file
58
email-account-create.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
# /usr/local/bin/email-account-create
|
||||
# Called by www-data via sudo to add a virtual mail user to /var/dovecot/users
|
||||
# Usage: sudo /usr/local/bin/email-account-create <username> <password>
|
||||
|
||||
set -e
|
||||
|
||||
USERNAME="$1"
|
||||
PASSWORD="$2"
|
||||
USERS_FILE="/var/dovecot/users"
|
||||
VMAIL_UID=$(id -u vmail)
|
||||
VMAIL_GID=$(id -g vmail)
|
||||
MAILDIR="/var/vmail/${USERNAME}@freedoms4.org/maildir"
|
||||
|
||||
if [[ -z "$USERNAME" || -z "$PASSWORD" ]]; then
|
||||
echo "Usage: $0 <username> <password>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate username
|
||||
if ! [[ "$USERNAME" =~ ^[a-zA-Z0-9_-]{1,32}$ ]]; then
|
||||
echo "Invalid username" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Check if already exists in passwd-file
|
||||
if grep -q "^${USERNAME}:" "${USERS_FILE}" 2>/dev/null; then
|
||||
echo "exists"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Skip if this is an existing system user — they have their own mailbox
|
||||
if id "${USERNAME}" &>/dev/null; then
|
||||
echo "system-user"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Hash the password using SHA512-CRYPT (Dovecot compatible)
|
||||
HASHED=$(doveadm pw -s SHA512-CRYPT -p "$PASSWORD")
|
||||
|
||||
# Append to users file
|
||||
echo "${USERNAME}:${HASHED}:${VMAIL_UID}:${VMAIL_GID}::${MAILDIR}::" >> "${USERS_FILE}"
|
||||
|
||||
# Create maildir structure
|
||||
mkdir -p "${MAILDIR}"
|
||||
chown -R vmail:mail "/var/vmail/${USERNAME}@freedoms4.org"
|
||||
chmod -R 700 "/var/vmail/${USERNAME}@freedoms4.org"
|
||||
|
||||
# Register this user in the per-user transport map so Postfix routes
|
||||
# inbound mail to the Dovecot LDA (system users are not in this map
|
||||
# and continue to receive via normal local delivery).
|
||||
VTRANSPORT_FILE="/etc/postfix/virtual_transport"
|
||||
if ! grep -q "^${USERNAME}@freedoms4.org" "${VTRANSPORT_FILE}" 2>/dev/null; then
|
||||
echo "${USERNAME}@freedoms4.org dovecot" >> "${VTRANSPORT_FILE}"
|
||||
postmap "${VTRANSPORT_FILE}"
|
||||
fi
|
||||
|
||||
echo "created"
|
||||
72
email-block.sh
Executable file
72
email-block.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# /usr/local/bin/email-block
|
||||
# Block or unblock a virtual mail user by moving their entry in/out of the users file.
|
||||
# Usage: sudo /usr/local/bin/email-block <block|unblock> <username>
|
||||
|
||||
ACTION="$1"
|
||||
USERNAME="$2"
|
||||
USERS_FILE="/var/dovecot/users"
|
||||
BACKUP_FILE="/var/dovecot/users.blocked"
|
||||
MAIL_ROOT="/var/vmail/${USERNAME}@freedoms4.org"
|
||||
VTRANSPORT_FILE="/etc/postfix/virtual_transport"
|
||||
ADDRESS="${USERNAME}@freedoms4.org"
|
||||
|
||||
if [[ -z "$ACTION" || -z "$USERNAME" ]]; then
|
||||
echo "Usage: $0 <block|unblock> <username>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ "$USERNAME" =~ ^[a-zA-Z0-9_-]{1,32}$ ]]; then
|
||||
echo "Invalid username" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Skip system users — they authenticate via PAM, not passwd-file
|
||||
if id "${USERNAME}" &>/dev/null; then
|
||||
echo "system-user"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$ACTION" == "block" ]]; then
|
||||
# Check if entry exists in users file
|
||||
if ! grep -q "^${USERNAME}:" "${USERS_FILE}" 2>/dev/null; then
|
||||
echo "not-found"
|
||||
exit 0
|
||||
fi
|
||||
# Move entry to backup file
|
||||
grep "^${USERNAME}:" "${USERS_FILE}" >> "${BACKUP_FILE}"
|
||||
sed -i "/^${USERNAME}:/d" "${USERS_FILE}"
|
||||
# Remove Postfix transport entry so new mail is no longer delivered
|
||||
if [[ -f "${VTRANSPORT_FILE}" ]]; then
|
||||
sed -i "/^${ADDRESS}[[:space:]]/d" "${VTRANSPORT_FILE}"
|
||||
postmap "${VTRANSPORT_FILE}" 2>/dev/null || true
|
||||
fi
|
||||
# Delete existing messages while keeping mailbox folders intact
|
||||
if [[ -d "${MAIL_ROOT}" ]]; then
|
||||
find "${MAIL_ROOT}" -type d \( -name cur -o -name new -o -name tmp \) -print0 |
|
||||
while IFS= read -r -d '' maildir_part; do
|
||||
find "${maildir_part}" -mindepth 1 -maxdepth 1 -type f -delete
|
||||
done
|
||||
fi
|
||||
echo "blocked"
|
||||
|
||||
elif [[ "$ACTION" == "unblock" ]]; then
|
||||
# Check if entry exists in backup file
|
||||
if ! grep -q "^${USERNAME}:" "${BACKUP_FILE}" 2>/dev/null; then
|
||||
echo "not-found"
|
||||
exit 0
|
||||
fi
|
||||
# Restore entry to users file
|
||||
grep "^${USERNAME}:" "${BACKUP_FILE}" >> "${USERS_FILE}"
|
||||
sed -i "/^${USERNAME}:/d" "${BACKUP_FILE}"
|
||||
# Restore Postfix transport entry so new mail is delivered again
|
||||
if [[ -f "${VTRANSPORT_FILE}" ]] && ! grep -q "^${ADDRESS}[[:space:]]" "${VTRANSPORT_FILE}" 2>/dev/null; then
|
||||
echo "${ADDRESS} dovecot" >> "${VTRANSPORT_FILE}"
|
||||
postmap "${VTRANSPORT_FILE}" 2>/dev/null || true
|
||||
fi
|
||||
echo "unblocked"
|
||||
|
||||
else
|
||||
echo "Invalid action: $ACTION" >&2
|
||||
exit 3
|
||||
fi
|
||||
51
email-delete.sh
Executable file
51
email-delete.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
# /usr/local/bin/email-delete
|
||||
# Permanently delete a virtual mail user and their mailbox.
|
||||
# Usage: sudo /usr/local/bin/email-delete <username>
|
||||
|
||||
set -e
|
||||
|
||||
USERNAME="$1"
|
||||
USERS_FILE="/var/dovecot/users"
|
||||
BACKUP_FILE="/var/dovecot/users.blocked"
|
||||
MAIL_ROOT="/var/vmail/${USERNAME}@freedoms4.org"
|
||||
VTRANSPORT_FILE="/etc/postfix/virtual_transport"
|
||||
|
||||
if [[ -z "$USERNAME" ]]; then
|
||||
echo "Usage: $0 <username>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ "$USERNAME" =~ ^[a-zA-Z0-9_-]{1,32}$ ]]; then
|
||||
echo "Invalid username" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Skip system users — they authenticate via PAM, not passwd-file
|
||||
if id "${USERNAME}" &>/dev/null; then
|
||||
echo "system-user"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Remove active/blocked Dovecot passwd-file entries
|
||||
sed -i "/^${USERNAME}:/d" "${USERS_FILE}" 2>/dev/null || true
|
||||
sed -i "/^${USERNAME}:/d" "${BACKUP_FILE}" 2>/dev/null || true
|
||||
|
||||
# Remove per-user Postfix transport route
|
||||
if [[ -f "${VTRANSPORT_FILE}" ]]; then
|
||||
sed -i "/^${USERNAME}@freedoms4\.org[[:space:]]/d" "${VTRANSPORT_FILE}"
|
||||
postmap "${VTRANSPORT_FILE}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Delete existing messages using the same Maildir cleanup as the block action
|
||||
if [[ -d "${MAIL_ROOT}" ]]; then
|
||||
find "${MAIL_ROOT}" -type d \( -name cur -o -name new -o -name tmp \) -print0 |
|
||||
while IFS= read -r -d '' maildir_part; do
|
||||
find "${maildir_part}" -mindepth 1 -maxdepth 1 -type f -delete
|
||||
done
|
||||
fi
|
||||
|
||||
# Permanently remove the virtual mailbox directory
|
||||
rm -rf "${MAIL_ROOT}"
|
||||
|
||||
echo "deleted"
|
||||
587
full-setup.sh
Executable file
587
full-setup.sh
Executable file
@@ -0,0 +1,587 @@
|
||||
#!/bin/bash
|
||||
# full-setup.sh — freedoms4 backend automation script
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
||||
|
||||
# Config (edit these before running)
|
||||
DB_NAME=""
|
||||
DB_USER=""
|
||||
DB_PASS=""
|
||||
PROSODY_DB_USER=""
|
||||
PROSODY_DB_PASS="" # must match /etc/prosody/prosody.cfg.lua
|
||||
DOMAIN=""
|
||||
CERTBOT_EMAIL=""
|
||||
API_DIR=""
|
||||
ENV_FILE=""
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
OTP_FROM="" # for example: no-reply@freedoms4.org
|
||||
|
||||
# Must run as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
error "Please run as root: sudo bash full-setup.sh"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}================================================${NC}"
|
||||
echo -e "${BLUE} freedoms4 backend setup${NC}"
|
||||
echo -e "${BLUE}================================================${NC}"
|
||||
echo ""
|
||||
|
||||
# ── STEP 1 PostgreSQL ──
|
||||
info "Checking PostgreSQL..."
|
||||
|
||||
if command -v psql &>/dev/null; then
|
||||
success "PostgreSQL is already installed."
|
||||
else
|
||||
info "Installing PostgreSQL..."
|
||||
apt update -qq
|
||||
apt install -y postgresql postgresql-contrib
|
||||
systemctl enable --now postgresql
|
||||
success "PostgreSQL installed and started."
|
||||
fi
|
||||
|
||||
if ! systemctl is-active --quiet postgresql; then
|
||||
systemctl start postgresql
|
||||
fi
|
||||
success "PostgreSQL is running."
|
||||
|
||||
# ── STEP 2 Database & user ──
|
||||
info "Setting up database and user..."
|
||||
|
||||
if (cd /tmp && sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" | grep -q 1); then
|
||||
warn "DB user '${DB_USER}' already exists — resetting password to ensure it matches."
|
||||
(cd /tmp && sudo -u postgres psql -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASS}';")
|
||||
else
|
||||
(cd /tmp && sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';")
|
||||
success "Created DB user '${DB_USER}'."
|
||||
fi
|
||||
|
||||
if (cd /tmp && sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" | grep -q 1); then
|
||||
warn "Database '${DB_NAME}' already exists, skipping."
|
||||
else
|
||||
(cd /tmp && sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};")
|
||||
success "Created database '${DB_NAME}'."
|
||||
fi
|
||||
|
||||
(cd /tmp && sudo -u postgres psql -d "${DB_NAME}") << SQL
|
||||
-- Users table
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(32) NOT NULL UNIQUE,
|
||||
email VARCHAR(254) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
blocked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
terms_agreed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Migration: add terms_agreed for existing installs
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS terms_agreed BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users (username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
|
||||
|
||||
REVOKE ALL ON TABLE users FROM PUBLIC;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE users TO ${DB_USER};
|
||||
GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO ${DB_USER};
|
||||
|
||||
-- OTP table for email verification during sign-up
|
||||
CREATE TABLE IF NOT EXISTS email_otps (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
email VARCHAR(254) NOT NULL,
|
||||
otp_hash VARCHAR(255) NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
used BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
otp_token VARCHAR(64),
|
||||
token_expires_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_email_otps_email ON email_otps (email);
|
||||
CREATE INDEX IF NOT EXISTS idx_email_otps_token ON email_otps (otp_token);
|
||||
|
||||
REVOKE ALL ON TABLE email_otps FROM PUBLIC;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE email_otps TO ${DB_USER};
|
||||
GRANT USAGE, SELECT ON SEQUENCE email_otps_id_seq TO ${DB_USER};
|
||||
|
||||
-- Session tracking table
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
session_id VARCHAR(128) NOT NULL UNIQUE,
|
||||
logged_in_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
logged_out_at TIMESTAMPTZ,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent VARCHAR(512)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_session_id ON user_sessions (session_id);
|
||||
|
||||
REVOKE ALL ON TABLE user_sessions FROM PUBLIC;
|
||||
GRANT SELECT, INSERT, UPDATE ON TABLE user_sessions TO ${DB_USER};
|
||||
GRANT USAGE, SELECT ON SEQUENCE user_sessions_id_seq TO ${DB_USER};
|
||||
|
||||
-- Comments table
|
||||
CREATE TABLE IF NOT EXISTS comments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
post_id VARCHAR(512) NOT NULL,
|
||||
parent_id BIGINT REFERENCES comments(id) ON DELETE SET NULL,
|
||||
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
|
||||
username VARCHAR(32),
|
||||
body TEXT,
|
||||
deleted_by VARCHAR(16),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
ALTER TABLE comments ADD COLUMN IF NOT EXISTS username VARCHAR(32);
|
||||
UPDATE comments c
|
||||
SET username = u.username
|
||||
FROM users u
|
||||
WHERE c.user_id = u.id AND c.username IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_comments_post_id ON comments (post_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_comments_parent_id ON comments (parent_id);
|
||||
|
||||
REVOKE ALL ON TABLE comments FROM PUBLIC;
|
||||
GRANT SELECT, INSERT, UPDATE ON TABLE comments TO ${DB_USER};
|
||||
GRANT USAGE, SELECT ON SEQUENCE comments_id_seq TO ${DB_USER};
|
||||
SQL
|
||||
|
||||
|
||||
# ── STEP 2b Grant freedoms4_user access to Prosody DB ──
|
||||
info "Granting freedoms4_user access to prosody database..."
|
||||
|
||||
(cd /tmp && sudo -u postgres psql -d prosody -c "GRANT CONNECT ON DATABASE prosody TO ${DB_USER};")
|
||||
(cd /tmp && sudo -u postgres psql -d prosody -c "GRANT SELECT, INSERT, UPDATE ON TABLE prosody TO ${DB_USER};")
|
||||
success "freedoms4_user can read/write prosody table."
|
||||
|
||||
|
||||
# ── STEP 3 Block port 5432 via ufw ──
|
||||
info "Blocking port 5432 externally via ufw..."
|
||||
|
||||
if command -v ufw &>/dev/null; then
|
||||
if ufw status | head -1 | grep -q "active"; then
|
||||
ufw deny 5432/tcp
|
||||
success "ufw: port 5432 blocked — PostgreSQL is localhost-only."
|
||||
else
|
||||
ufw --force enable
|
||||
ufw deny 5432/tcp
|
||||
success "ufw enabled and port 5432 blocked."
|
||||
fi
|
||||
else
|
||||
warn "ufw not found — please manually ensure port 5432 is not publicly exposed."
|
||||
fi
|
||||
|
||||
# ── STEP 4 PHP 8.2 fpm + pgsql ──
|
||||
info "Installing PHP 8.2 fpm and pgsql extension..."
|
||||
|
||||
apt install -y php8.2-fpm php8.2-pgsql php8.2-apcu
|
||||
|
||||
phpenmod -v 8.2 pgsql 2>/dev/null || true
|
||||
phpenmod -v 8.2 pdo_pgsql 2>/dev/null || true
|
||||
phpenmod -v 8.2 apcu 2>/dev/null || true
|
||||
|
||||
systemctl enable --now php8.2-fpm
|
||||
systemctl restart php8.2-fpm
|
||||
|
||||
if [[ ! -S /run/php/php8.2-fpm.sock ]]; then
|
||||
error "fpm socket not found at /run/php/php8.2-fpm.sock"
|
||||
fi
|
||||
success "php8.2-fpm ready."
|
||||
|
||||
# ── STEP 5 Mail server check ──
|
||||
info "Checking mail server (Postfix) for OTP delivery..."
|
||||
|
||||
if ! command -v postfix &>/dev/null; then
|
||||
warn "Postfix binary not found. Installing..."
|
||||
apt install -y postfix
|
||||
# Ensure it is configured as 'Internet Site' for freedoms4.org
|
||||
debconf-set-selections <<< "postfix postfix/main_mailer_type select Internet Site"
|
||||
debconf-set-selections <<< "postfix postfix/mailname string freedoms4.org"
|
||||
dpkg-reconfigure -f noninteractive postfix
|
||||
success "Postfix installed."
|
||||
fi
|
||||
|
||||
if ! systemctl is-active --quiet postfix; then
|
||||
systemctl start postfix
|
||||
success "Postfix started."
|
||||
else
|
||||
success "Postfix is running."
|
||||
fi
|
||||
|
||||
# Ensure PHP's sendmail_path points to Postfix's sendmail binary
|
||||
PHP_INI_FPM="/etc/php/8.2/fpm/php.ini"
|
||||
SENDMAIL_PATH=$(php8.2 -r "echo ini_get('sendmail_path');" 2>/dev/null || true)
|
||||
if [[ "$SENDMAIL_PATH" != *"sendmail"* ]]; then
|
||||
warn "sendmail_path may not be set — adding to ${PHP_INI_FPM}"
|
||||
if ! grep -q "^sendmail_path" "${PHP_INI_FPM}" 2>/dev/null; then
|
||||
echo 'sendmail_path = "/usr/sbin/sendmail -t -i"' >> "${PHP_INI_FPM}"
|
||||
systemctl restart php8.2-fpm
|
||||
success "sendmail_path set and php8.2-fpm restarted."
|
||||
fi
|
||||
else
|
||||
success "sendmail_path is configured: ${SENDMAIL_PATH}"
|
||||
fi
|
||||
|
||||
# Configure Postfix myhostname / myorigin if not already set
|
||||
POSTFIX_MAIN="/etc/postfix/main.cf"
|
||||
if ! grep -q "^myhostname\s*=\s*freedoms4.org" "${POSTFIX_MAIN}" 2>/dev/null; then
|
||||
info "Setting Postfix myhostname = freedoms4.org ..."
|
||||
postconf -e "myhostname = freedoms4.org"
|
||||
postconf -e "myorigin = freedoms4.org"
|
||||
systemctl reload postfix
|
||||
success "Postfix myhostname/myorigin updated."
|
||||
fi
|
||||
|
||||
# ── Configure Postfix to use Dovecot SASL for SMTP AUTH (so virtual users can send) ──
|
||||
info "Configuring Postfix to use Dovecot SASL for SMTP submission..."
|
||||
|
||||
# Install libsasl2 if needed
|
||||
apt install -y libsasl2-modules 2>/dev/null || true
|
||||
|
||||
postconf -e "smtpd_sasl_type = dovecot"
|
||||
postconf -e "smtpd_sasl_path = private/auth"
|
||||
postconf -e "smtpd_sasl_auth_enable = yes"
|
||||
postconf -e "smtpd_sasl_security_options = noanonymous"
|
||||
postconf -e "smtpd_sasl_local_domain = \$myhostname"
|
||||
postconf -e "broken_sasl_auth_clients = yes"
|
||||
postconf -e "smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination"
|
||||
|
||||
# ── Configure Postfix virtual mailbox delivery (Dovecot LDA) ──
|
||||
info "Configuring Postfix virtual mailbox delivery via transport_maps..."
|
||||
|
||||
# freedoms4.org stays in mydestination so system users (hyzen etc.) keep
|
||||
# working via local delivery. For site-created virtual users, we use
|
||||
# transport_maps on a per-address basis to route them to Dovecot LDA.
|
||||
#
|
||||
# The critical piece: by default, Postfix checks local_recipient_maps for
|
||||
# any domain in mydestination and rejects unknown recipients before ever
|
||||
# consulting transport_maps. We override local_recipient_maps to only list
|
||||
# actual Unix system accounts (passwd file). Virtual users are not in passwd,
|
||||
# so Postfix finds no match, skips the reject, and falls through to
|
||||
# transport_maps where their dovecot entry routes them correctly.
|
||||
postconf -e "local_recipient_maps ="
|
||||
|
||||
postconf -e "dovecot_destination_recipient_limit = 1"
|
||||
|
||||
# Initialise the per-user transport map (entries added by email-account-create)
|
||||
VTRANSPORT_FILE="/etc/postfix/virtual_transport"
|
||||
if [[ ! -f "${VTRANSPORT_FILE}" ]]; then
|
||||
touch "${VTRANSPORT_FILE}"
|
||||
success "Created empty ${VTRANSPORT_FILE}."
|
||||
fi
|
||||
postmap "${VTRANSPORT_FILE}"
|
||||
postconf -e "transport_maps = hash:${VTRANSPORT_FILE}"
|
||||
success "transport_maps initialised."
|
||||
|
||||
# Add dovecot LDA transport to master.cf if not already there.
|
||||
# Use dovecot-lda (the correct binary name on Debian/Ubuntu).
|
||||
MASTER_CF="/etc/postfix/master.cf"
|
||||
if ! grep -q "^dovecot" "${MASTER_CF}"; then
|
||||
cat >> "${MASTER_CF}" << 'MASTER'
|
||||
dovecot unix - n n - - pipe
|
||||
flags=DRhu user=vmail:mail argv=/usr/lib/dovecot/dovecot-lda -f ${sender} -d ${recipient}
|
||||
MASTER
|
||||
success "Dovecot LDA transport added to master.cf."
|
||||
fi
|
||||
|
||||
# Enable Dovecot auth-userdb socket for Postfix SASL.
|
||||
# Use a separate drop-in file instead of injecting into 10-master.conf
|
||||
# to avoid sed portability issues and keep changes isolated.
|
||||
DOVECOT_POSTFIX_CONF="/etc/dovecot/conf.d/99-postfix-auth.conf"
|
||||
if [[ ! -f "${DOVECOT_POSTFIX_CONF}" ]]; then
|
||||
cat > "${DOVECOT_POSTFIX_CONF}" << 'DOVECOTCONF'
|
||||
# Dovecot SASL socket for Postfix SMTP AUTH
|
||||
# Added by full-setup.sh — remove this file to undo
|
||||
service auth {
|
||||
unix_listener /var/spool/postfix/private/auth {
|
||||
mode = 0660
|
||||
user = postfix
|
||||
group = postfix
|
||||
}
|
||||
}
|
||||
DOVECOTCONF
|
||||
success "Created Dovecot Postfix-auth drop-in at ${DOVECOT_POSTFIX_CONF}."
|
||||
else
|
||||
success "Dovecot Postfix-auth drop-in already exists."
|
||||
fi
|
||||
|
||||
systemctl reload postfix
|
||||
systemctl reload dovecot
|
||||
success "Postfix SASL + virtual mailbox delivery configured."
|
||||
|
||||
# Quick test: send a mail from no-reply@freedoms4.org to itself (appears in /var/mail or mail queue)
|
||||
info "Sending a Postfix self-test email..."
|
||||
echo "Postfix OTP relay self-test from full-setup.sh" \
|
||||
| mail -s "freedoms4 mail test" \
|
||||
-a "From: ${OTP_FROM}" \
|
||||
root 2>/dev/null \
|
||||
&& success "Test email queued (check /var/mail/root or 'mailq')." \
|
||||
|| warn "mail command not available for self-test — install mailutils if needed."
|
||||
|
||||
# ── STEP 6 Generate env file + deploy auth.php ──
|
||||
info "Creating credentials env file at ${ENV_FILE}..."
|
||||
|
||||
# Write env file
|
||||
mkdir -p "$(dirname ${ENV_FILE})"
|
||||
cat > "${ENV_FILE}" << ENV
|
||||
# /etc/freedoms4/auth.env — generated by full-setup.sh on $(date)
|
||||
# Do NOT commit to version control.
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_NAME=${DB_NAME}
|
||||
DB_USER=${DB_USER}
|
||||
DB_PASS=${DB_PASS}
|
||||
PROSODY_DB_NAME=prosody
|
||||
PROSODY_DB_USER=${PROSODY_DB_USER}
|
||||
PROSODY_DB_PASS=${PROSODY_DB_PASS}
|
||||
PROSODY_HOST=freedoms4.org
|
||||
ENV
|
||||
|
||||
chown root:www-data "${ENV_FILE}"
|
||||
chmod 640 "${ENV_FILE}"
|
||||
success "Env file written and secured (root:www-data 640)."
|
||||
|
||||
info "Deploying auth.php to ${API_DIR}..."
|
||||
|
||||
if [[ ! -f "${SCRIPT_DIR}/auth.php" ]]; then
|
||||
error "auth.php not found in ${SCRIPT_DIR}."
|
||||
fi
|
||||
|
||||
mkdir -p "${API_DIR}"
|
||||
cp "${SCRIPT_DIR}/auth.php" "${API_DIR}/auth.php"
|
||||
chown -R www-data:www-data "${API_DIR}"
|
||||
chmod 640 "${API_DIR}/auth.php"
|
||||
success "auth.php deployed."
|
||||
|
||||
if [[ ! -f "${SCRIPT_DIR}/comments.php" ]]; then
|
||||
error "comments.php not found in ${SCRIPT_DIR}."
|
||||
fi
|
||||
cp "${SCRIPT_DIR}/comments.php" "${API_DIR}/comments.php"
|
||||
chown www-data:www-data "${API_DIR}/comments.php"
|
||||
chmod 640 "${API_DIR}/comments.php"
|
||||
success "comments.php deployed."
|
||||
|
||||
if [[ ! -f "${SCRIPT_DIR}/admin.php" ]]; then
|
||||
error "admin.php not found in ${SCRIPT_DIR}."
|
||||
fi
|
||||
cp "${SCRIPT_DIR}/admin.php" "${API_DIR}/admin.php"
|
||||
chown www-data:www-data "${API_DIR}/admin.php"
|
||||
chmod 640 "${API_DIR}/admin.php"
|
||||
success "admin.php deployed."
|
||||
|
||||
|
||||
|
||||
# ── STEP 6b Virtual mail setup (Dovecot + vmail) ──
|
||||
info "Setting up virtual mail user and Dovecot passwd-file..."
|
||||
|
||||
# Create vmail system user if not exists
|
||||
if ! id vmail &>/dev/null; then
|
||||
useradd -r -u 5000 -g mail -d /var/vmail -s /sbin/nologin vmail
|
||||
success "Created vmail system user (uid 5000)."
|
||||
else
|
||||
success "vmail user already exists."
|
||||
fi
|
||||
|
||||
# Create mailbox base directory
|
||||
mkdir -p /var/vmail
|
||||
mkdir -p /var/dovecot
|
||||
chown vmail:mail /var/vmail
|
||||
chmod 770 /var/vmail
|
||||
success "/var/vmail ready."
|
||||
|
||||
# Create /var/dovecot/users if not exists
|
||||
if [[ ! -f /var/dovecot/users ]]; then
|
||||
touch /var/dovecot/users
|
||||
chown root:root /var/dovecot/users
|
||||
chmod 644 /var/dovecot/users
|
||||
success "Created /var/dovecot/users."
|
||||
else
|
||||
success "/var/dovecot/users already exists."
|
||||
fi
|
||||
|
||||
# Enable auth-passwdfile in 10-auth.conf (add include if not already there)
|
||||
if ! grep -q "auth-passwdfile" /etc/dovecot/conf.d/10-auth.conf; then
|
||||
echo '!include auth-passwdfile.conf.ext' >> /etc/dovecot/conf.d/10-auth.conf
|
||||
success "Enabled auth-passwdfile.conf.ext in 10-auth.conf."
|
||||
fi
|
||||
|
||||
# Enable auth-passwdfile.conf.ext — uncomment the passdb/userdb blocks
|
||||
PASSWDFILE="/etc/dovecot/conf.d/auth-passwdfile.conf.ext"
|
||||
if grep -q "^#passdb" "${PASSWDFILE}" 2>/dev/null || ! grep -q "^passdb" "${PASSWDFILE}" 2>/dev/null; then
|
||||
cat > "${PASSWDFILE}" << 'DOVECOT'
|
||||
passdb {
|
||||
driver = passwd-file
|
||||
args = scheme=SHA512-CRYPT username_format=%n /var/dovecot/users
|
||||
}
|
||||
userdb {
|
||||
driver = passwd-file
|
||||
args = username_format=%n /var/dovecot/users
|
||||
default_fields = uid=vmail gid=mail home=/var/vmail/%n@freedoms4.org/maildir
|
||||
}
|
||||
DOVECOT
|
||||
success "auth-passwdfile.conf.ext configured."
|
||||
fi
|
||||
|
||||
systemctl reload dovecot
|
||||
success "Dovecot reloaded."
|
||||
|
||||
# ── STEP 6c Deploy email-account-create wrapper + sudoers ──
|
||||
info "Deploying email-account-create wrapper script..."
|
||||
|
||||
if [[ ! -f "${SCRIPT_DIR}/email-account-create.sh" ]]; then
|
||||
error "email-account-create.sh not found in ${SCRIPT_DIR}."
|
||||
fi
|
||||
|
||||
cp "${SCRIPT_DIR}/email-account-create.sh" /usr/local/bin/email-account-create
|
||||
chown root:root /usr/local/bin/email-account-create
|
||||
chmod 755 /usr/local/bin/email-account-create
|
||||
success "email-account-create deployed to /usr/local/bin/"
|
||||
|
||||
SUDOERS_EMAIL="/etc/sudoers.d/email-account-create"
|
||||
echo "www-data ALL=(root) NOPASSWD: /usr/local/bin/email-account-create" > "${SUDOERS_EMAIL}"
|
||||
chmod 440 "${SUDOERS_EMAIL}"
|
||||
visudo -cf "${SUDOERS_EMAIL}" && success "sudoers rule for email-account-create installed." \
|
||||
|| error "sudoers syntax check failed — check ${SUDOERS_EMAIL}"
|
||||
|
||||
|
||||
# ── STEP 6d Deploy email-block wrapper + sudoers ──
|
||||
info "Deploying email-block wrapper script..."
|
||||
|
||||
if [[ ! -f "${SCRIPT_DIR}/email-block.sh" ]]; then
|
||||
error "email-block.sh not found in ${SCRIPT_DIR}."
|
||||
fi
|
||||
|
||||
cp "${SCRIPT_DIR}/email-block.sh" /usr/local/bin/email-block
|
||||
chown root:root /usr/local/bin/email-block
|
||||
chmod 755 /usr/local/bin/email-block
|
||||
success "email-block deployed to /usr/local/bin/"
|
||||
|
||||
# Create backup file if not exists
|
||||
touch /var/dovecot/users.blocked
|
||||
chown root:root /var/dovecot/users.blocked
|
||||
chmod 644 /var/dovecot/users.blocked
|
||||
success "/var/dovecot/users.blocked ready."
|
||||
|
||||
SUDOERS_BLOCK="/etc/sudoers.d/email-block"
|
||||
echo "www-data ALL=(root) NOPASSWD: /usr/local/bin/email-block" > "${SUDOERS_BLOCK}"
|
||||
chmod 440 "${SUDOERS_BLOCK}"
|
||||
visudo -cf "${SUDOERS_BLOCK}" && success "sudoers rule for email-block installed." \
|
||||
|| error "sudoers syntax check failed — check ${SUDOERS_BLOCK}"
|
||||
|
||||
|
||||
# ── STEP 6e Deploy email-delete wrapper + sudoers ──
|
||||
info "Deploying email-delete wrapper script..."
|
||||
|
||||
if [[ ! -f "${SCRIPT_DIR}/email-delete.sh" ]]; then
|
||||
error "email-delete.sh not found in ${SCRIPT_DIR}."
|
||||
fi
|
||||
|
||||
cp "${SCRIPT_DIR}/email-delete.sh" /usr/local/bin/email-delete
|
||||
chown root:root /usr/local/bin/email-delete
|
||||
chmod 755 /usr/local/bin/email-delete
|
||||
success "email-delete deployed to /usr/local/bin/"
|
||||
|
||||
SUDOERS_DELETE="/etc/sudoers.d/email-delete"
|
||||
echo "www-data ALL=(root) NOPASSWD: /usr/local/bin/email-delete" > "${SUDOERS_DELETE}"
|
||||
chmod 440 "${SUDOERS_DELETE}"
|
||||
visudo -cf "${SUDOERS_DELETE}" && success "sudoers rule for email-delete installed." \
|
||||
|| error "sudoers syntax check failed — check ${SUDOERS_DELETE}"
|
||||
|
||||
|
||||
# ── STEP 7 Nginx config ──
|
||||
info "Deploying nginx config for ${DOMAIN}..."
|
||||
|
||||
if [[ ! -f "${SCRIPT_DIR}/backend.freedoms4.org" ]]; then
|
||||
error "backend.freedoms4.org not found in ${SCRIPT_DIR}."
|
||||
fi
|
||||
|
||||
cp "${SCRIPT_DIR}/backend.freedoms4.org" "/etc/nginx/sites-available/${DOMAIN}"
|
||||
ln -sf "/etc/nginx/sites-available/${DOMAIN}" "/etc/nginx/sites-enabled/${DOMAIN}"
|
||||
|
||||
nginx -t || error "Nginx config test failed."
|
||||
systemctl restart nginx
|
||||
success "Nginx restarted."
|
||||
|
||||
# ── STEP 8 Certbot / SSL ──
|
||||
info "Checking SSL certificate..."
|
||||
|
||||
if ! command -v certbot &>/dev/null; then
|
||||
apt install -y certbot python3-certbot-nginx
|
||||
success "Certbot installed."
|
||||
fi
|
||||
|
||||
if [[ -f "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" ]]; then
|
||||
warn "Certificate for ${DOMAIN} already exists, skipping issuance."
|
||||
else
|
||||
info "Obtaining SSL certificate for ${DOMAIN}..."
|
||||
certbot --nginx -d "${DOMAIN}" --non-interactive --agree-tos -m "${CERTBOT_EMAIL}"
|
||||
success "Certificate obtained."
|
||||
fi
|
||||
|
||||
if systemctl list-timers 2>/dev/null | grep -q certbot; then
|
||||
success "Certbot auto-renewal timer is active."
|
||||
else
|
||||
systemctl enable --now certbot.timer 2>/dev/null || true
|
||||
if ! crontab -l 2>/dev/null | grep -q certbot; then
|
||||
(crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet --post-hook 'systemctl reload nginx'") | crontab -
|
||||
success "Added certbot renewal cron job."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── STEP 9 OTP cleanup cron ──
|
||||
info "Installing OTP cleanup cron (purges rows older than 24 h)..."
|
||||
|
||||
OTP_CRON="0 4 * * * psql postgresql://${DB_USER}:${DB_PASS}@127.0.0.1/${DB_NAME} -c \"DELETE FROM email_otps WHERE created_at < NOW() - INTERVAL '24 hours';\" >/dev/null 2>&1"
|
||||
|
||||
if ! crontab -l 2>/dev/null | grep -q "email_otps"; then
|
||||
(crontab -l 2>/dev/null; echo "${OTP_CRON}") | crontab -
|
||||
success "OTP cleanup cron installed (runs daily at 04:00)."
|
||||
else
|
||||
success "OTP cleanup cron already present."
|
||||
fi
|
||||
|
||||
# ── STEP 10 Smoke test ──
|
||||
info "Running smoke test..."
|
||||
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||
"https://${DOMAIN}/auth.php" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Origin: https://freedoms4.org" \
|
||||
-d '{"action":"login","username":"__probe__","password":"__probe__"}' \
|
||||
--max-time 10 || true)
|
||||
|
||||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||||
success "Smoke test passed (HTTP 200)."
|
||||
elif [[ "$HTTP_CODE" == "000" ]]; then
|
||||
error "Smoke test failed (000) — endpoint unreachable. Run: curl -sv https://${DOMAIN}/auth.php"
|
||||
else
|
||||
success "Smoke test: HTTP ${HTTP_CODE} — endpoint is reachable."
|
||||
fi
|
||||
|
||||
# ── Done ──
|
||||
echo ""
|
||||
echo -e "${GREEN}================================================${NC}"
|
||||
echo -e "${GREEN} Setup complete!${NC}"
|
||||
echo -e "${GREEN}================================================${NC}"
|
||||
echo ""
|
||||
echo -e " API endpoint : ${BLUE}https://${DOMAIN}/auth.php${NC}"
|
||||
echo -e " Auth file : ${BLUE}${API_DIR}/auth.php${NC}"
|
||||
echo -e " OTP sender : ${BLUE}${OTP_FROM}${NC}"
|
||||
echo ""
|
||||
echo ""
|
||||
144
rollback-everything.sh
Executable file
144
rollback-everything.sh
Executable file
@@ -0,0 +1,144 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
|
||||
# Must run as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo -e "${RED}[ERROR]${NC} Please run as root: sudo bash rollback-everything.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${RED}WARNING:${NC} This will remove the database, email accounts, mailboxes, and all config."
|
||||
echo -n "Are you sure you want to proceed? (yes/no) [no]: "
|
||||
read -r CONFIRM
|
||||
if [[ "${CONFIRM}" != "yes" ]]; then
|
||||
echo "Aborted."
|
||||
exit 0
|
||||
fi
|
||||
echo ""
|
||||
info "Stopping php8.2-fpm..."
|
||||
systemctl stop php8.2-fpm
|
||||
|
||||
# ── 2. Terminate any remaining freedoms4 DB connections ──
|
||||
info "Terminating freedoms4 DB connections..."
|
||||
(cd /tmp && sudo -u postgres psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='freedoms4';" 2>/dev/null || true)
|
||||
|
||||
# ── 3. Revoke freedoms4_user access to prosody DB ──
|
||||
info "Revoking freedoms4_user from prosody DB..."
|
||||
(cd /tmp && sudo -u postgres psql -d prosody -c "REVOKE ALL ON TABLE prosody FROM freedoms4_user;" 2>/dev/null || true)
|
||||
(cd /tmp && sudo -u postgres psql -c "REVOKE CONNECT ON DATABASE prosody FROM freedoms4_user;" 2>/dev/null || true)
|
||||
|
||||
# ── 4. Drop the freedoms4 database and user ──
|
||||
info "Dropping freedoms4 database and user..."
|
||||
(cd /tmp && sudo -u postgres psql -c "DROP DATABASE IF EXISTS freedoms4;")
|
||||
(cd /tmp && sudo -u postgres psql -c "DROP USER IF EXISTS freedoms4_user;")
|
||||
success "Database and user dropped."
|
||||
|
||||
# ── 5. Remove deployed API dir and env file ──
|
||||
info "Removing API dir and env file..."
|
||||
rm -rf /var/www/freedoms4
|
||||
rm -rf /etc/freedoms4
|
||||
success "API dir and env file removed."
|
||||
|
||||
# ── 6. Remove nginx site ──
|
||||
info "Removing nginx site config..."
|
||||
rm -f /etc/nginx/sites-enabled/backend.freedoms4.org
|
||||
rm -f /etc/nginx/sites-available/backend.freedoms4.org
|
||||
systemctl restart nginx
|
||||
success "Nginx site removed and restarted."
|
||||
|
||||
# ── 7. Remove email account wrapper and sudoers rule ──
|
||||
info "Removing email-account-create script and sudoers rule..."
|
||||
rm -f /usr/local/bin/email-account-create
|
||||
rm -f /etc/sudoers.d/email-account-create
|
||||
rm -f /usr/local/bin/email-block
|
||||
rm -f /etc/sudoers.d/email-block
|
||||
rm -f /usr/local/bin/email-delete
|
||||
rm -f /etc/sudoers.d/email-delete
|
||||
success "email-account-create removed."
|
||||
|
||||
# ── 8. Remove ONLY site-created virtual users from /var/dovecot/users ──
|
||||
# System users (like hyzen) are NOT in /var/dovecot/users — they authenticate
|
||||
# via PAM/system auth — so we can safely remove the entire file.
|
||||
# Their mailboxes in /var/mail/<user> or ~/Maildir are untouched.
|
||||
info "Removing virtual users file (/var/dovecot/users)..."
|
||||
rm -f /var/dovecot/users
|
||||
rm -f /var/dovecot/users.blocked
|
||||
success "Virtual users file removed (system users unaffected)."
|
||||
|
||||
# Remove vmail mailbox data (only virtual users live here; system users use /var/mail)
|
||||
info "Removing /var/vmail (virtual mailboxes only)..."
|
||||
rm -rf /var/vmail
|
||||
success "/var/vmail removed."
|
||||
|
||||
# Remove vmail system user (not a login account, safe to remove)
|
||||
if id vmail &>/dev/null; then
|
||||
userdel vmail 2>/dev/null || true
|
||||
success "vmail system user removed."
|
||||
fi
|
||||
|
||||
# ── 9. Undo Dovecot passwd-file auth config ──
|
||||
info "Reverting Dovecot auth config..."
|
||||
sed -i '/auth-passwdfile/d' /etc/dovecot/conf.d/10-auth.conf
|
||||
cat > /etc/dovecot/conf.d/auth-passwdfile.conf.ext << 'DOVECOT'
|
||||
# passdb and userdb for virtual users — managed by full-setup.sh
|
||||
# (currently inactive; run full-setup.sh to re-enable)
|
||||
DOVECOT
|
||||
# Remove the Postfix-auth drop-in (do NOT touch 10-master.conf itself)
|
||||
rm -f /etc/dovecot/conf.d/99-postfix-auth.conf
|
||||
systemctl reload dovecot
|
||||
success "Dovecot config reverted."
|
||||
|
||||
# ── 10. Undo Postfix SASL and virtual mailbox config ──
|
||||
info "Reverting Postfix SASL and virtual mailbox config..."
|
||||
|
||||
# mydestination was never changed by full-setup, so nothing to restore there.
|
||||
|
||||
# Use postconf -X to fully REMOVE parameters (not set them empty).
|
||||
# postconf -e "param =" writes "param =" to main.cf which Postfix rejects
|
||||
# for parameters like virtual_transport that cannot be blank.
|
||||
postconf -X transport_maps
|
||||
postconf -X dovecot_destination_recipient_limit
|
||||
postconf -X local_recipient_maps
|
||||
postconf -X smtpd_sasl_type
|
||||
postconf -X smtpd_sasl_path
|
||||
postconf -e "smtpd_sasl_auth_enable = no"
|
||||
postconf -X smtpd_sasl_security_options
|
||||
postconf -X smtpd_sasl_local_domain
|
||||
postconf -e "broken_sasl_auth_clients = no"
|
||||
|
||||
# Restore default recipient restrictions
|
||||
postconf -e "smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination"
|
||||
|
||||
# Remove the dovecot pipe transport block from master.cf
|
||||
sed -i '/^dovecot[[:space:]]*unix.*pipe/,/argv=\/usr\/lib\/dovecot\//d' /etc/postfix/master.cf 2>/dev/null || true
|
||||
|
||||
# Remove transport map files
|
||||
rm -f /etc/postfix/virtual_transport /etc/postfix/virtual_transport.db
|
||||
|
||||
systemctl reload postfix
|
||||
success "Postfix config reverted — system users (hyzen etc.) restored."
|
||||
|
||||
# ── 11. Restart php-fpm ──
|
||||
info "Restarting php8.2-fpm..."
|
||||
systemctl start php8.2-fpm
|
||||
success "php8.2-fpm restarted."
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}================================================${NC}"
|
||||
echo -e "${GREEN} Rollback complete.${NC}"
|
||||
echo -e "${GREEN}================================================${NC}"
|
||||
echo ""
|
||||
echo " Run full-setup.sh again to restore the backend."
|
||||
echo ""
|
||||
108
uninstall.sh
Executable file
108
uninstall.sh
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
|
||||
# Must run as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo -e "${RED}[ERROR]${NC} Please run as root: sudo bash uninstall.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}================================================${NC}"
|
||||
echo -e "${BLUE} freedoms4 uninstall (keeps DB + email accounts)${NC}"
|
||||
echo -e "${BLUE}================================================${NC}"
|
||||
echo ""
|
||||
warn "This removes deployed files and config only."
|
||||
warn "Database, email accounts, and mailboxes are preserved."
|
||||
echo ""
|
||||
|
||||
# ── 1. Stop php-fpm to release DB connections ──
|
||||
info "Stopping php8.2-fpm..."
|
||||
systemctl stop php8.2-fpm
|
||||
|
||||
# ── 2. Remove deployed API dir and env file ──
|
||||
info "Removing API dir and env file..."
|
||||
rm -rf /var/www/freedoms4
|
||||
rm -rf /etc/freedoms4
|
||||
success "API dir and env file removed."
|
||||
|
||||
# ── 3. Remove nginx site ──
|
||||
info "Removing nginx site config..."
|
||||
rm -f /etc/nginx/sites-enabled/backend.freedoms4.org
|
||||
rm -f /etc/nginx/sites-available/backend.freedoms4.org
|
||||
systemctl restart nginx
|
||||
success "Nginx site removed and restarted."
|
||||
|
||||
# ── 4. Remove email account wrapper and sudoers rule ──
|
||||
info "Removing email-account-create script and sudoers rule..."
|
||||
rm -f /usr/local/bin/email-account-create
|
||||
rm -f /etc/sudoers.d/email-account-create
|
||||
success "email-account-create removed."
|
||||
|
||||
# ── 5. Undo Dovecot passwd-file auth config ──
|
||||
# NOTE: /var/dovecot/users and /var/vmail are intentionally preserved.
|
||||
info "Reverting Dovecot auth config (preserving user accounts and mailboxes)..."
|
||||
sed -i '/auth-passwdfile/d' /etc/dovecot/conf.d/10-auth.conf
|
||||
cat > /etc/dovecot/conf.d/auth-passwdfile.conf.ext << 'DOVECOT'
|
||||
# passdb and userdb for virtual users — managed by full-setup.sh
|
||||
# (currently inactive; run full-setup.sh to re-enable)
|
||||
DOVECOT
|
||||
rm -f /etc/dovecot/conf.d/99-postfix-auth.conf
|
||||
systemctl reload dovecot
|
||||
success "Dovecot config reverted (accounts and mailboxes untouched)."
|
||||
|
||||
# ── 6. Undo Postfix SASL and virtual mailbox config ──
|
||||
info "Reverting Postfix config..."
|
||||
|
||||
# Use postconf -X to fully remove parameters rather than set them empty.
|
||||
postconf -X transport_maps
|
||||
postconf -X dovecot_destination_recipient_limit
|
||||
postconf -X local_recipient_maps
|
||||
postconf -X smtpd_sasl_type
|
||||
postconf -X smtpd_sasl_path
|
||||
postconf -e "smtpd_sasl_auth_enable = no"
|
||||
postconf -X smtpd_sasl_security_options
|
||||
postconf -X smtpd_sasl_local_domain
|
||||
postconf -e "broken_sasl_auth_clients = no"
|
||||
postconf -e "smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination"
|
||||
|
||||
# Remove the dovecot pipe transport block from master.cf
|
||||
sed -i '/^dovecot[[:space:]]*unix.*pipe/,/argv=\/usr\/lib\/dovecot\//d' /etc/postfix/master.cf 2>/dev/null || true
|
||||
|
||||
# NOTE: /etc/postfix/virtual_transport and its .db are preserved so that
|
||||
# existing site-created email accounts retain their routing entries when
|
||||
# full-setup.sh re-enables transport_maps.
|
||||
success "Postfix config reverted — system users (hyzen etc.) unaffected."
|
||||
|
||||
systemctl reload postfix
|
||||
|
||||
# ── 7. Restart php-fpm ──
|
||||
info "Restarting php8.2-fpm..."
|
||||
systemctl start php8.2-fpm
|
||||
success "php8.2-fpm restarted."
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}================================================${NC}"
|
||||
echo -e "${GREEN} Uninstall complete.${NC}"
|
||||
echo -e "${GREEN}================================================${NC}"
|
||||
echo ""
|
||||
echo " Preserved:"
|
||||
echo " - PostgreSQL database 'freedoms4' and all data"
|
||||
echo " - /var/dovecot/users (virtual email accounts)"
|
||||
echo " - /var/vmail (mailboxes)"
|
||||
echo " - /etc/postfix/virtual_transport (routing entries)"
|
||||
echo " - vmail system user"
|
||||
echo ""
|
||||
echo " Run full-setup.sh again to redeploy without losing any data."
|
||||
echo ""
|
||||
Reference in New Issue
Block a user