Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a50409bdca | |||
| 60a722e6cc | |||
| 4d6cafa589 | |||
| 63e557430b | |||
| 04acb4423d | |||
| ea813acf4c | |||
| b1198dfdbf | |||
| 4342de3747 | |||
| ef8bc7e7e9 | |||
| e18e401f6b | |||
| ab998c47e8 | |||
| 9fb830b2e1 | |||
| 415c3f90a1 | |||
| 60c8ff7fb1 | |||
| 037816313c | |||
| 3d285d1ac6 | |||
| 135338786f | |||
| 661f1fce31 | |||
| 03664ef784 | |||
| d2111ef2b6 |
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"rules": {
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
# following files are skipped when exporting using git archive
|
||||
test export-ignore
|
||||
.jshintrc export-ignore
|
||||
.gitlab export-ignore
|
||||
docs export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
node_modules/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
webadmin/dist/
|
||||
setup/splash/website/
|
||||
installer/src/certs/server.key
|
||||
|
||||
# vim swap files
|
||||
*.swp
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
Please do not use this issue tracker for support requests and bug reports.
|
||||
This issue tracker is used by the Cloudron development team to track actual
|
||||
bugs in the code.
|
||||
|
||||
Please use the forum at https://forum.cloudron.io to report bugs. For
|
||||
confidential issues, please email us at support@cloudron.io.
|
||||
@@ -1,7 +0,0 @@
|
||||
Please do not use this issue tracker for support requests and feature reports.
|
||||
This issue tracker is used by the Cloudron development team to track issues in
|
||||
the code.
|
||||
|
||||
Please use the forum at https://forum.cloudron.io to report bugs. For
|
||||
confidential issues, please email us at support@cloudron.io.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"unused": true,
|
||||
"esversion": 11
|
||||
"globalstrict": true,
|
||||
"predef": [ "angular", "$" ],
|
||||
"esnext": true
|
||||
}
|
||||
|
||||
@@ -1,35 +1,661 @@
|
||||
The Cloudron Subscription license
|
||||
Copyright (c) 2022 Cloudron UG
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
With regard to the Cloudron Software:
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Cloudron Subscription Terms of Service, available
|
||||
at https://cloudron.io/legal/terms.html (the “Subscription Terms”), or other
|
||||
agreement governing the use of the Software, as agreed by you and Cloudron,
|
||||
and otherwise have a valid Cloudron Subscription. Subject to the foregoing sentence,
|
||||
you are free to modify this Software and publish patches to the Software. You agree
|
||||
that Subscription and/or its licensors (as applicable) retain all right, title and
|
||||
interest in and to all such modifications and/or patches, and all such modifications
|
||||
and/or patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Cloudron subscription. Notwithstanding the foregoing, you may copy
|
||||
and modify the Software for development and testing purposes, without requiring a
|
||||
subscription. You agree that Cloudron and/or its licensors (as applicable) retain
|
||||
all right, title and interest in and to all such modifications. You are not
|
||||
granted any other rights beyond what is expressly stated herein. Subject to the
|
||||
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
Preamble
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
For all third party components incorporated into the Cloudron Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
box
|
||||
Copyright (C) 2016 Cloudron UG
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||

|
||||
|
||||
# Cloudron
|
||||
|
||||
[Cloudron](https://cloudron.io) is the best way to run apps on your server.
|
||||
@@ -11,6 +9,10 @@ a complex task.
|
||||
We are building the ultimate platform for self-hosting web apps. The Cloudron allows
|
||||
anyone to effortlessly host web applications on their server on their own terms.
|
||||
|
||||
Support us on
|
||||
[](https://flattr.com/submit/auto?user_id=cloudron&url=https://cloudron.io&title=Cloudron&tags=opensource&category=software)
|
||||
or [pay us a coffee](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8982CKNM46D8U)
|
||||
|
||||
## Features
|
||||
|
||||
* Single click install for apps. Check out the [App Store](https://cloudron.io/appstore.html).
|
||||
@@ -31,43 +33,47 @@ anyone to effortlessly host web applications on their server on their own terms.
|
||||
* Trivially migrate to another server keeping your apps and data (for example, switch your
|
||||
infrastructure provider or move to a bigger server).
|
||||
|
||||
* Comprehensive [REST API](https://docs.cloudron.io/api/).
|
||||
* Comprehensive [REST API](https://cloudron.io/references/api.html).
|
||||
|
||||
* [CLI](https://docs.cloudron.io/custom-apps/cli/) to configure apps.
|
||||
* [CLI](https://git.cloudron.io/cloudron/cloudron-cli) to configure apps.
|
||||
|
||||
* Alerts, audit logs, graphs, dns management ... and much more
|
||||
|
||||
## Demo
|
||||
|
||||
Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudron).
|
||||
Try our demo at https://my-demo.cloudron.me (username: cloudron password: cloudron).
|
||||
|
||||
## Installing
|
||||
|
||||
[Install script](https://docs.cloudron.io/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
You can install the Cloudron platform on your own server or get a managed server
|
||||
from cloudron.io.
|
||||
|
||||
**Note:** This repo is a small part of what gets installed on your server - there is
|
||||
the dashboard, database addons, graph container, base image etc. Cloudron also relies
|
||||
on external services such as the App Store for apps to be installed. As such, don't
|
||||
clone this repo and npm install and expect something to work.
|
||||
* [Selfhosting](https://cloudron.io/references/selfhosting.html)
|
||||
* [Managed Hosting](https://cloudron.io/pricing.html)
|
||||
|
||||
## License
|
||||
## Documentation
|
||||
|
||||
Please note that the Cloudron code is under a source-available license. This is not the same as an
|
||||
open source license but ensures the code is available for introspection (and hacking!).
|
||||
* [User manual](https://cloudron.io/references/usermanual.html)
|
||||
* [Developer docs](https://cloudron.io/documentation.html)
|
||||
* [Architecture](https://cloudron.io/references/architecture.html)
|
||||
|
||||
## Contributions
|
||||
## Related repos
|
||||
|
||||
Just to give some heads up, we are a bit restrictive in merging changes. We are a small team and
|
||||
would like to keep our maintenance burden low. It might be best to discuss features first in the [forum](https://forum.cloudron.io),
|
||||
to also figure out how many other people will use it to justify maintenance for a feature.
|
||||
The [base image repo](https://git.cloudron.io/cloudron/docker-base-image) is the parent image of all
|
||||
the containers in the Cloudron.
|
||||
|
||||
# Localization
|
||||
The [graphite repo](https://git.cloudron.io/cloudron/docker-graphite) contains the graphite code
|
||||
that collects metrics for graphs.
|
||||
|
||||

|
||||
The addons are located in separate repositories
|
||||
* [Redis](https://git.cloudron.io/cloudron/redis-addon)
|
||||
* [Postgresql](https://git.cloudron.io/cloudron/postgresql-addon)
|
||||
* [MySQL](https://git.cloudron.io/cloudron/mysql-addon)
|
||||
* [Mongodb](https://git.cloudron.io/cloudron/mongodb-addon)
|
||||
* [Mail](https://git.cloudron.io/cloudron/mail-addon)
|
||||
|
||||
## Support
|
||||
|
||||
* [Documentation](https://docs.cloudron.io/)
|
||||
* [Forum](https://forum.cloudron.io/)
|
||||
## Community
|
||||
|
||||
* [Chat](https://chat.cloudron.io/)
|
||||
* [Support](mailto:support@cloudron.io)
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,193 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
assertNotEmpty() {
|
||||
: "${!1:? "$1 is not set."}"
|
||||
}
|
||||
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
|
||||
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
|
||||
|
||||
INSTANCE_TYPE="t2.micro"
|
||||
BLOCK_DEVICE="DeviceName=/dev/sda1,Ebs={VolumeSize=20,DeleteOnTermination=true,VolumeType=gp2}"
|
||||
SSH_KEY_NAME="id_rsa_yellowtent"
|
||||
|
||||
revision=$(git rev-parse HEAD)
|
||||
ami_name=""
|
||||
server_id=""
|
||||
server_ip=""
|
||||
destroy_server="yes"
|
||||
deploy_env="prod"
|
||||
image_id=""
|
||||
|
||||
args=$(getopt -o "" -l "revision:,name:,no-destroy,env:,region:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--env) deploy_env="$2"; shift 2;;
|
||||
--revision) revision="$2"; shift 2;;
|
||||
--name) ami_name="$2"; shift 2;;
|
||||
--no-destroy) destroy_server="no"; shift 2;;
|
||||
--region)
|
||||
case "$2" in
|
||||
"us-east-1")
|
||||
image_id="ami-6edd3078"
|
||||
security_group="sg-a5e17fd9"
|
||||
subnet_id="subnet-b8fbc0f1"
|
||||
;;
|
||||
"eu-central-1")
|
||||
image_id="ami-5aee2235"
|
||||
security_group="sg-19f5a770" # everything open on eu-central-1
|
||||
subnet_id=""
|
||||
;;
|
||||
*)
|
||||
echo "Unknown aws region $2"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
export AWS_DEFAULT_REGION="$2" # used by the aws cli tool
|
||||
shift 2
|
||||
;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
# TODO fix this
|
||||
export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY}"
|
||||
export AWS_SECRET_ACCESS_KEY="${AWS_ACCESS_SECRET}"
|
||||
|
||||
readonly ssh_keys="${HOME}/.ssh/id_rsa_yellowtent"
|
||||
readonly SSH="ssh -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
|
||||
if [[ ! -f "${ssh_keys}" ]]; then
|
||||
echo "caas ssh key is missing at ${ssh_keys} (pick it up from secrets repo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${image_id}" ]]; then
|
||||
echo "--region is required (us-east-1 or eu-central-1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function get_pretty_revision() {
|
||||
local git_rev="$1"
|
||||
local sha1=$(git rev-parse --short "${git_rev}" 2>/dev/null)
|
||||
|
||||
echo "${sha1}"
|
||||
}
|
||||
|
||||
function wait_for_ssh() {
|
||||
echo "=> Waiting for ssh connection"
|
||||
while true; do
|
||||
echo -n "."
|
||||
|
||||
if $SSH ubuntu@${server_ip} echo "hello"; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
now=$(date "+%Y-%m-%d-%H%M%S")
|
||||
pretty_revision=$(get_pretty_revision "${revision}")
|
||||
|
||||
if [[ -z "${ami_name}" ]]; then
|
||||
ami_name="box-${deploy_env}-${pretty_revision}-${now}"
|
||||
fi
|
||||
|
||||
echo "=> Create EC2 instance"
|
||||
id=$(aws ec2 run-instances --image-id "${image_id}" --instance-type "${INSTANCE_TYPE}" --security-group-ids "${security_group}" --block-device-mappings "${BLOCK_DEVICE}" --key-name "${SSH_KEY_NAME}" --subnet-id "${subnet_id}" --associate-public-ip-address \
|
||||
| $JSON Instances \
|
||||
| $JSON 0.InstanceId)
|
||||
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "Instance created ID $id"
|
||||
|
||||
echo "=> Waiting for instance to get a public IP"
|
||||
while true; do
|
||||
server_ip=$(aws ec2 describe-instances --instance-ids ${id} \
|
||||
| $JSON Reservations.0.Instances \
|
||||
| $JSON 0.PublicIpAddress)
|
||||
|
||||
if [[ ! -z "${server_ip}" ]]; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Got public IP ${server_ip}"
|
||||
|
||||
wait_for_ssh
|
||||
|
||||
echo "=> Fetching cloudron-setup"
|
||||
while true; do
|
||||
|
||||
if $SSH ubuntu@${server_ip} wget "https://cloudron.io/cloudron-setup" -O "cloudron-setup"; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "=> Running cloudron-setup"
|
||||
$SSH ubuntu@${server_ip} sudo /bin/bash "cloudron-setup" --env "${deploy_env}" --provider "ami" --skip-reboot
|
||||
|
||||
wait_for_ssh
|
||||
|
||||
echo "=> Removing ssh key"
|
||||
$SSH ubuntu@${server_ip} sudo rm /home/ubuntu/.ssh/authorized_keys /root/.ssh/authorized_keys
|
||||
|
||||
echo "=> Creating AMI"
|
||||
image_id=$(aws ec2 create-image --instance-id "${id}" --name "${ami_name}" | $JSON ImageId)
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "Creating AMI with Id ${image_id}"
|
||||
|
||||
echo "=> Waiting for AMI to be created"
|
||||
while true; do
|
||||
state=$(aws ec2 describe-images --image-ids ${image_id} \
|
||||
| $JSON Images \
|
||||
| $JSON 0.State)
|
||||
|
||||
if [[ "${state}" == "available" ]]; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [[ "${destroy_server}" == "yes" ]]; then
|
||||
echo "=> Deleting EC2 instance"
|
||||
|
||||
while true; do
|
||||
state=$(aws ec2 terminate-instances --instance-id "${id}" \
|
||||
| $JSON TerminatingInstances \
|
||||
| $JSON 0.CurrentState.Name)
|
||||
|
||||
if [[ "${state}" == "shutting-down" ]]; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 5
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Done."
|
||||
echo ""
|
||||
echo "New AMI is: ${image_id}"
|
||||
echo ""
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
assertNotEmpty() {
|
||||
: "${!1:? "$1 is not set."}"
|
||||
}
|
||||
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
|
||||
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
|
||||
|
||||
revision=$(git rev-parse HEAD)
|
||||
box_name=""
|
||||
server_id=""
|
||||
server_ip=""
|
||||
destroy_server="yes"
|
||||
deploy_env="dev"
|
||||
|
||||
# Only GNU getopt supports long options. OS X comes bundled with the BSD getopt
|
||||
# brew install gnu-getopt to get the GNU getopt on OS X
|
||||
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
|
||||
readonly GNU_GETOPT
|
||||
|
||||
args=$(${GNU_GETOPT} -o "" -l "revision:,regions:,size:,name:,no-destroy,env:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--env) deploy_env="$2"; shift 2;;
|
||||
--revision) revision="$2"; shift 2;;
|
||||
--name) box_name="$2"; destroy_server="no"; shift 2;;
|
||||
--no-destroy) destroy_server="no"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Creating digitalocean image"
|
||||
if [[ "${deploy_env}" == "staging" ]]; then
|
||||
assertNotEmpty DIGITAL_OCEAN_TOKEN_STAGING
|
||||
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_STAGING}"
|
||||
elif [[ "${deploy_env}" == "dev" ]]; then
|
||||
assertNotEmpty DIGITAL_OCEAN_TOKEN_DEV
|
||||
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_DEV}"
|
||||
elif [[ "${deploy_env}" == "prod" ]]; then
|
||||
assertNotEmpty DIGITAL_OCEAN_TOKEN_PROD
|
||||
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_PROD}"
|
||||
else
|
||||
echo "No such env ${deploy_env}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
vps="/bin/bash ${SCRIPT_DIR}/digitalocean.sh"
|
||||
|
||||
readonly ssh_keys="${HOME}/.ssh/id_rsa_caas_${deploy_env}"
|
||||
readonly scp202="scp -P 202 -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
readonly scp22="scp -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
|
||||
readonly ssh202="ssh -p 202 -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
readonly ssh22="ssh -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
|
||||
if [[ ! -f "${ssh_keys}" ]]; then
|
||||
echo "caas ssh key is missing at ${ssh_keys} (pick it up from secrets repo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function get_pretty_revision() {
|
||||
local git_rev="$1"
|
||||
local sha1=$(git rev-parse --short "${git_rev}" 2>/dev/null)
|
||||
|
||||
echo "${sha1}"
|
||||
}
|
||||
|
||||
now=$(date "+%Y-%m-%d-%H%M%S")
|
||||
pretty_revision=$(get_pretty_revision "${revision}")
|
||||
|
||||
if [[ -z "${box_name}" ]]; then
|
||||
# if you change this, change the regexp is appstore/janitor.js
|
||||
box_name="box-${deploy_env}-${pretty_revision}-${now}" # remove slashes
|
||||
|
||||
# create a new server if no name given
|
||||
if ! caas_ssh_key_id=$($vps get_ssh_key_id "caas"); then
|
||||
echo "Could not query caas ssh key"
|
||||
exit 1
|
||||
fi
|
||||
echo "Detected caas ssh key id: ${caas_ssh_key_id}"
|
||||
|
||||
echo "Creating Server with name [${box_name}]"
|
||||
if ! server_id=$($vps create ${caas_ssh_key_id} ${box_name}); then
|
||||
echo "Failed to create server"
|
||||
exit 1
|
||||
fi
|
||||
echo "Created server with id: ${server_id}"
|
||||
|
||||
# If we run scripts overenthusiastically without the wait, setup script randomly fails
|
||||
echo -n "Waiting 120 seconds for server creation"
|
||||
for i in $(seq 1 24); do
|
||||
echo -n "."
|
||||
sleep 5
|
||||
done
|
||||
echo ""
|
||||
else
|
||||
if ! server_id=$($vps get_id "${box_name}"); then
|
||||
echo "Could not determine id from name"
|
||||
exit 1
|
||||
fi
|
||||
echo "Reusing server with id: ${server_id}"
|
||||
|
||||
$vps power_on "${server_id}"
|
||||
fi
|
||||
|
||||
# Query until we get an IP
|
||||
while true; do
|
||||
echo "Trying to get the server IP"
|
||||
if server_ip=$($vps get_ip "${server_id}"); then
|
||||
echo "Server IP : [${server_ip}]"
|
||||
break
|
||||
fi
|
||||
echo "Timedout, trying again in 10 seconds"
|
||||
sleep 10
|
||||
done
|
||||
|
||||
while true; do
|
||||
echo "Trying to copy init script to server"
|
||||
if $scp22 "${SCRIPT_DIR}/initializeBaseUbuntuImage.sh" root@${server_ip}:.; then
|
||||
break
|
||||
fi
|
||||
echo "Timedout, trying again in 30 seconds"
|
||||
sleep 30
|
||||
done
|
||||
|
||||
echo "Copying infra_version.js"
|
||||
$scp22 "${SCRIPT_DIR}/../src/infra_version.js" root@${server_ip}:.
|
||||
|
||||
echo "Copying box source"
|
||||
cd "${SOURCE_DIR}"
|
||||
git archive --format=tar HEAD | $ssh22 "root@${server_ip}" "cat - > /tmp/box.tar.gz"
|
||||
|
||||
echo "Executing init script"
|
||||
if ! $ssh22 "root@${server_ip}" "/bin/bash /root/initializeBaseUbuntuImage.sh caas"; then
|
||||
echo "Init script failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Shutting down server with id : ${server_id}"
|
||||
$ssh22 "root@${server_ip}" "shutdown -f now" || true # shutdown sometimes terminates ssh connection immediately making this command fail
|
||||
|
||||
# wait 10 secs for actual shutdown
|
||||
echo "Waiting for 10 seconds for server to shutdown"
|
||||
sleep 30
|
||||
|
||||
echo "Powering off server"
|
||||
if ! $vps power_off "${server_id}"; then
|
||||
echo "Could not power off server"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
snapshot_name="box-${deploy_env}-${pretty_revision}-${now}"
|
||||
echo "Snapshotting as ${snapshot_name}"
|
||||
if ! image_id=$($vps snapshot "${server_id}" "${snapshot_name}"); then
|
||||
echo "Could not snapshot and get image id"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${destroy_server}" == "yes" ]]; then
|
||||
echo "Destroying server"
|
||||
if ! $vps destroy "${server_id}"; then
|
||||
echo "Could not destroy server"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Skipping server destroy"
|
||||
fi
|
||||
|
||||
echo "Transferring image ${image_id} to other regions"
|
||||
$vps transfer_image_to_all_regions "${image_id}"
|
||||
|
||||
echo "Done."
|
||||
@@ -0,0 +1,261 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ -z "${DIGITAL_OCEAN_TOKEN}" ]]; then
|
||||
echo "Script requires DIGITAL_OCEAN_TOKEN env to be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${JSON}" ]]; then
|
||||
echo "Script requires JSON env to be set to path of JSON binary"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly CURL="curl --retry 5 -s -u ${DIGITAL_OCEAN_TOKEN}:"
|
||||
|
||||
function debug() {
|
||||
echo "$@" >&2
|
||||
}
|
||||
|
||||
function get_ssh_key_id() {
|
||||
id=$($CURL "https://api.digitalocean.com/v2/account/keys" \
|
||||
| $JSON ssh_keys \
|
||||
| $JSON -c "this.name === \"$1\"" \
|
||||
| $JSON 0.id)
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "$id"
|
||||
}
|
||||
|
||||
function create_droplet() {
|
||||
local ssh_key_id="$1"
|
||||
local box_name="$2"
|
||||
|
||||
local image_region="sfo1"
|
||||
local ubuntu_image_slug="ubuntu-16-04-x64"
|
||||
local box_size="1gb"
|
||||
|
||||
local data="{\"name\":\"${box_name}\",\"size\":\"${box_size}\",\"region\":\"${image_region}\",\"image\":\"${ubuntu_image_slug}\",\"ssh_keys\":[ \"${ssh_key_id}\" ],\"backups\":false}"
|
||||
|
||||
id=$($CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets" | $JSON droplet.id)
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "$id"
|
||||
}
|
||||
|
||||
function get_droplet_ip() {
|
||||
local droplet_id="$1"
|
||||
ip=$($CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}" | $JSON "droplet.networks.v4[0].ip_address")
|
||||
[[ -z "$ip" ]] && exit 1
|
||||
echo "$ip"
|
||||
}
|
||||
|
||||
function get_droplet_id() {
|
||||
local droplet_name="$1"
|
||||
id=$($CURL "https://api.digitalocean.com/v2/droplets?per_page=200" | $JSON "droplets" | $JSON -c "this.name === '${droplet_name}'" | $JSON "[0].id")
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "$id"
|
||||
}
|
||||
|
||||
function power_off_droplet() {
|
||||
local droplet_id="$1"
|
||||
local data='{"type":"power_off"}'
|
||||
local response=$($CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions")
|
||||
local event_id=`echo "${response}" | $JSON action.id`
|
||||
|
||||
if [[ -z "${event_id}" ]]; then
|
||||
debug "Got no event id, assuming already powered off."
|
||||
debug "Response: ${response}"
|
||||
return
|
||||
fi
|
||||
|
||||
debug "Powered off droplet. Event id: ${event_id}"
|
||||
debug -n "Waiting for droplet to power off"
|
||||
|
||||
while true; do
|
||||
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
|
||||
if [[ "${event_status}" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
debug -n "."
|
||||
sleep 10
|
||||
done
|
||||
debug ""
|
||||
}
|
||||
|
||||
function power_on_droplet() {
|
||||
local droplet_id="$1"
|
||||
local data='{"type":"power_on"}'
|
||||
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions" | $JSON action.id`
|
||||
|
||||
debug "Powered on droplet. Event id: ${event_id}"
|
||||
|
||||
if [[ -z "${event_id}" ]]; then
|
||||
debug "Got no event id, assuming already powered on"
|
||||
return
|
||||
fi
|
||||
|
||||
debug -n "Waiting for droplet to power on"
|
||||
|
||||
while true; do
|
||||
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
|
||||
if [[ "${event_status}" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
debug -n "."
|
||||
sleep 10
|
||||
done
|
||||
debug ""
|
||||
}
|
||||
|
||||
function get_image_id() {
|
||||
local snapshot_name="$1"
|
||||
local image_id=""
|
||||
|
||||
if ! response=$($CURL "https://api.digitalocean.com/v2/images?per_page=200"); then
|
||||
echo "Failed to get image listing. ${response}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! image_id=$(echo "$response" \
|
||||
| $JSON images \
|
||||
| $JSON -c "this.name === \"${snapshot_name}\"" 0.id); then
|
||||
echo "Failed to parse curl response: ${response}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -z "${image_id}" ]]; then
|
||||
echo "Failed to get image id of ${snapshot_name}. reponse: ${response}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "${image_id}"
|
||||
}
|
||||
|
||||
function snapshot_droplet() {
|
||||
local droplet_id="$1"
|
||||
local snapshot_name="$2"
|
||||
local data="{\"type\":\"snapshot\",\"name\":\"${snapshot_name}\"}"
|
||||
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions" | $JSON action.id`
|
||||
|
||||
debug "Droplet snapshotted as ${snapshot_name}. Event id: ${event_id}"
|
||||
debug -n "Waiting for snapshot to complete"
|
||||
|
||||
while true; do
|
||||
if ! response=$($CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}"); then
|
||||
echo "Could not get action status. ${response}"
|
||||
continue
|
||||
fi
|
||||
if ! event_status=$(echo "${response}" | $JSON action.status); then
|
||||
echo "Could not parse action.status from response. ${response}"
|
||||
continue
|
||||
fi
|
||||
if [[ "${event_status}" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
debug -n "."
|
||||
sleep 10
|
||||
done
|
||||
debug "! done"
|
||||
|
||||
if ! image_id=$(get_image_id "${snapshot_name}"); then
|
||||
return 1
|
||||
fi
|
||||
echo "${image_id}"
|
||||
}
|
||||
|
||||
function destroy_droplet() {
|
||||
local droplet_id="$1"
|
||||
# TODO: check for 204 status
|
||||
$CURL -X DELETE "https://api.digitalocean.com/v2/droplets/${droplet_id}"
|
||||
debug "Droplet destroyed"
|
||||
debug ""
|
||||
}
|
||||
|
||||
function transfer_image() {
|
||||
local image_id="$1"
|
||||
local region_slug="$2"
|
||||
local data="{\"type\":\"transfer\",\"region\":\"${region_slug}\"}"
|
||||
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/images/${image_id}/actions" | $JSON action.id`
|
||||
echo "${event_id}"
|
||||
}
|
||||
|
||||
function wait_for_image_event() {
|
||||
local image_id="$1"
|
||||
local event_id="$2"
|
||||
|
||||
debug -n "Waiting for ${event_id}"
|
||||
|
||||
while true; do
|
||||
local event_status=`$CURL "https://api.digitalocean.com/v2/images/${image_id}/actions/${event_id}" | $JSON action.status`
|
||||
if [[ "${event_status}" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
debug -n "."
|
||||
sleep 10
|
||||
done
|
||||
debug ""
|
||||
}
|
||||
|
||||
function transfer_image_to_all_regions() {
|
||||
local image_id="$1"
|
||||
|
||||
xfer_events=()
|
||||
image_regions=(ams2) ## sfo1 is where the image is created
|
||||
for image_region in ${image_regions[@]}; do
|
||||
xfer_event=$(transfer_image ${image_id} ${image_region})
|
||||
echo "Image transfer to ${image_region} initiated. Event id: ${xfer_event}"
|
||||
xfer_events+=("${xfer_event}")
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Image transfer initiated, but they will take some time to get transferred."
|
||||
|
||||
for xfer_event in ${xfer_events[@]}; do
|
||||
$vps wait_for_image_event "${image_id}" "${xfer_event}"
|
||||
done
|
||||
}
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
debug "<command> <params...>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case $1 in
|
||||
get_ssh_key_id)
|
||||
get_ssh_key_id "${@:2}"
|
||||
;;
|
||||
|
||||
create)
|
||||
create_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
get_id)
|
||||
get_droplet_id "${@:2}"
|
||||
;;
|
||||
|
||||
get_ip)
|
||||
get_droplet_ip "${@:2}"
|
||||
;;
|
||||
|
||||
power_on)
|
||||
power_on_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
power_off)
|
||||
power_off_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
snapshot)
|
||||
snapshot_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
destroy)
|
||||
destroy_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
transfer_image_to_all_regions)
|
||||
transfer_image_to_all_regions "${@:2}"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown command $1"
|
||||
exit 1
|
||||
esac
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euv -o pipefail
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
readonly arg_provider="${1:-generic}"
|
||||
readonly arg_infraversionpath="${SOURCE_DIR}/${2:-}"
|
||||
|
||||
function die {
|
||||
echo $1
|
||||
exit 1
|
||||
}
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
apt-get -o Dpkg::Options::="--force-confdef" update -y
|
||||
apt-get -o Dpkg::Options::="--force-confdef" dist-upgrade -y
|
||||
|
||||
echo "==> Installing required packages"
|
||||
|
||||
debconf-set-selections <<< 'mysql-server mysql-server/root_password password password'
|
||||
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
|
||||
|
||||
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
|
||||
apt-get -y install \
|
||||
acl \
|
||||
awscli \
|
||||
btrfs-tools \
|
||||
build-essential \
|
||||
cron \
|
||||
curl \
|
||||
dmsetup \
|
||||
iptables \
|
||||
logrotate \
|
||||
mysql-server-5.7 \
|
||||
nginx-full \
|
||||
openssh-server \
|
||||
pwgen \
|
||||
rcconf \
|
||||
swaks \
|
||||
unattended-upgrades \
|
||||
unbound
|
||||
|
||||
echo "==> Installing node.js"
|
||||
mkdir -p /usr/local/node-6.9.2
|
||||
curl -sL https://nodejs.org/dist/v6.9.2/node-v6.9.2-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.9.2
|
||||
ln -sf /usr/local/node-6.9.2/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-6.9.2/bin/npm /usr/bin/npm
|
||||
apt-get install -y python # Install python which is required for npm rebuild
|
||||
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
|
||||
|
||||
# https://docs.docker.com/engine/installation/linux/ubuntulinux/
|
||||
echo "==> Installing Docker"
|
||||
docker_key="-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v1
|
||||
|
||||
mQINBFWln24BEADrBl5p99uKh8+rpvqJ48u4eTtjeXAWbslJotmC/CakbNSqOb9o
|
||||
ddfzRvGVeJVERt/Q/mlvEqgnyTQy+e6oEYN2Y2kqXceUhXagThnqCoxcEJ3+KM4R
|
||||
mYdoe/BJ/J/6rHOjq7Omk24z2qB3RU1uAv57iY5VGw5p45uZB4C4pNNsBJXoCvPn
|
||||
TGAs/7IrekFZDDgVraPx/hdiwopQ8NltSfZCyu/jPpWFK28TR8yfVlzYFwibj5WK
|
||||
dHM7ZTqlA1tHIG+agyPf3Rae0jPMsHR6q+arXVwMccyOi+ULU0z8mHUJ3iEMIrpT
|
||||
X+80KaN/ZjibfsBOCjcfiJSB/acn4nxQQgNZigna32velafhQivsNREFeJpzENiG
|
||||
HOoyC6qVeOgKrRiKxzymj0FIMLru/iFF5pSWcBQB7PYlt8J0G80lAcPr6VCiN+4c
|
||||
NKv03SdvA69dCOj79PuO9IIvQsJXsSq96HB+TeEmmL+xSdpGtGdCJHHM1fDeCqkZ
|
||||
hT+RtBGQL2SEdWjxbF43oQopocT8cHvyX6Zaltn0svoGs+wX3Z/H6/8P5anog43U
|
||||
65c0A+64Jj00rNDr8j31izhtQMRo892kGeQAaaxg4Pz6HnS7hRC+cOMHUU4HA7iM
|
||||
zHrouAdYeTZeZEQOA7SxtCME9ZnGwe2grxPXh/U/80WJGkzLFNcTKdv+rwARAQAB
|
||||
tDdEb2NrZXIgUmVsZWFzZSBUb29sIChyZWxlYXNlZG9ja2VyKSA8ZG9ja2VyQGRv
|
||||
Y2tlci5jb20+iQI4BBMBAgAiBQJVpZ9uAhsvBgsJCAcDAgYVCAIJCgsEFgIDAQIe
|
||||
AQIXgAAKCRD3YiFXLFJgnbRfEAC9Uai7Rv20QIDlDogRzd+Vebg4ahyoUdj0CH+n
|
||||
Ak40RIoq6G26u1e+sdgjpCa8jF6vrx+smpgd1HeJdmpahUX0XN3X9f9qU9oj9A4I
|
||||
1WDalRWJh+tP5WNv2ySy6AwcP9QnjuBMRTnTK27pk1sEMg9oJHK5p+ts8hlSC4Sl
|
||||
uyMKH5NMVy9c+A9yqq9NF6M6d6/ehKfBFFLG9BX+XLBATvf1ZemGVHQusCQebTGv
|
||||
0C0V9yqtdPdRWVIEhHxyNHATaVYOafTj/EF0lDxLl6zDT6trRV5n9F1VCEh4Aal8
|
||||
L5MxVPcIZVO7NHT2EkQgn8CvWjV3oKl2GopZF8V4XdJRl90U/WDv/6cmfI08GkzD
|
||||
YBHhS8ULWRFwGKobsSTyIvnbk4NtKdnTGyTJCQ8+6i52s+C54PiNgfj2ieNn6oOR
|
||||
7d+bNCcG1CdOYY+ZXVOcsjl73UYvtJrO0Rl/NpYERkZ5d/tzw4jZ6FCXgggA/Zxc
|
||||
jk6Y1ZvIm8Mt8wLRFH9Nww+FVsCtaCXJLP8DlJLASMD9rl5QS9Ku3u7ZNrr5HWXP
|
||||
HXITX660jglyshch6CWeiUATqjIAzkEQom/kEnOrvJAtkypRJ59vYQOedZ1sFVEL
|
||||
MXg2UCkD/FwojfnVtjzYaTCeGwFQeqzHmM241iuOmBYPeyTY5veF49aBJA1gEJOQ
|
||||
TvBR8Q==
|
||||
=Fm3p
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
"
|
||||
echo "$docker_key" | apt-key add -
|
||||
echo "deb https://apt.dockerproject.org/repo ubuntu-xenial main" > /etc/apt/sources.list.d/docker.list
|
||||
apt-get -y update
|
||||
|
||||
# create systemd drop-in file
|
||||
mkdir -p /etc/systemd/system/docker.service.d
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=devicemapper" > /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
|
||||
apt-get -y --allow-downgrades install docker-engine=1.12.5-0~ubuntu-xenial # apt-cache madison docker-engine
|
||||
apt-mark hold docker-engine # do not update docker
|
||||
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
|
||||
if [[ "${storage_driver}" != "devicemapper" ]]; then
|
||||
echo "Docker is using "${storage_driver}" instead of devicemapper"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Enable memory accounting"
|
||||
apt-get -y install grub2
|
||||
sed -e 's/^GRUB_CMDLINE_LINUX="\(.*\)"$/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
|
||||
update-grub
|
||||
|
||||
echo "==> Downloading docker images"
|
||||
if [ ! -f "${arg_infraversionpath}/infra_version.js" ]; then
|
||||
echo "No infra_versions.js found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
|
||||
|
||||
echo -e "\tPulling docker images: ${images}"
|
||||
for image in ${images}; do
|
||||
docker pull "${image}"
|
||||
done
|
||||
|
||||
echo "==> Install collectd"
|
||||
if ! apt-get install -y collectd collectd-utils; then
|
||||
# FQDNLookup is true in default debian config. The box code has a custom collectd.conf that fixes this
|
||||
echo "Failed to install collectd. Presumably because of http://mailman.verplant.org/pipermail/collectd/2015-March/006491.html"
|
||||
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
|
||||
fi
|
||||
|
||||
@@ -2,82 +2,55 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs'),
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs(args) {
|
||||
args[0] = this.namespace + ' ' + args[0];
|
||||
};
|
||||
|
||||
var appHealthMonitor = require('./src/apphealthmonitor.js'),
|
||||
async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
oidc = require('./src/oidc.js'),
|
||||
paths = require('./src/paths.js'),
|
||||
proxyAuth = require('./src/proxyauth.js'),
|
||||
safe = require('safetydance'),
|
||||
server = require('./src/server.js'),
|
||||
directoryServer = require('./src/directoryserver.js');
|
||||
server = require('./src/server.js');
|
||||
|
||||
let logFd;
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
console.log(' Cloudron will use the following settings ');
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
console.log(' Environment: ', config.CLOUDRON ? 'CLOUDRON' : 'TEST');
|
||||
console.log(' Version: ', config.version());
|
||||
console.log(' Admin Origin: ', config.adminOrigin());
|
||||
console.log(' Appstore API server origin: ', config.apiServerOrigin());
|
||||
console.log(' Appstore Web server origin: ', config.webServerOrigin());
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
|
||||
async function setupLogging() {
|
||||
if (process.env.BOX_ENV === 'test') return;
|
||||
async.series([
|
||||
server.start,
|
||||
ldap.start,
|
||||
appHealthMonitor.start,
|
||||
], function (error) {
|
||||
if (error) {
|
||||
console.error('Error starting server', error);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Cloudron is up and running');
|
||||
});
|
||||
|
||||
logFd = fs.openSync(paths.BOX_LOG_FILE, 'a');
|
||||
// we used to write using a stream before but it caches internally and there is no way to flush it when things crash
|
||||
process.stdout.write = process.stderr.write = function (...args) {
|
||||
const callback = typeof args[args.length-1] === 'function' ? args.pop() : function () {}; // callback is required for fs.write
|
||||
fs.write.apply(fs, [logFd, ...args, callback]);
|
||||
};
|
||||
}
|
||||
var NOOP_CALLBACK = function () { };
|
||||
|
||||
// this is also used as the 'uncaughtException' handler which can only have synchronous functions
|
||||
function exitSync(status) {
|
||||
if (status.error) fs.write(logFd, status.error.stack + '\n', function () {});
|
||||
fs.fsyncSync(logFd);
|
||||
fs.closeSync(logFd);
|
||||
process.exit(status.code);
|
||||
}
|
||||
process.on('SIGINT', function () {
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
async function startServers() {
|
||||
await setupLogging();
|
||||
await server.start(); // do this first since it also inits the database
|
||||
await proxyAuth.start();
|
||||
await ldap.start();
|
||||
|
||||
const conf = await directoryServer.getConfig();
|
||||
if (conf.enabled) await directoryServer.start();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [error] = await safe(startServers());
|
||||
if (error) return exitSync({ error: new Error(`Error starting server: ${JSON.stringify(error)}`), code: 1 });
|
||||
|
||||
// require this here so that logging handler is already setup
|
||||
const debug = require('debug')('box:box');
|
||||
|
||||
process.on('SIGHUP', async function () {
|
||||
debug('Received SIGHUP. Re-reading configs.');
|
||||
const conf = await directoryServer.getConfig();
|
||||
if (conf.enabled) await directoryServer.checkCertificate();
|
||||
});
|
||||
|
||||
process.on('SIGINT', async function () {
|
||||
debug('Received SIGINT. Shutting down.');
|
||||
|
||||
await proxyAuth.stop();
|
||||
await server.stop();
|
||||
await directoryServer.stop();
|
||||
await ldap.stop();
|
||||
await oidc.stop();
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async function () {
|
||||
debug('Received SIGTERM. Shutting down.');
|
||||
|
||||
await proxyAuth.stop();
|
||||
await server.stop();
|
||||
await directoryServer.stop();
|
||||
await ldap.stop();
|
||||
await oidc.stop();
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => exitSync({ error, code: 1 }));
|
||||
}
|
||||
|
||||
main();
|
||||
process.on('SIGTERM', function () {
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
|
||||
|
||||
function main() {
|
||||
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
|
||||
|
||||
var processName = process.argv[2];
|
||||
console.log('Started crash notifier for', processName);
|
||||
|
||||
sendFailureLogs(processName, { unit: processName });
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,6 +0,0 @@
|
||||
# following files are skipped when exporting using git archive
|
||||
test export-ignore
|
||||
docs export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
dist/
|
||||
node_modules/
|
||||
|
||||
# vim swap files
|
||||
*.swp
|
||||
|
||||
# these are not done yet
|
||||
src/translation/ja.json
|
||||
src/translation/pl.json
|
||||
src/translation/si.json
|
||||
src/translation/gl.json
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"unused": true,
|
||||
"esversion": 6,
|
||||
"globalstrict": false,
|
||||
"predef": [
|
||||
"$",
|
||||
"angular",
|
||||
"async",
|
||||
"describe",
|
||||
"it",
|
||||
"before",
|
||||
"after",
|
||||
"require",
|
||||
"monaco",
|
||||
"Mimer",
|
||||
"ISTATES"
|
||||
]
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
The Cloudron Subscription license
|
||||
Copyright (c) 2022 Cloudron UG
|
||||
|
||||
With regard to the Cloudron Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Cloudron Subscription Terms of Service, available
|
||||
at https://cloudron.io/legal/terms.html (the “Subscription Terms”), or other
|
||||
agreement governing the use of the Software, as agreed by you and Cloudron,
|
||||
and otherwise have a valid Cloudron Subscription. Subject to the foregoing sentence,
|
||||
you are free to modify this Software and publish patches to the Software. You agree
|
||||
that Subscription and/or its licensors (as applicable) retain all right, title and
|
||||
interest in and to all such modifications and/or patches, and all such modifications
|
||||
and/or patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Cloudron subscription. Notwithstanding the foregoing, you may copy
|
||||
and modify the Software for development and testing purposes, without requiring a
|
||||
subscription. You agree that Cloudron and/or its licensors (as applicable) retain
|
||||
all right, title and interest in and to all such modifications. You are not
|
||||
granted any other rights beyond what is expressly stated herein. Subject to the
|
||||
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Cloudron Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# Cloudron Dashboard
|
||||
|
||||
This is the front end code of Cloudron. The backend code is [here](https://git.cloudron.io/cloudron/box).
|
||||
|
||||
## Developing
|
||||
|
||||
* `npm install`
|
||||
* `gulp develop --api-origin=https://my.example.com`
|
||||
|
||||
## License
|
||||
|
||||
Please note that the Cloudron code is under a source-available license. This is not the same as an
|
||||
open source license but ensures the code is available for inspection (and hacking!).
|
||||
|
||||
## Contributions
|
||||
|
||||
Just to give a heads-up, we are a bit restrictive in merging changes. We are a small team and
|
||||
would like to keep our maintenance burden low. It might be best to first discuss features in the [forum](https://forum.cloudron.io),
|
||||
which also helps to determine how many other people will use it to justify maintenance for a feature.
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
const argv = require('yargs').argv,
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
concat = require('gulp-concat'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
ejs = require('gulp-ejs'),
|
||||
execSync = require('child_process').execSync,
|
||||
fs = require('fs'),
|
||||
gulp = require('gulp'),
|
||||
sass = require('gulp-sass')(require('sass')),
|
||||
serve = require('gulp-serve'),
|
||||
sourcemaps = require('gulp-sourcemaps');
|
||||
|
||||
if (argv.help || argv.h) {
|
||||
console.log('Supported arguments for "gulp develop":');
|
||||
console.log(' --api-origin <cloudron api uri>');
|
||||
console.log(' --revision <revision>');
|
||||
console.log(' --appstore-console-origin <appstore console uri>');
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const revision = argv.revision || '';
|
||||
|
||||
let apiOrigin = '';
|
||||
if (argv.apiOrigin) {
|
||||
if (argv.apiOrigin.indexOf('https://') === 0) apiOrigin = argv.apiOrigin;
|
||||
else apiOrigin = 'https://' + argv.apiOrigin;
|
||||
}
|
||||
|
||||
var appstore = {
|
||||
consoleOrigin: argv.appstoreConsoleOrigin || ''
|
||||
};
|
||||
|
||||
console.log();
|
||||
console.log('Cloudron API: %s', apiOrigin || 'default');
|
||||
console.log('Building for revision: %s', revision);
|
||||
console.log();
|
||||
console.log('Overriding appstore origin:');
|
||||
console.log(' Console: %s', appstore.consoleOrigin || 'no');
|
||||
console.log();
|
||||
|
||||
gulp.task('fontawesome', function () {
|
||||
return gulp.src('node_modules/@fortawesome/fontawesome-free/**/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/fontawesome/'));
|
||||
});
|
||||
|
||||
gulp.task('bootstrap', function () {
|
||||
return gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
|
||||
.pipe(gulp.dest('dist/3rdparty/js'));
|
||||
});
|
||||
|
||||
gulp.task('moment', function () {
|
||||
return gulp.src('node_modules/moment/min/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/js'));
|
||||
});
|
||||
|
||||
gulp.task('3rdparty-copy', function () {
|
||||
return gulp.src([
|
||||
'src/3rdparty/**/*.js',
|
||||
'src/3rdparty/**/*.map',
|
||||
'src/3rdparty/**/*.css',
|
||||
'src/3rdparty/**/*.otf',
|
||||
'src/3rdparty/**/*.eot',
|
||||
'src/3rdparty/**/*.svg',
|
||||
'src/3rdparty/**/*.gif',
|
||||
'src/3rdparty/**/*.ttf',
|
||||
'node_modules/chart.js/dist/chart.umd.js'
|
||||
]).pipe(gulp.dest('dist/3rdparty/'));
|
||||
});
|
||||
|
||||
gulp.task('3rdparty', gulp.series(['3rdparty-copy', 'moment', 'bootstrap', 'fontawesome']));
|
||||
|
||||
// --------------
|
||||
// JavaScript
|
||||
// --------------
|
||||
|
||||
gulp.task('js-index', function () {
|
||||
return gulp.src([
|
||||
'src/js/index.js',
|
||||
'src/js/client.js',
|
||||
'src/js/utils.js',
|
||||
'src/views/*.js'
|
||||
])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('index.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-passwordreset', function () {
|
||||
return gulp.src(['src/js/passwordreset.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('passwordreset.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-setupaccount', function () {
|
||||
return gulp.src(['src/js/setupaccount.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setupaccount.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-setup', function () {
|
||||
return gulp.src(['src/js/setup.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setup.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-setupdns', function () {
|
||||
return gulp.src(['src/js/setupdns.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setupdns.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-restore', function () {
|
||||
return gulp.src(['src/js/restore.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('restore.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js', gulp.series([ 'js-index', 'js-passwordreset', 'js-setupaccount', 'js-setup', 'js-setupdns', 'js-restore' ]));
|
||||
|
||||
// --------------
|
||||
// HTML
|
||||
// --------------
|
||||
|
||||
gulp.task('html-views', function () {
|
||||
return gulp.src('src/views/**/*.html').pipe(gulp.dest('dist/views'));
|
||||
});
|
||||
|
||||
gulp.task('html-templates', function () {
|
||||
return gulp.src('src/templates/**/*').pipe(gulp.dest('dist/templates'));
|
||||
});
|
||||
|
||||
gulp.task('html-raw', function () {
|
||||
return gulp.src('src/*.html').pipe(ejs({ apiOrigin: apiOrigin, revision: revision }, {}, { ext: '.html' })).pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('html', gulp.series(['html-views', 'html-templates', 'html-raw']));
|
||||
|
||||
// --------------
|
||||
// CSS
|
||||
// --------------
|
||||
|
||||
gulp.task('css', function () {
|
||||
return gulp.src('src/*.scss')
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
|
||||
.pipe(autoprefixer())
|
||||
.pipe(cssnano())
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('images', function () {
|
||||
return gulp.src('src/img/**')
|
||||
.pipe(gulp.dest('dist/img'));
|
||||
});
|
||||
|
||||
gulp.task('translation', function () {
|
||||
return gulp.src('src/translation/**')
|
||||
.pipe(gulp.dest('dist/translation'));
|
||||
});
|
||||
|
||||
gulp.task('timezones', function (done) {
|
||||
execSync('./scripts/createTimezones.js ./dist/js/timezones.js');
|
||||
done();
|
||||
});
|
||||
|
||||
// --------------
|
||||
// Utilities
|
||||
// --------------
|
||||
|
||||
gulp.task('clean', function (done) {
|
||||
fs.rm('dist', { recursive: true, force: true }, done);
|
||||
});
|
||||
|
||||
gulp.task('default', gulp.series(['clean', 'html', 'js', 'timezones', '3rdparty', 'translation', 'images', 'css']));
|
||||
|
||||
gulp.task('watch', function (done) {
|
||||
gulp.watch(['src/*.scss'], gulp.series(['css']));
|
||||
gulp.watch(['src/img/*'], gulp.series(['images']));
|
||||
gulp.watch(['src/translation/*'], gulp.series(['translation']));
|
||||
gulp.watch(['src/**/*.html'], gulp.series(['html']));
|
||||
gulp.watch(['src/views/*.html'], gulp.series(['html-views']));
|
||||
gulp.watch(['src/templates/*.html'], gulp.series(['html-templates']));
|
||||
gulp.watch(['scripts/createTimezones.js', 'src/js/utils.js'], gulp.series(['timezones']));
|
||||
gulp.watch(['src/js/setup.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-setup']));
|
||||
gulp.watch(['src/js/setupdns.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-setupdns']));
|
||||
gulp.watch(['src/js/restore.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-restore']));
|
||||
gulp.watch(['src/js/passwordreset.js', 'src/js/utils.js'], gulp.series(['js-passwordreset']));
|
||||
gulp.watch(['src/js/setupaccount.js', 'src/js/utils.js'], gulp.series(['js-setupaccount']));
|
||||
gulp.watch(['src/js/index.js', 'src/js/client.js', 'src/views/*.js', 'src/js/utils.js'], gulp.series(['js-index']));
|
||||
gulp.watch(['src/3rdparty/**/*'], gulp.series(['3rdparty']));
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('serve', serve({ root: 'dist', port: 4000, hostname: '0.0.0.0' }));
|
||||
|
||||
gulp.task('develop', gulp.series(['default', 'watch', 'serve']));
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "dashboard",
|
||||
"version": "1.0.0",
|
||||
"description": "[Cloudron](https://cloudron.io) is the best way to run apps on your server.",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"update-translations": "curl https://translate.cloudron.io/api/components/cloudron/dashboard/file/ -o lang.zip && unzip -jo lang.zip -d ./src/translation/ && rm lang.zip"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://git@git.cloudron.io:6000/cloudron/dashboard.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"bootstrap-sass": "^3.4.3",
|
||||
"chart.js": "^4.3.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-autoprefixer": "^8.0.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-cssnano": "^2.1.3",
|
||||
"gulp-ejs": "^5.1.0",
|
||||
"gulp-sass": "^5.1.0",
|
||||
"gulp-serve": "^1.4.0",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"sass": "^1.63.3",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
"browser": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// This script creates many users to test the UI for such a case
|
||||
|
||||
// WARNING keep those in sync with delUsers.js
|
||||
const USERNAME_PREFIX = 'manyuser';
|
||||
const PASSOWRD_PREFIX = 'password';
|
||||
const DISPLAYNAME_PREFIX = 'User ';
|
||||
const EMAIL_DOMAIN = 'example.com'; // addresses will be username@EMAIL_DOMAIN
|
||||
const COUNT = 100;
|
||||
|
||||
var async = require('async'),
|
||||
readlineSync = require('readline-sync'),
|
||||
superagent = require('superagent');
|
||||
|
||||
if (process.argv.length !== 3) {
|
||||
console.log('Usage: ./addUsers.js <cloudronDomain>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cloudronDomain = process.argv[2];
|
||||
|
||||
function getAccessToken(callback) {
|
||||
let username = readlineSync.question('Username: ', {});
|
||||
let password = readlineSync.question('Password: ', { noEchoBack: true });
|
||||
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/auth/login`, { username: username, password: password }).end(function (error, result) {
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.log('Login failed');
|
||||
return getAccessToken(callback);
|
||||
}
|
||||
|
||||
callback(result.body.accessToken);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Login to ${cloudronDomain}`);
|
||||
getAccessToken(function (accessToken) {
|
||||
console.log(`Now creating ${COUNT} users...`);
|
||||
|
||||
async.timesLimit(COUNT, 5, function (n, next) {
|
||||
let user = {
|
||||
username: USERNAME_PREFIX + n,
|
||||
password: PASSOWRD_PREFIX + n,
|
||||
email: USERNAME_PREFIX + n + '@' + EMAIL_DOMAIN,
|
||||
displayName: DISPLAYNAME_PREFIX + n
|
||||
};
|
||||
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/users`, user).query({ access_token: accessToken }).end(function (error) {
|
||||
if (error) return next(error);
|
||||
|
||||
process.stdout.write('.');
|
||||
|
||||
next();
|
||||
});
|
||||
}, function (error) {
|
||||
console.log();
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Done');
|
||||
});
|
||||
});
|
||||
@@ -1,387 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// This script creates a specific timezones.js to be consumed by the dashboard
|
||||
|
||||
var execSync = require('child_process').execSync,
|
||||
fs = require('fs'),
|
||||
path = require('path');
|
||||
|
||||
if (process.argv.length !== 3) {
|
||||
console.log('Usage: createTimezones.js <output.js>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const destinationFilePath = path.resolve(process.argv[2]);
|
||||
console.log('Creating timezone info at:', destinationFilePath);
|
||||
|
||||
var ubuntuTimezones = execSync('timedatectl list-timezones --no-pager').toString().split('\n').filter(function (t) { return !!t; });
|
||||
|
||||
// from https://github.com/dmfilipenko/timezones.json/blob/master/timezones.json
|
||||
var details = [
|
||||
{ name: 'UTC', offset: '+00:00' },
|
||||
{ name: 'Africa/Abidjan', offset: '+00:00' },
|
||||
{ name: 'Africa/Accra', offset: '+00:00' },
|
||||
{ name: 'Africa/Algiers', offset: '+01:00' },
|
||||
{ name: 'Africa/Bissau', offset: '+00:00' },
|
||||
{ name: 'Africa/Cairo', offset: '+02:00' },
|
||||
{ name: 'Africa/Casablanca', offset: '+01:00' },
|
||||
{ name: 'Africa/Ceuta', offset: '+01:00' },
|
||||
{ name: 'Africa/El_Aaiun', offset: '+00:00' },
|
||||
{ name: 'Africa/Johannesburg', offset: '+02:00' },
|
||||
{ name: 'Africa/Juba', offset: '+03:00' },
|
||||
{ name: 'Africa/Khartoum', offset: '+02:00' },
|
||||
{ name: 'Africa/Lagos', offset: '+01:00' },
|
||||
{ name: 'Africa/Maputo', offset: '+02:00' },
|
||||
{ name: 'Africa/Monrovia', offset: '+00:00' },
|
||||
{ name: 'Africa/Nairobi', offset: '+03:00' },
|
||||
{ name: 'Africa/Ndjamena', offset: '+01:00' },
|
||||
{ name: 'Africa/Tripoli', offset: '+02:00' },
|
||||
{ name: 'Africa/Tunis', offset: '+01:00' },
|
||||
{ name: 'Africa/Windhoek', offset: '+02:00' },
|
||||
{ name: 'America/Adak', offset: '−10:00' },
|
||||
{ name: 'America/Anchorage', offset: '−09:00' },
|
||||
{ name: 'America/Araguaina', offset: '−03:00' },
|
||||
{ name: 'America/Argentina/Buenos_Aires', offset: '−03:00' },
|
||||
{ name: 'America/Argentina/Catamarca', offset: '−03:00' },
|
||||
{ name: 'America/Argentina/Cordoba', offset: '−03:00' },
|
||||
{ name: 'America/Argentina/Jujuy', offset: '−03:00' },
|
||||
{ name: 'America/Argentina/La_Rioja', offset: '−03:00' },
|
||||
{ name: 'America/Argentina/Mendoza', offset: '−03:00' },
|
||||
{ name: 'America/Argentina/Rio_Gallegos', offset: '−03:00' },
|
||||
{ name: 'America/Argentina/Salta', offset: '−03:00' },
|
||||
{ name: 'America/Argentina/San_Juan', offset: '−03:00' },
|
||||
{ name: 'America/Argentina/San_Luis', offset: '−03:00' },
|
||||
{ name: 'America/Argentina/Tucuman', offset: '−03:00' },
|
||||
{ name: 'America/Argentina/Ushuaia', offset: '−03:00' },
|
||||
{ name: 'America/Asuncion', offset: '−04:00' },
|
||||
{ name: 'America/Atikokan', offset: '−05:00' },
|
||||
{ name: 'America/Bahia', offset: '−03:00' },
|
||||
{ name: 'America/Bahia_Banderas', offset: '−06:00' },
|
||||
{ name: 'America/Barbados', offset: '−04:00' },
|
||||
{ name: 'America/Belem', offset: '−03:00' },
|
||||
{ name: 'America/Belize', offset: '−06:00' },
|
||||
{ name: 'America/Blanc-Sablon', offset: '−04:00' },
|
||||
{ name: 'America/Boa_Vista', offset: '−04:00' },
|
||||
{ name: 'America/Bogota', offset: '−05:00' },
|
||||
{ name: 'America/Boise', offset: '−07:00' },
|
||||
{ name: 'America/Cambridge_Bay', offset: '−07:00' },
|
||||
{ name: 'America/Campo_Grande', offset: '−04:00' },
|
||||
{ name: 'America/Cancun', offset: '−05:00' },
|
||||
{ name: 'America/Caracas', offset: '−04:00' },
|
||||
{ name: 'America/Cayenne', offset: '−03:00' },
|
||||
{ name: 'America/Chicago', offset: '−06:00' },
|
||||
{ name: 'America/Chihuahua', offset: '−07:00' },
|
||||
{ name: 'America/Costa_Rica', offset: '−06:00' },
|
||||
{ name: 'America/Creston', offset: '−07:00' },
|
||||
{ name: 'America/Cuiaba', offset: '−04:00' },
|
||||
{ name: 'America/Curacao', offset: '−04:00' },
|
||||
{ name: 'America/Danmarkshavn', offset: '+00:00' },
|
||||
{ name: 'America/Dawson', offset: '−08:00' },
|
||||
{ name: 'America/Dawson_Creek', offset: '−07:00' },
|
||||
{ name: 'America/Denver', offset: '−07:00' },
|
||||
{ name: 'America/Detroit', offset: '−05:00' },
|
||||
{ name: 'America/Edmonton', offset: '−07:00' },
|
||||
{ name: 'America/Eirunepe', offset: '−05:00' },
|
||||
{ name: 'America/El_Salvador', offset: '−06:00' },
|
||||
{ name: 'America/Fort_Nelson', offset: '−07:00' },
|
||||
{ name: 'America/Fortaleza', offset: '−03:00' },
|
||||
{ name: 'America/Glace_Bay', offset: '−04:00' },
|
||||
{ name: 'America/Godthab', offset: '−03:00' },
|
||||
{ name: 'America/Goose_Bay', offset: '−04:00' },
|
||||
{ name: 'America/Grand_Turk', offset: '−05:00' },
|
||||
{ name: 'America/Guatemala', offset: '−06:00' },
|
||||
{ name: 'America/Guayaquil', offset: '−05:00' },
|
||||
{ name: 'America/Guyana', offset: '−04:00' },
|
||||
{ name: 'America/Halifax', offset: '−04:00' },
|
||||
{ name: 'America/Havana', offset: '−05:00' },
|
||||
{ name: 'America/Hermosillo', offset: '−07:00' },
|
||||
{ name: 'America/Indiana/Indianapolis', offset: '−05:00' },
|
||||
{ name: 'America/Indiana/Knox', offset: '−06:00' },
|
||||
{ name: 'America/Indiana/Marengo', offset: '−05:00' },
|
||||
{ name: 'America/Indiana/Petersburg', offset: '−05:00' },
|
||||
{ name: 'America/Indiana/Tell_City', offset: '−06:00' },
|
||||
{ name: 'America/Indiana/Vevay', offset: '−05:00' },
|
||||
{ name: 'America/Indiana/Vincennes', offset: '−05:00' },
|
||||
{ name: 'America/Indiana/Winamac', offset: '−05:00' },
|
||||
{ name: 'America/Inuvik', offset: '−07:00' },
|
||||
{ name: 'America/Iqaluit', offset: '−05:00' },
|
||||
{ name: 'America/Jamaica', offset: '−05:00' },
|
||||
{ name: 'America/Juneau', offset: '−09:00' },
|
||||
{ name: 'America/Kentucky/Louisville', offset: '−05:00' },
|
||||
{ name: 'America/Kentucky/Monticello', offset: '−05:00' },
|
||||
{ name: 'America/La_Paz', offset: '−04:00' },
|
||||
{ name: 'America/Lima', offset: '−05:00' },
|
||||
{ name: 'America/Los_Angeles', offset: '−08:00' },
|
||||
{ name: 'America/Maceio', offset: '−03:00' },
|
||||
{ name: 'America/Managua', offset: '−06:00' },
|
||||
{ name: 'America/Manaus', offset: '−04:00' },
|
||||
{ name: 'America/Martinique', offset: '−04:00' },
|
||||
{ name: 'America/Matamoros', offset: '−06:00' },
|
||||
{ name: 'America/Mazatlan', offset: '−07:00' },
|
||||
{ name: 'America/Menominee', offset: '−06:00' },
|
||||
{ name: 'America/Merida', offset: '−06:00' },
|
||||
{ name: 'America/Metlakatla', offset: '−09:00' },
|
||||
{ name: 'America/Mexico_City', offset: '−06:00' },
|
||||
{ name: 'America/Miquelon', offset: '−03:00' },
|
||||
{ name: 'America/Moncton', offset: '−04:00' },
|
||||
{ name: 'America/Monterrey', offset: '−06:00' },
|
||||
{ name: 'America/Montevideo', offset: '−03:00' },
|
||||
{ name: 'America/Nassau', offset: '−05:00' },
|
||||
{ name: 'America/New_York', offset: '−05:00' },
|
||||
{ name: 'America/Nipigon', offset: '−05:00' },
|
||||
{ name: 'America/Nome', offset: '−09:00' },
|
||||
{ name: 'America/Noronha', offset: '−02:00' },
|
||||
{ name: 'America/North_Dakota/Beulah', offset: '−06:00' },
|
||||
{ name: 'America/North_Dakota/Center', offset: '−06:00' },
|
||||
{ name: 'America/North_Dakota/New_Salem', offset: '−06:00' },
|
||||
{ name: 'America/Ojinaga', offset: '−07:00' },
|
||||
{ name: 'America/Panama', offset: '−05:00' },
|
||||
{ name: 'America/Pangnirtung', offset: '−05:00' },
|
||||
{ name: 'America/Paramaribo', offset: '−03:00' },
|
||||
{ name: 'America/Phoenix', offset: '−07:00' },
|
||||
{ name: 'America/Port_of_Spain', offset: '−04:00' },
|
||||
{ name: 'America/Port-au-Prince', offset: '−05:00' },
|
||||
{ name: 'America/Porto_Velho', offset: '−04:00' },
|
||||
{ name: 'America/Puerto_Rico', offset: '−04:00' },
|
||||
{ name: 'America/Punta_Arenas', offset: '−03:00' },
|
||||
{ name: 'America/Rainy_River', offset: '−06:00' },
|
||||
{ name: 'America/Rankin_Inlet', offset: '−06:00' },
|
||||
{ name: 'America/Recife', offset: '−03:00' },
|
||||
{ name: 'America/Regina', offset: '−06:00' },
|
||||
{ name: 'America/Resolute', offset: '−06:00' },
|
||||
{ name: 'America/Rio_Branco', offset: '−05:00' },
|
||||
{ name: 'America/Santarem', offset: '−03:00' },
|
||||
{ name: 'America/Santiago', offset: '−04:00' },
|
||||
{ name: 'America/Santo_Domingo', offset: '−04:00' },
|
||||
{ name: 'America/Sao_Paulo', offset: '−03:00' },
|
||||
{ name: 'America/Scoresbysund', offset: '−01:00' },
|
||||
{ name: 'America/Sitka', offset: '−09:00' },
|
||||
{ name: 'America/St_Johns', offset: '−03:30' },
|
||||
{ name: 'America/Swift_Current', offset: '−06:00' },
|
||||
{ name: 'America/Tegucigalpa', offset: '−06:00' },
|
||||
{ name: 'America/Thule', offset: '−04:00' },
|
||||
{ name: 'America/Thunder_Bay', offset: '−05:00' },
|
||||
{ name: 'America/Tijuana', offset: '−08:00' },
|
||||
{ name: 'America/Toronto', offset: '−05:00' },
|
||||
{ name: 'America/Vancouver', offset: '−08:00' },
|
||||
{ name: 'America/Whitehorse', offset: '−08:00' },
|
||||
{ name: 'America/Winnipeg', offset: '−06:00' },
|
||||
{ name: 'America/Yakutat', offset: '−09:00' },
|
||||
{ name: 'America/Yellowknife', offset: '−07:00' },
|
||||
{ name: 'Antarctica/Casey', offset: '+11:00' },
|
||||
{ name: 'Antarctica/Davis', offset: '+07:00' },
|
||||
{ name: 'Antarctica/DumontDUrville', offset: '+10:00' },
|
||||
{ name: 'Antarctica/Macquarie', offset: '+11:00' },
|
||||
{ name: 'Antarctica/Mawson', offset: '+05:00' },
|
||||
{ name: 'Antarctica/Palmer', offset: '−03:00' },
|
||||
{ name: 'Antarctica/Rothera', offset: '−03:00' },
|
||||
{ name: 'Antarctica/Syowa', offset: '+03:00' },
|
||||
{ name: 'Antarctica/Troll', offset: '+00:00' },
|
||||
{ name: 'Antarctica/Vostok', offset: '+06:00' },
|
||||
{ name: 'Asia/Almaty', offset: '+06:00' },
|
||||
{ name: 'Asia/Amman', offset: '+02:00' },
|
||||
{ name: 'Asia/Anadyr', offset: '+12:00' },
|
||||
{ name: 'Asia/Aqtau', offset: '+05:00' },
|
||||
{ name: 'Asia/Aqtobe', offset: '+05:00' },
|
||||
{ name: 'Asia/Ashgabat', offset: '+05:00' },
|
||||
{ name: 'Asia/Atyrau', offset: '+05:00' },
|
||||
{ name: 'Asia/Baghdad', offset: '+03:00' },
|
||||
{ name: 'Asia/Baku', offset: '+04:00' },
|
||||
{ name: 'Asia/Bangkok', offset: '+07:00' },
|
||||
{ name: 'Asia/Barnaul', offset: '+07:00' },
|
||||
{ name: 'Asia/Beirut', offset: '+02:00' },
|
||||
{ name: 'Asia/Bishkek', offset: '+06:00' },
|
||||
{ name: 'Asia/Brunei', offset: '+08:00' },
|
||||
{ name: 'Asia/Chita', offset: '+09:00' },
|
||||
{ name: 'Asia/Choibalsan', offset: '+08:00' },
|
||||
{ name: 'Asia/Colombo', offset: '+05:30' },
|
||||
{ name: 'Asia/Damascus', offset: '+02:00' },
|
||||
{ name: 'Asia/Dhaka', offset: '+06:00' },
|
||||
{ name: 'Asia/Dili', offset: '+09:00' },
|
||||
{ name: 'Asia/Dubai', offset: '+04:00' },
|
||||
{ name: 'Asia/Dushanbe', offset: '+05:00' },
|
||||
{ name: 'Asia/Famagusta', offset: '+02:00' },
|
||||
{ name: 'Asia/Gaza', offset: '+02:00' },
|
||||
{ name: 'Asia/Hebron', offset: '+02:00' },
|
||||
{ name: 'Asia/Ho_Chi_Minh', offset: '+07:00' },
|
||||
{ name: 'Asia/Hong_Kong', offset: '+08:00' },
|
||||
{ name: 'Asia/Hovd', offset: '+07:00' },
|
||||
{ name: 'Asia/Irkutsk', offset: '+08:00' },
|
||||
{ name: 'Asia/Jakarta', offset: '+07:00' },
|
||||
{ name: 'Asia/Jayapura', offset: '+09:00' },
|
||||
{ name: 'Asia/Jerusalem', offset: '+02:00' },
|
||||
{ name: 'Asia/Kabul', offset: '+04:30' },
|
||||
{ name: 'Asia/Kamchatka', offset: '+12:00' },
|
||||
{ name: 'Asia/Karachi', offset: '+05:00' },
|
||||
{ name: 'Asia/Kathmandu', offset: '+05:45' },
|
||||
{ name: 'Asia/Khandyga', offset: '+09:00' },
|
||||
{ name: 'Asia/Kolkata', offset: '+05:30' },
|
||||
{ name: 'Asia/Krasnoyarsk', offset: '+07:00' },
|
||||
{ name: 'Asia/Kuala_Lumpur', offset: '+08:00' },
|
||||
{ name: 'Asia/Kuching', offset: '+08:00' },
|
||||
{ name: 'Asia/Macau', offset: '+08:00' },
|
||||
{ name: 'Asia/Magadan', offset: '+11:00' },
|
||||
{ name: 'Asia/Makassar', offset: '+08:00' },
|
||||
{ name: 'Asia/Manila', offset: '+08:00' },
|
||||
{ name: 'Asia/Novokuznetsk', offset: '+07:00' },
|
||||
{ name: 'Asia/Novosibirsk', offset: '+07:00' },
|
||||
{ name: 'Asia/Omsk', offset: '+06:00' },
|
||||
{ name: 'Asia/Oral', offset: '+05:00' },
|
||||
{ name: 'Asia/Pontianak', offset: '+07:00' },
|
||||
{ name: 'Asia/Pyongyang', offset: '+09:00' },
|
||||
{ name: 'Asia/Qatar', offset: '+03:00' },
|
||||
{ name: 'Asia/Qyzylorda', offset: '+05:00' },
|
||||
{ name: 'Asia/Riyadh', offset: '+03:00' },
|
||||
{ name: 'Asia/Sakhalin', offset: '+11:00' },
|
||||
{ name: 'Asia/Samarkand', offset: '+05:00' },
|
||||
{ name: 'Asia/Seoul', offset: '+09:00' },
|
||||
{ name: 'Asia/Shanghai', offset: '+08:00' },
|
||||
{ name: 'Asia/Singapore', offset: '+08:00' },
|
||||
{ name: 'Asia/Srednekolymsk', offset: '+11:00' },
|
||||
{ name: 'Asia/Taipei', offset: '+08:00' },
|
||||
{ name: 'Asia/Tashkent', offset: '+05:00' },
|
||||
{ name: 'Asia/Tbilisi', offset: '+04:00' },
|
||||
{ name: 'Asia/Tehran', offset: '+03:30' },
|
||||
{ name: 'Asia/Thimphu', offset: '+06:00' },
|
||||
{ name: 'Asia/Tokyo', offset: '+09:00' },
|
||||
{ name: 'Asia/Tomsk', offset: '+07:00' },
|
||||
{ name: 'Asia/Ulaanbaatar', offset: '+08:00' },
|
||||
{ name: 'Asia/Urumqi', offset: '+06:00' },
|
||||
{ name: 'Asia/Ust-Nera', offset: '+10:00' },
|
||||
{ name: 'Asia/Vladivostok', offset: '+10:00' },
|
||||
{ name: 'Asia/Yakutsk', offset: '+09:00' },
|
||||
{ name: 'Asia/Yangon', offset: '+06:30' },
|
||||
{ name: 'Asia/Yekaterinburg', offset: '+05:00' },
|
||||
{ name: 'Asia/Yerevan', offset: '+04:00' },
|
||||
{ name: 'Atlantic/Azores', offset: '−01:00' },
|
||||
{ name: 'Atlantic/Bermuda', offset: '−04:00' },
|
||||
{ name: 'Atlantic/Canary', offset: '+00:00' },
|
||||
{ name: 'Atlantic/Cape_Verde', offset: '−01:00' },
|
||||
{ name: 'Atlantic/Faroe', offset: '+00:00' },
|
||||
{ name: 'Atlantic/Madeira', offset: '+00:00' },
|
||||
{ name: 'Atlantic/Reykjavik', offset: '+00:00' },
|
||||
{ name: 'Atlantic/South_Georgia', offset: '−02:00' },
|
||||
{ name: 'Atlantic/Stanley', offset: '−03:00' },
|
||||
{ name: 'Australia/Adelaide', offset: '+09:30' },
|
||||
{ name: 'Australia/Brisbane', offset: '+10:00' },
|
||||
{ name: 'Australia/Broken_Hill', offset: '+09:30' },
|
||||
{ name: 'Australia/Currie', offset: '+10:00' },
|
||||
{ name: 'Australia/Darwin', offset: '+09:30' },
|
||||
{ name: 'Australia/Eucla', offset: '+08:45' },
|
||||
{ name: 'Australia/Hobart', offset: '+10:00' },
|
||||
{ name: 'Australia/Lindeman', offset: '+10:00' },
|
||||
{ name: 'Australia/Lord_Howe', offset: '+10:30' },
|
||||
{ name: 'Australia/Melbourne', offset: '+10:00' },
|
||||
{ name: 'Australia/Perth', offset: '+08:00' },
|
||||
{ name: 'Australia/Sydney', offset: '+10:00' },
|
||||
{ name: 'Europe/Amsterdam', offset: '+01:00' },
|
||||
{ name: 'Europe/Andorra', offset: '+01:00' },
|
||||
{ name: 'Europe/Astrakhan', offset: '+04:00' },
|
||||
{ name: 'Europe/Athens', offset: '+02:00' },
|
||||
{ name: 'Europe/Belgrade', offset: '+01:00' },
|
||||
{ name: 'Europe/Berlin', offset: '+01:00' },
|
||||
{ name: 'Europe/Brussels', offset: '+01:00' },
|
||||
{ name: 'Europe/Bucharest', offset: '+02:00' },
|
||||
{ name: 'Europe/Budapest', offset: '+01:00' },
|
||||
{ name: 'Europe/Chisinau', offset: '+02:00' },
|
||||
{ name: 'Europe/Copenhagen', offset: '+01:00' },
|
||||
{ name: 'Europe/Dublin', offset: '+00:00' },
|
||||
{ name: 'Europe/Gibraltar', offset: '+01:00' },
|
||||
{ name: 'Europe/Helsinki', offset: '+02:00' },
|
||||
{ name: 'Europe/Istanbul', offset: '+03:00' },
|
||||
{ name: 'Europe/Kaliningrad', offset: '+02:00' },
|
||||
{ name: 'Europe/Kiev', offset: '+02:00' },
|
||||
{ name: 'Europe/Kirov', offset: '+03:00' },
|
||||
{ name: 'Europe/Lisbon', offset: '+00:00' },
|
||||
{ name: 'Europe/London', offset: '+00:00' },
|
||||
{ name: 'Europe/Luxembourg', offset: '+01:00' },
|
||||
{ name: 'Europe/Madrid', offset: '+01:00' },
|
||||
{ name: 'Europe/Malta', offset: '+01:00' },
|
||||
{ name: 'Europe/Minsk', offset: '+03:00' },
|
||||
{ name: 'Europe/Monaco', offset: '+01:00' },
|
||||
{ name: 'Europe/Moscow', offset: '+03:00' },
|
||||
{ name: 'Asia/Nicosia', offset: '+02:00' },
|
||||
{ name: 'Europe/Oslo', offset: '+01:00' },
|
||||
{ name: 'Europe/Paris', offset: '+01:00' },
|
||||
{ name: 'Europe/Prague', offset: '+01:00' },
|
||||
{ name: 'Europe/Riga', offset: '+02:00' },
|
||||
{ name: 'Europe/Rome', offset: '+01:00' },
|
||||
{ name: 'Europe/Samara', offset: '+04:00' },
|
||||
{ name: 'Europe/Saratov', offset: '+04:00' },
|
||||
{ name: 'Europe/Simferopol', offset: '+03:00' },
|
||||
{ name: 'Europe/Sofia', offset: '+02:00' },
|
||||
{ name: 'Europe/Stockholm', offset: '+01:00' },
|
||||
{ name: 'Europe/Tallinn', offset: '+02:00' },
|
||||
{ name: 'Europe/Tirane', offset: '+01:00' },
|
||||
{ name: 'Europe/Ulyanovsk', offset: '+04:00' },
|
||||
{ name: 'Europe/Uzhgorod', offset: '+02:00' },
|
||||
{ name: 'Europe/Vienna', offset: '+01:00' },
|
||||
{ name: 'Europe/Vilnius', offset: '+02:00' },
|
||||
{ name: 'Europe/Volgograd', offset: '+04:00' },
|
||||
{ name: 'Europe/Warsaw', offset: '+01:00' },
|
||||
{ name: 'Europe/Zaporozhye', offset: '+02:00' },
|
||||
{ name: 'Europe/Zurich', offset: '+01:00' },
|
||||
{ name: 'Indian/Chagos', offset: '+06:00' },
|
||||
{ name: 'Indian/Christmas', offset: '+07:00' },
|
||||
{ name: 'Indian/Cocos', offset: '+06:30' },
|
||||
{ name: 'Indian/Kerguelen', offset: '+05:00' },
|
||||
{ name: 'Indian/Mahe', offset: '+04:00' },
|
||||
{ name: 'Indian/Maldives', offset: '+05:00' },
|
||||
{ name: 'Indian/Mauritius', offset: '+04:00' },
|
||||
{ name: 'Indian/Reunion', offset: '+04:00' },
|
||||
{ name: 'Pacific/Apia', offset: '+13:00' },
|
||||
{ name: 'Pacific/Auckland', offset: '+12:00' },
|
||||
{ name: 'Pacific/Bougainville', offset: '+11:00' },
|
||||
{ name: 'Pacific/Chatham', offset: '+12:45' },
|
||||
{ name: 'Pacific/Chuuk', offset: '+10:00' },
|
||||
{ name: 'Pacific/Easter', offset: '−06:00' },
|
||||
{ name: 'Pacific/Efate', offset: '+11:00' },
|
||||
{ name: 'Pacific/Enderbury', offset: '+13:00' },
|
||||
{ name: 'Pacific/Fakaofo', offset: '+13:00' },
|
||||
{ name: 'Pacific/Fiji', offset: '+12:00' },
|
||||
{ name: 'Pacific/Funafuti', offset: '+12:00' },
|
||||
{ name: 'Pacific/Galapagos', offset: '−06:00' },
|
||||
{ name: 'Pacific/Gambier', offset: '−09:00' },
|
||||
{ name: 'Pacific/Guadalcanal', offset: '+11:00' },
|
||||
{ name: 'Pacific/Guam', offset: '+10:00' },
|
||||
{ name: 'Pacific/Honolulu', offset: '−10:00' },
|
||||
{ name: 'Pacific/Kiritimati', offset: '+14:00' },
|
||||
{ name: 'Pacific/Kosrae', offset: '+11:00' },
|
||||
{ name: 'Pacific/Kwajalein', offset: '+12:00' },
|
||||
{ name: 'Pacific/Majuro', offset: '+12:00' },
|
||||
{ name: 'Pacific/Marquesas', offset: '−09:30' },
|
||||
{ name: 'Pacific/Nauru', offset: '+12:00' },
|
||||
{ name: 'Pacific/Niue', offset: '−11:00' },
|
||||
{ name: 'Pacific/Norfolk', offset: '+11:00' },
|
||||
{ name: 'Pacific/Noumea', offset: '+11:00' },
|
||||
{ name: 'Pacific/Pago_Pago', offset: '−11:00' },
|
||||
{ name: 'Pacific/Palau', offset: '+09:00' },
|
||||
{ name: 'Pacific/Pitcairn', offset: '−08:00' },
|
||||
{ name: 'Pacific/Pohnpei', offset: '+11:00' },
|
||||
{ name: 'Pacific/Port_Moresby', offset: '+10:00' },
|
||||
{ name: 'Pacific/Rarotonga', offset: '−10:00' },
|
||||
{ name: 'Pacific/Tahiti', offset: '−10:00' },
|
||||
{ name: 'Pacific/Tarawa', offset: '+12:00' },
|
||||
{ name: 'Pacific/Tongatapu', offset: '+13:00' },
|
||||
{ name: 'Pacific/Wake', offset: '+12:00' },
|
||||
{ name: 'Pacific/Wallis', offset: '+12:00' },
|
||||
];
|
||||
|
||||
var timezones = ubuntuTimezones.map(function (t) {
|
||||
var detail = details.find(function (d) {
|
||||
return d.name === t;
|
||||
});
|
||||
|
||||
if (!detail) return null;
|
||||
|
||||
return {
|
||||
id: t,
|
||||
display: `${t} (UTC${detail.offset})`
|
||||
}
|
||||
}).filter(function (t) { return !!t; });
|
||||
|
||||
var output = `(function () { window.timezones = ${JSON.stringify(timezones)}; })();\n`;
|
||||
|
||||
fs.writeFileSync(destinationFilePath, output, 'utf-8');
|
||||
|
||||
console.log('Done');
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// This script deletes many users to test the UI for such a case
|
||||
|
||||
// WARNING keep those in sync with addUsers.js
|
||||
const USERNAME_PREFIX = 'manyuser';
|
||||
|
||||
var async = require('async'),
|
||||
readlineSync = require('readline-sync'),
|
||||
superagent = require('superagent');
|
||||
|
||||
if (process.argv.length !== 3) {
|
||||
console.log('Usage: ./delUsers.js <cloudronDomain>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cloudronDomain = process.argv[2];
|
||||
|
||||
function getAccessToken(callback) {
|
||||
let username = readlineSync.question('Username: ', {});
|
||||
let password = readlineSync.question('Password: ', { noEchoBack: true });
|
||||
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/auth/login`, { username: username, password: password }).end(function (error, result) {
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.log('Login failed');
|
||||
return getAccessToken(callback);
|
||||
}
|
||||
|
||||
callback(result.body.accessToken);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Login to ${cloudronDomain}`);
|
||||
getAccessToken(function (accessToken) {
|
||||
console.log('Listing users...');
|
||||
|
||||
superagent.get(`https://${cloudronDomain}/api/v1/users`).query({ access_token: accessToken, per_page: 1000 }).end(function (error, result) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Found ${result.body.users.length} users`);
|
||||
let matchingUsers = result.body.users.filter(function (u) { return u.username.indexOf(USERNAME_PREFIX) === 0; });
|
||||
console.log(`Found ${matchingUsers.length} users with matching prefix`);
|
||||
|
||||
console.log('Deleting users...');
|
||||
|
||||
async.eachLimit(matchingUsers, 5, function (user, next) {
|
||||
superagent.del(`https://${cloudronDomain}/api/v1/users/${user.id}`).query({ access_token: accessToken }).end(function (error) {
|
||||
if (error) return next(error);
|
||||
|
||||
process.stdout.write('.');
|
||||
|
||||
next();
|
||||
});
|
||||
}, function (error) {
|
||||
console.log();
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Done');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,163 +0,0 @@
|
||||
{
|
||||
"name": "scripts",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.1.0.tgz",
|
||||
"integrity": "sha512-4vx/aaY6j/j3Lw3fbCHNWP0pPaTCew3F6F3hYyl/tHs/ndmV1q7NW9T5yuJ2XAGwdQrP+6Wu20x06U4APo/iQQ=="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
|
||||
},
|
||||
"cookiejar": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||
"integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
},
|
||||
"fast-safe-stringify": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
|
||||
"integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
|
||||
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"formidable": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz",
|
||||
"integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg=="
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
},
|
||||
"mime": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
|
||||
"integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
|
||||
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.24",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
|
||||
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
|
||||
"requires": {
|
||||
"mime-db": "1.40.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.0.tgz",
|
||||
"integrity": "sha512-27RP4UotQORTpmNQDX8BHPukOnBP3p1uUJY5UnDhaJB+rMt9iMsok724XL+UHU23bEFOHRMQ2ZhI99qOWUMGFA=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
|
||||
"integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"readline-sync": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
|
||||
"integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
|
||||
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"superagent": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-5.1.0.tgz",
|
||||
"integrity": "sha512-7V6JVx5N+eTL1MMqRBX0v0bG04UjrjAvvZJTF/VDH/SH2GjSLqlrcYepFlpTrXpm37aSY6h3GGVWGxXl/98TKA==",
|
||||
"requires": {
|
||||
"component-emitter": "^1.3.0",
|
||||
"cookiejar": "^2.1.2",
|
||||
"debug": "^4.1.1",
|
||||
"fast-safe-stringify": "^2.0.6",
|
||||
"form-data": "^2.3.3",
|
||||
"formidable": "^1.2.1",
|
||||
"methods": "^1.1.2",
|
||||
"mime": "^2.4.4",
|
||||
"qs": "^6.7.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"semver": "^6.1.1"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "scripts",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "manyUsers.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"async": "^3.1.0",
|
||||
"readline-sync": "^1.4.10",
|
||||
"superagent": "^5.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
|
||||
* @author Alex_Crack
|
||||
* @version v0.3.6
|
||||
* @link https://github.com/alexcrack/angular-ui-notification
|
||||
* @license MIT
|
||||
*/
|
||||
.ui-notification
|
||||
{
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
|
||||
width: 300px;
|
||||
|
||||
-webkit-transition: all ease .5s;
|
||||
-o-transition: all ease .5s;
|
||||
transition: all ease .5s;
|
||||
|
||||
color: #fff;
|
||||
border-radius: 0;
|
||||
background: #337ab7;
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, .3);
|
||||
}
|
||||
.ui-notification.clickable
|
||||
{
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui-notification.clickable:hover
|
||||
{
|
||||
opacity: .7;
|
||||
}
|
||||
.ui-notification.killed
|
||||
{
|
||||
-webkit-transition: opacity ease 1s;
|
||||
-o-transition: opacity ease 1s;
|
||||
transition: opacity ease 1s;
|
||||
|
||||
opacity: 0;
|
||||
}
|
||||
.ui-notification > h3
|
||||
{
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
|
||||
display: block;
|
||||
|
||||
margin: 10px 10px 0 10px;
|
||||
padding: 0 0 5px 0;
|
||||
|
||||
text-align: left;
|
||||
|
||||
border-bottom: 1px solid rgba(255, 255, 255, .3);
|
||||
}
|
||||
.ui-notification a
|
||||
{
|
||||
color: #fff;
|
||||
}
|
||||
.ui-notification a:hover
|
||||
{
|
||||
text-decoration: underline;
|
||||
}
|
||||
.ui-notification > .message
|
||||
{
|
||||
margin: 10px 10px 10px 10px;
|
||||
}
|
||||
.ui-notification.warning
|
||||
{
|
||||
color: #fff;
|
||||
background: #f0ad4e;
|
||||
}
|
||||
.ui-notification.error
|
||||
{
|
||||
color: #fff;
|
||||
background: #d9534f;
|
||||
}
|
||||
.ui-notification.success
|
||||
{
|
||||
color: #fff;
|
||||
background: #5cb85c;
|
||||
}
|
||||
.ui-notification.info
|
||||
{
|
||||
color: #fff;
|
||||
background: #5bc0de;
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// -------------------------------
|
||||
// WARNING
|
||||
// -------------------------------
|
||||
// This file is taken from https://github.com/sebastianha/angular-bootstrap-multiselect
|
||||
// There are local modifications like support for translation
|
||||
// -------------------------------
|
||||
|
||||
angular.module("ui.multiselect", ["multiselect.tpl.html"])
|
||||
//from bootstrap-ui typeahead parser
|
||||
.factory("optionParser", ["$parse", function($parse) {
|
||||
// 00000111000000000000022200000000000000003333333333333330000000000044000
|
||||
var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;
|
||||
|
||||
return {
|
||||
parse: function(input) {
|
||||
|
||||
var match = input.match(TYPEAHEAD_REGEXP);
|
||||
if(!match) {
|
||||
throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" + " but got '" + input + "'.");
|
||||
}
|
||||
|
||||
return {
|
||||
itemName : match[3],
|
||||
source : $parse(match[4]),
|
||||
viewMapper : $parse(match[2] || match[1]),
|
||||
modelMapper: $parse(match[1])
|
||||
};
|
||||
}
|
||||
};
|
||||
}])
|
||||
.directive("multiselect", ["$parse", "$document", "$compile", "$interpolate", "$translate", "optionParser", function($parse, $document, $compile, $interpolate, $translate, optionParser) {
|
||||
return {
|
||||
restrict: "E",
|
||||
require : "ngModel",
|
||||
link : function(originalScope, element, attrs, modelCtrl) {
|
||||
|
||||
var exp = attrs.options;
|
||||
var parsedResult = optionParser.parse(exp);
|
||||
var isMultiple = attrs.multiple ? true : false;
|
||||
var compareByKey = attrs.compareBy;
|
||||
var headerKey = attrs.headerKey;
|
||||
var dividerKey = attrs.dividerKey;
|
||||
var scrollAfterRows = attrs.scrollAfterRows;
|
||||
var tabindex = attrs.tabindex;
|
||||
var maxWidth = attrs.maxWidth;
|
||||
var required = false;
|
||||
var scope = originalScope.$new();
|
||||
scope.filterAfterRows = attrs.filterAfterRows;
|
||||
var changeHandler = attrs.change || angular.noop;
|
||||
|
||||
scope.items = [];
|
||||
scope.header = "Select";
|
||||
scope.multiple = isMultiple;
|
||||
scope.disabled = false;
|
||||
|
||||
scope.ulStyle = {};
|
||||
if(scrollAfterRows !== undefined && parseInt(scrollAfterRows).toString() === scrollAfterRows) {
|
||||
scope.ulStyle = {"max-height": (scrollAfterRows*26+14)+"px", "overflow-y": "auto", "overflow-x": "hidden"};
|
||||
}
|
||||
if(tabindex !== undefined && parseInt(tabindex).toString() === tabindex) {
|
||||
scope.tabindex = tabindex;
|
||||
}
|
||||
if(maxWidth !== undefined && parseInt(maxWidth).toString() === maxWidth) {
|
||||
scope.maxWidth = {"max-width": maxWidth + "px"};
|
||||
}
|
||||
|
||||
originalScope.$on("$destroy", function() {
|
||||
scope.$destroy();
|
||||
});
|
||||
|
||||
var popUpEl = angular.element("<multiselect-popup></multiselect-popup>");
|
||||
|
||||
//required validator
|
||||
if(attrs.required || attrs.ngRequired) {
|
||||
required = true;
|
||||
}
|
||||
attrs.$observe("required", function(newVal) {
|
||||
required = newVal;
|
||||
});
|
||||
|
||||
//watch disabled state
|
||||
scope.$watch(function() {
|
||||
return $parse(attrs.ngDisabled)(originalScope);
|
||||
}, function(newVal) {
|
||||
scope.disabled = newVal;
|
||||
});
|
||||
|
||||
//watch single/multiple state for dynamically change single to multiple
|
||||
scope.$watch(function() {
|
||||
return $parse(attrs.multiple)(originalScope);
|
||||
}, function(newVal) {
|
||||
isMultiple = newVal || false;
|
||||
});
|
||||
|
||||
//watch option changes for options that are populated dynamically
|
||||
scope.$watch(function() {
|
||||
return parsedResult.source(originalScope);
|
||||
}, function(newVal) {
|
||||
if(angular.isDefined(newVal)) {
|
||||
parseModel();
|
||||
}
|
||||
}, true);
|
||||
|
||||
//watch model change
|
||||
scope.$watch(function() {
|
||||
return modelCtrl.$modelValue;
|
||||
}, function(newVal, oldVal) {
|
||||
//when directive initialize, newVal usually undefined. Also, if model value already set in the controller
|
||||
//for preselected list then we need to mark checked in our scope item. But we don't want to do this every time
|
||||
//model changes. We need to do this only if it is done outside directive scope, from controller, for example.
|
||||
if(angular.isDefined(newVal)) {
|
||||
markChecked(newVal);
|
||||
scope.$eval(changeHandler);
|
||||
}
|
||||
getHeaderText();
|
||||
modelCtrl.$setValidity("required", scope.valid());
|
||||
});
|
||||
|
||||
function parseModel() {
|
||||
scope.items.length = 0;
|
||||
var model = parsedResult.source(originalScope);
|
||||
if(!angular.isDefined(model) || model === null) {
|
||||
return;
|
||||
}
|
||||
for(var i = 0; i < model.length; i++) {
|
||||
var local = {};
|
||||
local[parsedResult.itemName] = model[i];
|
||||
|
||||
// calculate checked status of the option
|
||||
// https://github.com/sebastianha/angular-bootstrap-multiselect/pull/4/files
|
||||
var id = model[i];
|
||||
var checked = false;
|
||||
var modelValue = modelCtrl.$modelValue;
|
||||
if (modelValue) {
|
||||
if (angular.isArray(modelValue)) {
|
||||
for (var j = 0; j < modelValue.length; j++)
|
||||
if (modelValue[j] == id) {
|
||||
checked = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
checked = modelValue == id;
|
||||
}
|
||||
}
|
||||
|
||||
scope.items.push({
|
||||
label : parsedResult.viewMapper(local),
|
||||
model : model[i],
|
||||
checked: checked,
|
||||
header : model[i][headerKey],
|
||||
divider : model[i][dividerKey]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
parseModel();
|
||||
|
||||
element.append($compile(popUpEl)(scope));
|
||||
|
||||
function getHeaderText() {
|
||||
if(isEmpty(modelCtrl.$modelValue)) {
|
||||
scope.header = attrs.msHeader || $translate.instant('main.multiselect.select');
|
||||
return scope.header;
|
||||
}
|
||||
|
||||
if(isMultiple) {
|
||||
if(attrs.msSelected) {
|
||||
scope.header = $interpolate(attrs.msSelected)(scope);
|
||||
} else {
|
||||
scope.header = $translate.instant('main.multiselect.selected', { n: modelCtrl.$modelValue.length });
|
||||
}
|
||||
} else {
|
||||
var local = {};
|
||||
local[parsedResult.itemName] = modelCtrl.$modelValue;
|
||||
scope.header = parsedResult.viewMapper(local);
|
||||
}
|
||||
}
|
||||
|
||||
function isEmpty(obj) {
|
||||
if(obj === true || obj === false) {
|
||||
return false;
|
||||
}
|
||||
if(!obj) {
|
||||
return true;
|
||||
}
|
||||
if(obj.length && obj.length > 0) {
|
||||
return false;
|
||||
}
|
||||
for(var prop in obj) {
|
||||
if(obj[prop]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(compareByKey !== undefined && obj[compareByKey] !== undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
scope.valid = function validModel() {
|
||||
if(!required) {
|
||||
return true;
|
||||
}
|
||||
var value = modelCtrl.$modelValue;
|
||||
return (angular.isArray(value) && value.length > 0) || (!angular.isArray(value) && value !== null);
|
||||
};
|
||||
|
||||
function selectSingle(item) {
|
||||
if(!item.checked) {
|
||||
scope.uncheckAll();
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
setModelValue(false);
|
||||
}
|
||||
|
||||
function selectMultiple(item) {
|
||||
item.checked = !item.checked;
|
||||
setModelValue(true);
|
||||
}
|
||||
|
||||
function setModelValue(isMultiple) {
|
||||
var value;
|
||||
|
||||
if(isMultiple) {
|
||||
value = [];
|
||||
angular.forEach(scope.items, function(item) {
|
||||
if(item.checked) {
|
||||
value.push(item.model);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
angular.forEach(scope.items, function(item) {
|
||||
if(item.checked) {
|
||||
value = item.model;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
modelCtrl.$setViewValue(value);
|
||||
}
|
||||
|
||||
function markChecked(newVal) {
|
||||
if(!angular.isArray(newVal)) {
|
||||
angular.forEach(scope.items, function(item) {
|
||||
item.checked = false;
|
||||
if(compareByKey === undefined && angular.equals(item.model, newVal)) {
|
||||
item.checked = true;
|
||||
} else if(compareByKey !== undefined && newVal !== null && item.model[compareByKey] !== undefined && angular.equals(item.model[compareByKey], newVal[compareByKey])) {
|
||||
item.checked = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
angular.forEach(scope.items, function(item) {
|
||||
item.checked = false;
|
||||
angular.forEach(newVal, function(i) {
|
||||
if(compareByKey === undefined && angular.equals(item.model, i)) {
|
||||
item.checked = true;
|
||||
} else if(compareByKey !== undefined && item.model[compareByKey] !== undefined && angular.equals(item.model[compareByKey], i[compareByKey])) {
|
||||
item.checked = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scope.checkAll = function() {
|
||||
if(!isMultiple) {
|
||||
return;
|
||||
}
|
||||
angular.forEach(scope.items, function(item) {
|
||||
item.checked = true;
|
||||
});
|
||||
setModelValue(true);
|
||||
};
|
||||
|
||||
scope.uncheckAll = function() {
|
||||
angular.forEach(scope.items, function(item) {
|
||||
item.checked = false;
|
||||
});
|
||||
setModelValue(true);
|
||||
};
|
||||
|
||||
scope.select = function(event, item) {
|
||||
if(isMultiple === false) {
|
||||
selectSingle(item);
|
||||
scope.toggleSelect();
|
||||
} else {
|
||||
event.stopPropagation();
|
||||
selectMultiple(item);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}])
|
||||
.directive("multiselectPopup", ["$document", function($document) {
|
||||
return {
|
||||
restrict : "E",
|
||||
scope : false,
|
||||
replace : true,
|
||||
templateUrl: "multiselect.tpl.html",
|
||||
link : function(scope, element, attrs) {
|
||||
|
||||
scope.isVisible = false;
|
||||
|
||||
scope.toggleSelect = function() {
|
||||
if(element.hasClass("open")) {
|
||||
scope.filter = "";
|
||||
element.removeClass("open");
|
||||
$document.unbind("click", clickHandler);
|
||||
} else {
|
||||
scope.filter = "";
|
||||
element.addClass("open");
|
||||
$document.bind("click", clickHandler);
|
||||
}
|
||||
};
|
||||
|
||||
// $("ul.dropdown-menu").on("click", "[data-stopPropagation]", function(e) {
|
||||
// e.stopPropagation();
|
||||
// });
|
||||
|
||||
function clickHandler(event) {
|
||||
if(elementMatchesAnyInArray(event.target, element.find(event.target.tagName))) {
|
||||
return;
|
||||
}
|
||||
element.removeClass("open");
|
||||
$document.unbind("click", clickHandler);
|
||||
scope.$apply();
|
||||
}
|
||||
|
||||
var elementMatchesAnyInArray = function(element, elementArray) {
|
||||
for(var i = 0; i < elementArray.length; i++) {
|
||||
if(element === elementArray[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
angular.module("multiselect.tpl.html", []).run(["$templateCache", function($templateCache) {
|
||||
$templateCache.put("multiselect.tpl.html",
|
||||
"<div class=\"btn-group\">\n" +
|
||||
" <button tabindex=\"{{tabindex}}\" title=\"{{header}}\" type=\"button\" class=\"btn btn-default dropdown-toggle\" ng-click=\"toggleSelect()\" ng-disabled=\"disabled\" ng-class=\"{'error': !valid()}\">\n" +
|
||||
" <div ng-style=\"maxWidth\" style=\"padding-right: 13px; overflow: hidden; text-overflow: ellipsis;\">{{header}}</div><span class=\"caret\" style=\"position:absolute;right:10px;top:14px;\"></span>\n" +
|
||||
" </button>\n" +
|
||||
" <ul class=\"dropdown-menu\" style=\"margin-bottom:30px;padding-left:5px;padding-right:5px;\" ng-style=\"ulStyle\">\n" +
|
||||
" <input ng-show=\"items.length > filterAfterRows\" ng-model=\"filter\" style=\"padding: 0px 3px; margin-bottom: 4px;\" placeholder=\"{{ 'main.multiselect.filterPlaceholder' | tr }}\">" +
|
||||
" <li data-stopPropagation=\"true\" ng-repeat=\"i in items | filter:filter\" ng-class=\"{'dropdown-header': i.header, 'divider': i.divider}\">\n" +
|
||||
" <a ng-if=\"!i.header && !i.divider\" ng-click=\"select($event, i)\" style=\"padding:3px 10px;cursor:pointer;\">\n" +
|
||||
" <i class=\"fa\" ng-class=\"{'fa-check': i.checked, 'empty': !i.checked}\"></i> {{i.label}}" +
|
||||
" </a>\n" +
|
||||
" <span ng-if=\"i.header\">{{i.label}}</span>" +
|
||||
" </li>\n" +
|
||||
" </ul>\n" +
|
||||
"</div>");
|
||||
}]);
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
AngularJS v1.5.8
|
||||
(c) 2010-2016 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(n,c){'use strict';function l(b,a,g){var d=g.baseHref(),k=b[0];return function(b,e,f){var g,h;f=f||{};h=f.expires;g=c.isDefined(f.path)?f.path:d;c.isUndefined(e)&&(h="Thu, 01 Jan 1970 00:00:00 GMT",e="");c.isString(h)&&(h=new Date(h));e=encodeURIComponent(b)+"="+encodeURIComponent(e);e=e+(g?";path="+g:"")+(f.domain?";domain="+f.domain:"");e+=h?";expires="+h.toUTCString():"";e+=f.secure?";secure":"";f=e.length+1;4096<f&&a.warn("Cookie '"+b+"' possibly not set or overflowed because it was too large ("+
|
||||
f+" > 4096 bytes)!");k.cookie=e}}c.module("ngCookies",["ng"]).provider("$cookies",[function(){var b=this.defaults={};this.$get=["$$cookieReader","$$cookieWriter",function(a,g){return{get:function(d){return a()[d]},getObject:function(d){return(d=this.get(d))?c.fromJson(d):d},getAll:function(){return a()},put:function(d,a,m){g(d,a,m?c.extend({},b,m):b)},putObject:function(d,b,a){this.put(d,c.toJson(b),a)},remove:function(a,k){g(a,void 0,k?c.extend({},b,k):b)}}}]}]);c.module("ngCookies").factory("$cookieStore",
|
||||
["$cookies",function(b){return{get:function(a){return b.getObject(a)},put:function(a,c){b.putObject(a,c)},remove:function(a){b.remove(a)}}}]);l.$inject=["$document","$log","$browser"];c.module("ngCookies").provider("$$cookieWriter",function(){this.$get=l})})(window,window.angular);
|
||||
//# sourceMappingURL=angular-cookies.min.js.map
|
||||
@@ -1 +0,0 @@
|
||||
!function(t,e,i,n){"use strict";i.module("ngFitText",[]).value("fitTextDefaultConfig",{debounce:!1,delay:250,loadDelay:10,compressor:1,min:0,max:Number.POSITIVE_INFINITY}).directive("fittext",["$timeout","fitTextDefaultConfig","fitTextConfig",function(e,n,o){return{restrict:"A",scope:!0,link:function(f,a,l){function r(){var t=T*h/s.offsetWidth/h;return Math.max(Math.min((c[0].offsetWidth-6)*t*p,parseFloat(y)),parseFloat(m))}function u(){s.offsetHeight*s.offsetWidth!==0&&(d.fontSize=T+"px",d.lineHeight="1",d.display="inline-block",d.fontSize=r()+"px",d.lineHeight=b,d.display=v)}i.extend(n,o.config);var c=a.parent(),s=a[0],d=s.style,x=t.getComputedStyle(a[0],null),h=a.children().length||1,g=l.fittextLoadDelay||n.loadDelay,p=l.fittext||n.compressor,m=("inherit"===l.fittextMin?x["font-size"]:l.fittextMin)||n.min,y=("inherit"===l.fittextMax?x["font-size"]:l.fittextMax)||n.max,b=x["line-height"],v=x.display,T=10;e(function(){u()},g),f.$watch(l.ngBind,function(){u()}),n.debounce?i.element(t).bind("resize",n.debounce(function(){f.$apply(u)},n.delay)):i.element(t).bind("resize",function(){f.$apply(u)})}}}]).provider("fitTextConfig",function(){var t=this;return this.config={},this.$get=function(){var e={};return e.config=t.config,e},this})}(window,document,angular);
|
||||
@@ -1,6 +0,0 @@
|
||||
/*!
|
||||
* angular-translate - v2.18.3 - 2020-07-08
|
||||
*
|
||||
* Copyright (c) 2020 The angular-translate team, Pascal Precht; Licensed MIT
|
||||
*/
|
||||
!function(e,i){"function"==typeof define&&define.amd?define([],function(){return i()}):"object"==typeof module&&module.exports?module.exports=i():i()}(0,function(){function e(n,a){"use strict";return function(r){if(!(r&&(angular.isArray(r.files)||angular.isString(r.prefix)&&angular.isString(r.suffix))))throw new Error("Couldn't load static files, no files and prefix or suffix specified!");r.files||(r.files=[{prefix:r.prefix,suffix:r.suffix}]);for(var e=function(e){if(!e||!angular.isString(e.prefix)||!angular.isString(e.suffix))throw new Error("Couldn't load static file, no prefix or suffix specified!");var i=[e.prefix,r.key,e.suffix].join("");return angular.isObject(r.fileMap)&&r.fileMap[i]&&(i=r.fileMap[i]),a(angular.extend({url:i,method:"GET"},r.$http)).then(function(e){return e.data},function(){return n.reject(r.key)})},i=[],t=r.files.length,f=0;f<t;f++)i.push(e({prefix:r.files[f].prefix,key:r.key,suffix:r.files[f].suffix}));return n.all(i).then(function(e){for(var i=e.length,r={},t=0;t<i;t++)for(var f in e[t])r[f]=e[t][f];return r})}}return e.$inject=["$q","$http"],angular.module("pascalprecht.translate").factory("$translateStaticFilesLoader",e),e.displayName="$translateStaticFilesLoader","pascalprecht.translate"});
|
||||
@@ -1,6 +0,0 @@
|
||||
/*!
|
||||
* angular-translate - v2.18.3 - 2020-07-08
|
||||
*
|
||||
* Copyright (c) 2020 The angular-translate team, Pascal Precht; Licensed MIT
|
||||
*/
|
||||
!function(t,e){"function"==typeof define&&define.amd?define([],function(){return e()}):"object"==typeof module&&module.exports?module.exports=e():e()}(0,function(){function t(t){"use strict";var n;if(1===angular.version.major&&4<=angular.version.minor){var o=t.get("$cookies");n={get:function(t){return o.get(t)},put:function(t,e){o.put(t,e)}}}else{var r=t.get("$cookieStore");n={get:function(t){return r.get(t)},put:function(t,e){r.put(t,e)}}}return{get:function(t){return n.get(t)},set:function(t,e){n.put(t,e)},put:function(t,e){n.put(t,e)}}}return t.$inject=["$injector"],angular.module("pascalprecht.translate").factory("$translateCookieStorage",t),t.displayName="$translateCookieStorage","pascalprecht.translate"});
|
||||
@@ -1,6 +0,0 @@
|
||||
/*!
|
||||
* angular-translate - v2.18.3 - 2020-07-08
|
||||
*
|
||||
* Copyright (c) 2020 The angular-translate team, Pascal Precht; Licensed MIT
|
||||
*/
|
||||
!function(t,e){"function"==typeof define&&define.amd?define([],function(){return e()}):"object"==typeof module&&module.exports?module.exports=e():e()}(0,function(){function t(a,t){"use strict";var o,e={get:function(t){return o||(o=a.localStorage.getItem(t)),o},set:function(t,e){o=e,a.localStorage.setItem(t,e)},put:function(t,e){o=e,a.localStorage.setItem(t,e)}},r="localStorage"in a;if(r){var n="pascalprecht.translate.storageTest";try{r=null!==a.localStorage&&(a.localStorage.setItem(n,"foo"),a.localStorage.removeItem(n),!0)}catch(t){r=!1}}return r?e:t}return t.$inject=["$window","$translateCookieStorage"],angular.module("pascalprecht.translate").factory("$translateLocalStorage",t),t.displayName="$translateLocalStorageFactory","pascalprecht.translate"});
|
||||
@@ -1,271 +0,0 @@
|
||||
/**
|
||||
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
|
||||
* @author Alex_Crack
|
||||
* @version v0.3.6
|
||||
* @link https://github.com/alexcrack/angular-ui-notification
|
||||
* @license MIT
|
||||
*/
|
||||
angular.module('ui-notification', []);
|
||||
|
||||
angular.module('ui-notification').provider('Notification', function () {
|
||||
|
||||
this.options = {
|
||||
delay: 5000,
|
||||
startTop: 10,
|
||||
startRight: 10,
|
||||
verticalSpacing: 10,
|
||||
horizontalSpacing: 10,
|
||||
positionX: 'right',
|
||||
positionY: 'top',
|
||||
replaceMessage: false,
|
||||
templateUrl: 'angular-ui-notification.html',
|
||||
onClose: undefined,
|
||||
onClick: undefined,
|
||||
closeOnClick: true,
|
||||
maxCount: 0, // 0 - Infinite
|
||||
container: 'body',
|
||||
priority: 10
|
||||
};
|
||||
|
||||
this.setOptions = function (options) {
|
||||
if (!angular.isObject(options)) throw new Error("Options should be an object!");
|
||||
this.options = angular.extend({}, this.options, options);
|
||||
};
|
||||
|
||||
this.$get = ["$timeout", "$http", "$compile", "$templateCache", "$rootScope", "$injector", "$sce", "$q", "$window", function ($timeout, $http, $compile, $templateCache, $rootScope, $injector, $sce, $q, $window) {
|
||||
var options = this.options;
|
||||
|
||||
var startTop = options.startTop;
|
||||
var startRight = options.startRight;
|
||||
var verticalSpacing = options.verticalSpacing;
|
||||
var horizontalSpacing = options.horizontalSpacing;
|
||||
var delay = options.delay;
|
||||
|
||||
var messageElements = [];
|
||||
var isResizeBound = false;
|
||||
|
||||
var notify = function (args, t) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
if (typeof args !== 'object' || args === null) {
|
||||
args = {message: args};
|
||||
}
|
||||
|
||||
args.scope = args.scope ? args.scope : $rootScope;
|
||||
args.template = args.templateUrl ? args.templateUrl : options.templateUrl;
|
||||
args.delay = !angular.isUndefined(args.delay) ? args.delay : delay;
|
||||
args.type = t || args.type || options.type || '';
|
||||
args.positionY = args.positionY ? args.positionY : options.positionY;
|
||||
args.positionX = args.positionX ? args.positionX : options.positionX;
|
||||
args.replaceMessage = args.replaceMessage ? args.replaceMessage : options.replaceMessage;
|
||||
args.onClose = args.onClose ? args.onClose : options.onClose;
|
||||
args.onClick = args.onClick ? args.onClick : options.onClick;
|
||||
args.closeOnClick = (args.closeOnClick !== null && args.closeOnClick !== undefined) ? args.closeOnClick : options.closeOnClick;
|
||||
args.container = args.container ? args.container : options.container;
|
||||
args.priority = args.priority ? args.priority : options.priority;
|
||||
|
||||
var template = $templateCache.get(args.template);
|
||||
|
||||
if (template) {
|
||||
processNotificationTemplate(template);
|
||||
} else {
|
||||
// load it via $http only if it isn't default template and template isn't exist in template cache
|
||||
// cache:true means cache it for later access.
|
||||
$http.get(args.template, {cache: true})
|
||||
.then(function (response) {
|
||||
processNotificationTemplate(response.data);
|
||||
})
|
||||
.catch(function (data) {
|
||||
throw new Error('Template (' + args.template + ') could not be loaded. ' + data);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function processNotificationTemplate(template) {
|
||||
|
||||
var scope = args.scope.$new();
|
||||
scope.message = $sce.trustAsHtml(args.message);
|
||||
scope.title = $sce.trustAsHtml(args.title);
|
||||
scope.t = args.type.substr(0, 1);
|
||||
scope.delay = args.delay;
|
||||
scope.onClose = args.onClose;
|
||||
scope.onClick = args.onClick;
|
||||
|
||||
var priorityCompareTop = function (a, b) {
|
||||
return a._priority - b._priority;
|
||||
};
|
||||
|
||||
var priorityCompareBtm = function (a, b) {
|
||||
return b._priority - a._priority;
|
||||
};
|
||||
|
||||
var reposite = function () {
|
||||
var j = 0;
|
||||
var k = 0;
|
||||
var lastTop = startTop;
|
||||
var lastRight = startRight;
|
||||
var lastPosition = [];
|
||||
|
||||
if (args.positionY === 'top') {
|
||||
messageElements.sort(priorityCompareTop);
|
||||
} else if (args.positionY === 'bottom') {
|
||||
messageElements.sort(priorityCompareBtm);
|
||||
}
|
||||
|
||||
for (var i = messageElements.length - 1; i >= 0; i--) {
|
||||
var element = messageElements[i];
|
||||
if (args.replaceMessage && i < messageElements.length - 1) {
|
||||
element.addClass('killed');
|
||||
continue;
|
||||
}
|
||||
var elHeight = parseInt(element[0].offsetHeight);
|
||||
var elWidth = parseInt(element[0].offsetWidth);
|
||||
var position = lastPosition[element._positionY + element._positionX];
|
||||
|
||||
if ((top + elHeight) > window.innerHeight) {
|
||||
position = startTop;
|
||||
k++;
|
||||
j = 0;
|
||||
}
|
||||
|
||||
var top = (lastTop = position ? (j === 0 ? position : position + verticalSpacing) : startTop);
|
||||
var right = lastRight + (k * (horizontalSpacing + elWidth));
|
||||
|
||||
element.css(element._positionY, top + 'px');
|
||||
if (element._positionX === 'center') {
|
||||
element.css('left', parseInt(window.innerWidth / 2 - elWidth / 2) + 'px');
|
||||
} else {
|
||||
element.css(element._positionX, right + 'px');
|
||||
}
|
||||
|
||||
lastPosition[element._positionY + element._positionX] = top + elHeight;
|
||||
|
||||
if (options.maxCount > 0 && messageElements.length > options.maxCount && i === 0) {
|
||||
element.scope().kill(true);
|
||||
}
|
||||
|
||||
j++;
|
||||
}
|
||||
};
|
||||
|
||||
var templateElement = $compile(template)(scope);
|
||||
templateElement._positionY = args.positionY;
|
||||
templateElement._positionX = args.positionX;
|
||||
templateElement._priority = args.priority;
|
||||
templateElement.addClass(args.type);
|
||||
|
||||
var closeEvent = function (e) {
|
||||
e = e.originalEvent || e;
|
||||
if (e.type === 'click' || e.propertyName === 'opacity' && e.elapsedTime >= 1) {
|
||||
|
||||
if (scope.onClose) {
|
||||
scope.$apply(scope.onClose(templateElement));
|
||||
}
|
||||
|
||||
if (e.type === 'click')
|
||||
if (scope.onClick) {
|
||||
scope.$apply(scope.onClick(templateElement));
|
||||
}
|
||||
|
||||
templateElement.remove();
|
||||
messageElements.splice(messageElements.indexOf(templateElement), 1);
|
||||
scope.$destroy();
|
||||
reposite();
|
||||
}
|
||||
};
|
||||
|
||||
if (args.closeOnClick) {
|
||||
templateElement.addClass('clickable');
|
||||
templateElement.bind('click', closeEvent);
|
||||
}
|
||||
|
||||
templateElement.bind('webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd', closeEvent);
|
||||
|
||||
if (angular.isNumber(args.delay)) {
|
||||
$timeout(function () {
|
||||
templateElement.addClass('killed');
|
||||
}, args.delay);
|
||||
}
|
||||
|
||||
setCssTransitions('none');
|
||||
|
||||
angular.element(document.querySelector(args.container)).append(templateElement);
|
||||
var offset = -(parseInt(templateElement[0].offsetHeight) + 50);
|
||||
templateElement.css(templateElement._positionY, offset + "px");
|
||||
messageElements.push(templateElement);
|
||||
|
||||
if (args.positionX == 'center') {
|
||||
var elWidth = parseInt(templateElement[0].offsetWidth);
|
||||
templateElement.css('left', parseInt(window.innerWidth / 2 - elWidth / 2) + 'px');
|
||||
}
|
||||
|
||||
$timeout(function () {
|
||||
setCssTransitions('');
|
||||
});
|
||||
|
||||
function setCssTransitions(value) {
|
||||
['-webkit-transition', '-o-transition', 'transition'].forEach(function (prefix) {
|
||||
templateElement.css(prefix, value);
|
||||
});
|
||||
}
|
||||
|
||||
scope._templateElement = templateElement;
|
||||
|
||||
scope.kill = function (isHard) {
|
||||
if (isHard) {
|
||||
if (scope.onClose) {
|
||||
scope.$apply(scope.onClose(scope._templateElement));
|
||||
}
|
||||
|
||||
messageElements.splice(messageElements.indexOf(scope._templateElement), 1);
|
||||
scope._templateElement.remove();
|
||||
scope.$destroy();
|
||||
$timeout(reposite);
|
||||
} else {
|
||||
scope._templateElement.addClass('killed');
|
||||
}
|
||||
};
|
||||
|
||||
$timeout(reposite);
|
||||
|
||||
if (!isResizeBound) {
|
||||
angular.element($window).bind('resize', function (e) {
|
||||
$timeout(reposite);
|
||||
});
|
||||
isResizeBound = true;
|
||||
}
|
||||
|
||||
deferred.resolve(scope);
|
||||
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
notify.primary = function (args) {
|
||||
return this(args, 'primary');
|
||||
};
|
||||
notify.error = function (args) {
|
||||
return this(args, 'error');
|
||||
};
|
||||
notify.success = function (args) {
|
||||
return this(args, 'success');
|
||||
};
|
||||
notify.info = function (args) {
|
||||
return this(args, 'info');
|
||||
};
|
||||
notify.warning = function (args) {
|
||||
return this(args, 'warning');
|
||||
};
|
||||
|
||||
notify.clearAll = function () {
|
||||
angular.forEach(messageElements, function (element) {
|
||||
element.addClass('killed');
|
||||
});
|
||||
};
|
||||
|
||||
return notify;
|
||||
}];
|
||||
});
|
||||
|
||||
angular.module("ui-notification").run(["$templateCache", function($templateCache) {$templateCache.put("angular-ui-notification.html","<div class=\"ui-notification\"><h3 ng-show=\"title\" ng-bind-html=\"title\"></h3><div class=\"message\" ng-bind-html=\"message\"></div></div>");}]);
|
||||
@@ -1,32 +0,0 @@
|
||||
// Custom library to add password show/hide icons to input element with `password-reveal` attribute
|
||||
// util.js has the angular version, this is for plain js
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
var svgEye = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>';
|
||||
var svgEyeSlash = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>';
|
||||
|
||||
document.querySelectorAll('[password-reveal]').forEach(function (element) {
|
||||
var eye = document.createElement('i');
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
eye.style.width = '18px';
|
||||
eye.style.height = '18px';
|
||||
eye.style.position = 'relative';
|
||||
eye.style.float = 'right';
|
||||
eye.style.marginTop = '-24px';
|
||||
eye.style.marginRight = '10px';
|
||||
eye.style.cursor = 'pointer';
|
||||
|
||||
eye.addEventListener('click', function () {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
eye.innerHTML = svgEye;
|
||||
} else {
|
||||
element.type = 'password';
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
}
|
||||
});
|
||||
|
||||
element.parentNode.style.position = 'relative';
|
||||
element.parentNode.insertBefore(eye, element.nextSibling);
|
||||
});
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Cloudron App Error</title>
|
||||
<meta name="description" content="Cloudron App Error">
|
||||
|
||||
<!-- Use static style as we can't include local stylesheets -->
|
||||
<style>
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: white;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
line-height: 1.846;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: #EFEFEF;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
color: #555;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
opacity: .5;
|
||||
transition: all .25s;
|
||||
}
|
||||
|
||||
footer:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #2196f3;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0a6ebd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="content">
|
||||
<h1>⌛</h1>
|
||||
<p>This app is currently not responding. Please try refreshing the page in a few minutes.</p>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<span class="text-muted"><a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script>
|
||||
|
||||
var tmp = window.location.hash.slice(1).split('&');
|
||||
|
||||
tmp.forEach(function (pair) {
|
||||
if (pair.indexOf('access_token=') === 0) localStorage.token = pair.split('=')[1];
|
||||
});
|
||||
|
||||
window.location.href = '/';
|
||||
|
||||
</script>
|
||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,102 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="546.13336"
|
||||
height="546.13336"
|
||||
viewBox="0 0 512.00001 512.00001"
|
||||
id="svg4519"
|
||||
version="1.1"
|
||||
inkscape:version="0.92.2 2405546, 2018-03-11"
|
||||
sodipodi:docname="appicon_fallback.svg"
|
||||
inkscape:export-filename="/home/nebulon/projects/yellowtent/dashboard/src/img/appicon_fallback.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4521" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.7"
|
||||
inkscape:cx="89.894291"
|
||||
inkscape:cy="162.5294"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="g4496"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="2880"
|
||||
inkscape:window-height="1565"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="55"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata4524">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-540.36216)">
|
||||
<g
|
||||
id="g4467"
|
||||
transform="matrix(20.50952,0,0,20.859456,-526.58031,-94.042799)">
|
||||
<g
|
||||
inkscape:export-ydpi="67.349998"
|
||||
inkscape:export-xdpi="67.349998"
|
||||
transform="matrix(0.59473169,0,0,0.59473169,31.04719,102.48374)"
|
||||
id="g4382">
|
||||
<g
|
||||
id="g4496">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="opacity:1;fill:#03a9f4;fill-opacity:1;stroke:none;stroke-width:1.10000002;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path4162"
|
||||
sodipodi:sides="6"
|
||||
sodipodi:cx="12.46875"
|
||||
sodipodi:cy="-99.893143"
|
||||
sodipodi:r1="19.266006"
|
||||
sodipodi:r2="16.307295"
|
||||
sodipodi:arg1="-0.52224059"
|
||||
sodipodi:arg2="0.0013581913"
|
||||
inkscape:flatsided="true"
|
||||
inkscape:rounded="0.12490573"
|
||||
inkscape:randomized="0"
|
||||
d="m 29.166669,-109.50348 c 1.200386,2.08567 1.17988,17.183595 -0.02617,19.265993 -1.206046,2.082397 -14.291486,9.613601 -16.697919,9.610333 -2.406432,-0.0033 -15.4713664,-7.56999 -16.671752,-9.655655 -1.2003857,-2.085666 -1.1798799,-17.183591 0.026167,-19.265991 1.2060467,-2.0824 14.2914862,-9.6136 16.6979192,-9.61033 2.406432,0.003 15.471366,7.56999 16.671752,9.65565 z"
|
||||
transform="rotate(-30,10.993604,-99.259973)"
|
||||
inkscape:export-xdpi="67.349998"
|
||||
inkscape:export-ydpi="67.349998" />
|
||||
<rect
|
||||
inkscape:transform-center-x="0.66390665"
|
||||
ry="3.9522502"
|
||||
y="-107.69034"
|
||||
x="4.8100815"
|
||||
height="14.288903"
|
||||
width="14.288903"
|
||||
id="rect4168-1-1"
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.75875854;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
inkscape:transform-center-y="3.7035412e-06" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,133 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="16"
|
||||
height="16"
|
||||
id="svg2"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.91 r13725"
|
||||
version="1.0"
|
||||
sodipodi:docname="avatar-default-symbolic.svg"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#e7e7e7"
|
||||
borderopacity="1"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="11.964497"
|
||||
inkscape:cx="6.5536056"
|
||||
inkscape:cy="-0.025360958"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:showpageshadow="false"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1030"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-global="true">
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="0,112"
|
||||
id="guide2383" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="78.156291,0"
|
||||
id="guide2389" />
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid3672"
|
||||
visible="true"
|
||||
enabled="true" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="22.008699,4.1542523"
|
||||
id="guide2950" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="11.22532,22.008699"
|
||||
id="guide2952" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccsccccc"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
id="path3935"
|
||||
d="m -13.771529,5.9050966 c 0.181174,0.8569201 0.2823,1.5051186 0.135325,2.3620387 -1.145861,0.9506717 -4.076448,1.3778558 -4.072056,2.3620387 l -0.393673,2.558875 c 0,0.978388 2.731928,1.771529 6.101933,1.771529 3.370005,0 6.101933,-0.793141 6.101933,-1.771529 L -6.29174,10.629174 c -0.0047,-0.8423279 -2.952548,-1.377856 -4.084358,-2.3620387 -0.09668,-0.7953524 -0.01972,-1.5666863 0.147627,-2.3620387 l -3.543058,0 z" />
|
||||
<path
|
||||
transform="matrix(0.34209356,0,0,0.34209356,-8.638748,-12.26548)"
|
||||
d="m -9.75,73.09375 c -3.766412,0.121068 -7.468069,1.386362 -11.40625,3.25 a 1.25331,1.25331 0 0 0 -0.6875,1.4375 l 0.625,2.53125 a 1.25331,1.25331 0 0 0 0.78125,0.84375 c 0.161757,0.06256 0.275429,0.183794 0.71875,0.3125 2.335298,0.677989 5.907957,1.15625 9.90625,1.15625 3.9982931,0 7.5709518,-0.478261 9.90625,-1.15625 0.44332111,-0.128707 0.55699247,-0.24994 0.71875,-0.3125 a 1.25331,1.25331 0 0 0 0.78125,-0.8125 L 2.25,78.03125 a 1.25331,1.25331 0 0 0 -0.53125,-1.375 C -2.2051532,74.042333 -5.9835879,72.972682 -9.75,73.09375 z"
|
||||
id="path3937"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
inkscape:original="M -9.71875 74.34375 C -13.230599 74.456635 -16.76467 75.641953 -20.625 77.46875 L -20 80 C -19.731211 80.103955 -19.729288 80.147142 -19.375 80.25 C -17.218663 80.876033 -13.703662 81.375 -9.8125 81.375 C -5.9213382 81.375 -2.4063369 80.876033 -0.25 80.25 C 0.10428761 80.147142 0.10621054 80.103955 0.375 80 L 1.03125 77.6875 C -2.7172738 75.190412 -6.2069011 74.230865 -9.71875 74.34375 z "
|
||||
inkscape:radius="1.2531847"
|
||||
sodipodi:type="inkscape:offset" />
|
||||
<rect
|
||||
transform="matrix(0.9205234,-0.39068744,0.39068744,0.9205234,0,0)"
|
||||
ry="1.1810193"
|
||||
rx="1.1810193"
|
||||
y="-2.754653"
|
||||
x="-15.569602"
|
||||
height="2.1871843"
|
||||
width="1.0935922"
|
||||
id="rect3939"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
id="rect3941"
|
||||
width="1.0935922"
|
||||
height="2.1871843"
|
||||
x="6.5567312"
|
||||
y="6.6361833"
|
||||
rx="1.1810193"
|
||||
ry="1.1810193"
|
||||
transform="matrix(-0.9205234,-0.39068744,-0.39068744,0.9205234,0,0)" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
id="path3943"
|
||||
d="m -12,0 c -1.630647,0 -2.952548,1.2337743 -2.952548,2.7557118 0.01278,0.5632387 0.06085,1.232346 0.393673,2.7557117 0.196837,0.5905097 1.558851,2.1652021 1.574692,2.3620387 0.381733,0.1968365 1.771529,0.1968365 2.165203,0 0,-0.1968366 1.181019,-1.771529 1.377855,-2.3620387 C -9.066594,3.9281919 -9.06754,3.3462214 -9.047452,2.7557118 -9.047452,1.2337743 -10.369352,0 -12,0 z" />
|
||||
<path
|
||||
id="path3157"
|
||||
d="m 38,0 c -1.630647,0 -2.9375,1.2280625 -2.9375,2.75 0.0037,0.1620664 0.01579,0.3963239 0.03125,0.59375 -0.27885,0.118349 -0.299198,0.6610508 -0.0625,1.21875 0.09386,0.2211566 0.213411,0.3909677 0.34375,0.53125 0.03167,0.1567366 0.02336,0.2271022 0.0625,0.40625 0.196837,0.5905097 1.577909,2.1781634 1.59375,2.375 0.381733,0.1968365 1.762576,0.1968365 2.15625,0 0,-0.1968366 1.178164,-1.7844903 1.375,-2.375 C 40.60622,5.3151913 40.62213,5.1903792 40.65625,5.03125 40.764832,4.8997227 40.857512,4.7509639 40.9375,4.5625 41.162363,4.0326858 41.147829,3.5269131 40.90625,3.375 40.920493,3.1615298 40.931227,2.9343906 40.9375,2.75 40.9375,1.2280625 39.630648,0 38,0 z m -1.78125,8.40625 c -1.233461,0.8706787 -3.941711,1.2750309 -3.9375,2.21875 l -0.375,2.5625 c 0,0.519013 0.775005,0.988493 2,1.3125 l 0.1875,0.71875 A 0.42874928,0.42874928 0 0 0 34.375,15.5 c 0.05534,0.0214 0.09834,0.04972 0.25,0.09375 C 35.42389,15.825686 36.63221,16 38,16 39.36779,16 40.60736,15.825686 41.40625,15.59375 41.557907,15.54972 41.569664,15.5214 41.625,15.5 a 0.42874928,0.42874928 0 0 0 0.28125,-0.28125 L 42.125,14.5 c 1.208619,-0.323691 1.96875,-0.797472 1.96875,-1.3125 l -0.375,-2.5625 C 43.714419,9.848863 41.21753,9.3437322 39.9375,8.5 A 0.97584188,0.97584188 0 0 1 39.625,8.75 C 39.020006,9.0524961 38.608286,9 38.09375,9 37.836482,9 37.587947,9.0004922 37.34375,8.96875 37.099553,8.937008 36.902156,8.909026 36.59375,8.75 a 0.97584188,0.97584188 0 0 1 -0.375,-0.34375 z"
|
||||
style="fill:#bebebe;fill-opacity:1;stroke:none"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#bebebe;fill-opacity:1;stroke:none"
|
||||
d="m 8,0.7783785 c -2.256463,0 -4.0648649,1.699373 -4.0648649,3.8054054 0.00509,0.2242649 0.021845,0.5484266 0.043244,0.8216216 -0.3858679,0.1637694 -0.4140257,0.9147514 -0.086487,1.6864865 0.1298861,0.3060329 0.295314,0.5410147 0.4756757,0.7351351 0.043823,0.2168896 0.032325,0.3142604 0.086486,0.5621622 0.1511376,0.4534118 0.7470076,1.3420395 1.2972972,2.0756757 0.05396,0.563421 0.109936,1.132004 0,1.772973 -1.5856236,1.315524 -5.67094212,1.881347 -5.66486451,3.243243 L 0,16 16,16 15.91351,15.481081 c -0.0065,-1.1656 -4.098682,-1.881347 -5.664862,-3.243243 -0.06337,-0.521335 -0.07545,-1.043272 -0.04324,-1.556757 0.501434,-0.7738141 1.172201,-1.7868737 1.34054,-2.2918917 0.0605,-0.2557354 0.08252,-0.4284482 0.129729,-0.6486487 0.150255,-0.1820053 0.278504,-0.3878554 0.38919,-0.6486486 0.311162,-0.7331483 0.291049,-1.4330284 -0.04324,-1.6432432 0.01971,-0.2953966 0.03456,-0.6097082 0.04324,-0.8648649 0,-2.1060324 -1.8084,-3.8054054 -4.064865,-3.8054054 z"
|
||||
id="path3159"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 568 400"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="background-image-placeholder.svg"
|
||||
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
|
||||
width="568"
|
||||
height="400"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
id="namedview6"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.0029297"
|
||||
inkscape:cx="109.18014"
|
||||
inkscape:cy="337.01266"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4" />
|
||||
<!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) -->
|
||||
<path
|
||||
d="m 374.10635,275.08863 v 7.50887 c 0,12.44125 -10.08533,22.52659 -22.52659,22.52659 H 171.36704 c -12.44124,0 -22.52658,-10.08534 -22.52658,-22.52659 V 162.45567 c 0,-12.44124 10.08534,-22.52659 22.52658,-22.52659 h 7.50887 v 22.52659 h -4.69305 a 2.8158239,2.8158239 0 0 0 -2.81582,2.81583 v 114.51018 a 2.8158239,2.8158239 0 0 0 2.81582,2.81582 h 174.58109 a 2.8158239,2.8158239 0 0 0 2.81581,-2.81582 v -4.69305 z M 393.81713,117.4025 H 219.23606 a 2.8158239,2.8158239 0 0 0 -2.81582,2.81582 v 114.51017 a 2.8158239,2.8158239 0 0 0 2.81582,2.81582 h 174.58107 a 2.8158239,2.8158239 0 0 0 2.81581,-2.81582 V 120.21832 a 2.8158239,2.8158239 0 0 0 -2.81581,-2.81582 z m 2.81581,-22.52659 c 12.44126,0 22.5266,10.08534 22.5266,22.52659 v 120.14181 c 0,12.44125 -10.08535,22.5266 -22.5266,22.5266 h -180.2127 c -12.44125,0 -22.5266,-10.08535 -22.5266,-22.5266 V 117.4025 c 0,-12.44125 10.08535,-22.52659 22.5266,-22.52659 z m -123.89623,52.56204 c 0,10.3674 -8.40478,18.77216 -18.77216,18.77216 -10.3674,0 -18.77217,-8.40476 -18.77217,-18.77216 0,-10.3674 8.40477,-18.77216 18.77217,-18.77216 10.3674,0 18.77216,8.40476 18.77216,18.77216 z m -33.78989,45.05318 18.54454,-18.54455 c 2.19916,-2.19915 5.76494,-2.19915 7.96456,0 L 284,192.49113 l 48.58001,-48.58 c 2.19916,-2.19916 5.76493,-2.19916 7.96456,0 l 33.5618,33.56227 v 37.54432 H 238.94683 Z"
|
||||
id="path2"
|
||||
style="fill:#999999;stroke-width:0.469304" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,209 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="Application" ng-controller="MainController">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title>Cloudron Dashboard</title>
|
||||
<meta name="description" content="Cloudron Dashboard">
|
||||
|
||||
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/slick.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/bootstrap-slider/bootstrap-slider.min.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- toBlob() polyfill-->
|
||||
<script type="text/javascript" src="/3rdparty/js/canvas-to-blob.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Slick carousel -->
|
||||
<script type="text/javascript" src="/3rdparty/js/slick.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-route.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-animate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-sanitize.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-slick.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-fittext.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angular directives for tldjs -->
|
||||
<script type="text/javascript" src="/3rdparty/js/tld.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-tld.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- <link type="text/css" rel="stylesheet" href="/3rdparty/Chart/Chart.min.css?<%= revision %>"/> -->
|
||||
<!-- Chart.js https://www.chartjs.org/ -->
|
||||
<script type="text/javascript" src="/3rdparty/chart.umd.js?<%= revision %>"></script>
|
||||
|
||||
<script type="text/javascript" src="/3rdparty/js/ansi_up.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/clipboard.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Bootstrap slider -->
|
||||
<script type="text/javascript" src="/3rdparty/bootstrap-slider/bootstrap-slider.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/bootstrap-slider/slider.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Anugular Multiselect https://github.com/sebastianha/angular-bootstrap-multiselect -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-bootstrap-multiselect.js?<%= revision %>"></script>
|
||||
|
||||
<!-- moment -->
|
||||
<script type="text/javascript" src="/3rdparty/js/moment-with-locales.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- timezone list -->
|
||||
<script type="text/javascript" src="/js/timezones.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Main Application -->
|
||||
<script type="text/javascript" src="/js/index.js?<%= revision %>"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script type="text/ng-template" id="notification.html">
|
||||
<div class="ui-notification">
|
||||
<h3 ng-show="title" ng-bind-html="title"></h3>
|
||||
<div class="message">
|
||||
<a href="{{action}}" ng-show="action" ng-bind-html="message"></a>
|
||||
<span ng-hide="action" ng-bind-html="message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://docs.cloudron.io/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> {{ 'main.offline' | tr }}</a>
|
||||
|
||||
<!-- Modal reboot server -->
|
||||
<div class="modal fade" id="rebootModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'main.rebootDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-bold">{{ 'main.rebootDialog.warning' | tr }}</p>
|
||||
<p>{{ 'main.rebootDialog.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="reboot.submit()" ng-disabled="reboot.busy"><i class="fa fa-circle-notch fa-spin" ng-show="reboot.busy"></i> {{ 'main.rebootDialog.rebootAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mainContentContainer" class="animateMe ng-hide layout-root" ng-show="initialized">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-default navbar-static-top shadow" role="navigation" style="margin-bottom: 0">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand navbar-brand-icon" href="#/"><img ng-src="{{ client.avatar }}" width="40" height="40"/></a>
|
||||
<a class="navbar-brand" href="#/">{{ config.cloudronName || 'Cloudron' }}</a>
|
||||
</div>
|
||||
<!-- /.navbar-header -->
|
||||
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav navbar-right" ng-hide="hideNavBarActions">
|
||||
<li ng-show="user.isAtLeastOwner && (subscription.plan.id === 'free' || subscription.plan.id === 'expired')">
|
||||
<a ng-click="openSubscriptionSetup()" style="cursor: pointer">
|
||||
<span class="badge" ng-class="{'badge-danger': subscription.plan.id !== 'free', 'badge-success': subscription.plan.id === 'free' }">
|
||||
{{ subscription.plan.id === 'free' ? ('settings.appstoreAccount.subscriptionSetupAction' | tr) : ('settings.appstoreAccount.subscriptionReactivateAction' | tr) }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="!user.isAtLeastOwner && subscription.plan.id === 'expired'">
|
||||
<a>
|
||||
<span class="badge badge-danger">Subscription Expired</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-class="{ active: isActive('/apps')}" href="#/apps"><i class="fa fa-grip fa-fw"></i> {{ 'apps.title' | tr }}</a>
|
||||
</li>
|
||||
<li ng-show="user.isAtLeastAdmin">
|
||||
<a ng-class="{ active: isActive('/appstore')}" href="#/appstore"><i class="fa fa-cloud-download-alt fa-fw"></i> {{ 'appstore.title' | tr }}</a>
|
||||
</li>
|
||||
<li ng-show="user.isAtLeastUserManager">
|
||||
<a ng-class="{ active: isActive('/users')}" href="#/users"><i class="fa fa-users fa-fw"></i> {{ 'main.navbar.users' | tr }}</a>
|
||||
</li>
|
||||
<li ng-show="user.isAtLeastAdmin">
|
||||
<a href="#/notifications">
|
||||
<i class="fas fa-bell" ng-show="notificationCount"></i>
|
||||
<i class="far fa-bell" ng-hide="notificationCount"></i>
|
||||
<span class="badge badge-danger" ng-show="notificationCount">{{ notificationCount === 100 ? '100+' : notificationCount }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><img ng-src="{{user.avatarUrl}}" style="width: 24px; height: 24px;"/> {{user.username}} <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#/profile"><i class="fa fa-user fa-fw"></i> {{ 'profile.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastMailManager" class="divider"></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/backups"><i class="fa fa-archive fa-fw"></i> {{ 'backups.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastOwner"><a href="#/branding"><i class="fa fa-passport fa-fw"></i> {{ 'branding.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/domains"><i class="fa fa-globe fa-fw"></i> {{ 'domains.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastMailManager"><a href="#/email"><i class="fa fa-envelope fa-fw"></i> {{ 'emails.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/eventlog"><i class="fa fa-list-alt fa-fw"></i> {{ 'eventlog.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/network"><i class="fas fa-network-wired fa-fw"></i> {{ 'network.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/services"><i class="fa fa-cogs fa-fw"></i> {{ 'services.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> {{ 'settings.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/usersettings"><i class="fa fa-users-gear fa-fw"></i> {{ 'users.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/volumes"><i class="fa fa-hdd fa-fw"></i> {{ 'volumes.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin" class="divider"></li>
|
||||
<li ng-show="user.isAtLeastOwner"><a href="#/support"><i class="fa fa-comment fa-fw"></i> {{ 'support.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/system"><i class="fa fa-chart-area fa-fw"></i> {{ 'system.title' | tr }}</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out-alt fa-fw"></i> {{ 'main.logout' | tr }}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div ng-view id="ng-view" class="layout-content"></div>
|
||||
|
||||
<footer class="text-center ng-cloak">
|
||||
<span class="text-muted" ng-bind-html="config.footer | markdown2html"></span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,847 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global ERROR,ISTATES,HSTATES,RSTATES,APP_TYPES,NOTIFICATION_TYPES */
|
||||
|
||||
// deal with accessToken in the query, this is passed for example on password reset and account setup upon invite
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
if (search.accessToken) {
|
||||
localStorage.token = search.accessToken;
|
||||
|
||||
// strip the accessToken and expiresAt, then preserve the rest
|
||||
delete search.accessToken;
|
||||
delete search.expiresAt;
|
||||
|
||||
// this will reload the page as this is not a hash change
|
||||
window.location.search = encodeURIComponent(Object.keys(search).map(function (key) { return key + '=' + search[key]; }).join('&'));
|
||||
}
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld', 'ui.multiselect']);
|
||||
|
||||
app.config(['NotificationProvider', function (NotificationProvider) {
|
||||
NotificationProvider.setOptions({
|
||||
delay: 5000,
|
||||
startTop: 60,
|
||||
positionX: 'left',
|
||||
templateUrl: 'notification.html'
|
||||
});
|
||||
}]);
|
||||
|
||||
// configure resourceUrlWhitelist https://code.angularjs.org/1.5.8/docs/api/ng/provider/$sceDelegateProvider#resourceUrlWhitelist
|
||||
app.config(function ($sceDelegateProvider) {
|
||||
$sceDelegateProvider.resourceUrlWhitelist([
|
||||
// Allow same origin resource loads.
|
||||
'self',
|
||||
// Allow loading from our assets domain.
|
||||
'https://cloudron.io/**',
|
||||
'https://staging.cloudron.io/**',
|
||||
'https://dev.cloudron.io/**',
|
||||
// Allow local development against the appstore pages
|
||||
'http://localhost:5000/**'
|
||||
]);
|
||||
});
|
||||
|
||||
// setup all major application routes
|
||||
app.config(['$routeProvider', function ($routeProvider) {
|
||||
$routeProvider.when('/', {
|
||||
redirectTo: '/apps'
|
||||
}).when('/users', {
|
||||
controller: 'UsersController',
|
||||
templateUrl: 'views/users.html?<%= revision %>'
|
||||
}).when('/usersettings', {
|
||||
controller: 'UserSettingsController',
|
||||
templateUrl: 'views/user-settings.html?<%= revision %>'
|
||||
}).when('/app/:appId/:view?', {
|
||||
controller: 'AppController',
|
||||
templateUrl: 'views/app.html?<%= revision %>'
|
||||
}).when('/appstore', {
|
||||
controller: 'AppStoreController',
|
||||
templateUrl: 'views/appstore.html?<%= revision %>'
|
||||
}).when('/appstore/:appId', {
|
||||
controller: 'AppStoreController',
|
||||
templateUrl: 'views/appstore.html?<%= revision %>'
|
||||
}).when('/apps', {
|
||||
controller: 'AppsController',
|
||||
templateUrl: 'views/apps.html?<%= revision %>'
|
||||
}).when('/profile', {
|
||||
controller: 'ProfileController',
|
||||
templateUrl: 'views/profile.html?<%= revision %>'
|
||||
}).when('/backups', {
|
||||
controller: 'BackupsController',
|
||||
templateUrl: 'views/backups.html?<%= revision %>'
|
||||
}).when('/branding', {
|
||||
controller: 'BrandingController',
|
||||
templateUrl: 'views/branding.html?<%= revision %>'
|
||||
}).when('/network', {
|
||||
controller: 'NetworkController',
|
||||
templateUrl: 'views/network.html?<%= revision %>'
|
||||
}).when('/domains', {
|
||||
controller: 'DomainsController',
|
||||
templateUrl: 'views/domains.html?<%= revision %>'
|
||||
}).when('/email', {
|
||||
controller: 'EmailsController',
|
||||
templateUrl: 'views/emails.html?<%= revision %>'
|
||||
}).when('/emails-eventlog', {
|
||||
controller: 'EmailsEventlogController',
|
||||
templateUrl: 'views/emails-eventlog.html?<%= revision %>'
|
||||
}).when('/emails-queue', {
|
||||
controller: 'EmailsQueueController',
|
||||
templateUrl: 'views/emails-queue.html?<%= revision %>'
|
||||
}).when('/email/:domain/:view?', {
|
||||
controller: 'EmailController',
|
||||
templateUrl: 'views/email.html?<%= revision %>'
|
||||
}).when('/notifications', {
|
||||
controller: 'NotificationsController',
|
||||
templateUrl: 'views/notifications.html?<%= revision %>'
|
||||
}).when('/oidc', {
|
||||
redirectTo: '/usersettings'
|
||||
}).when('/settings', {
|
||||
controller: 'SettingsController',
|
||||
templateUrl: 'views/settings.html?<%= revision %>'
|
||||
}).when('/eventlog', {
|
||||
controller: 'EventLogController',
|
||||
templateUrl: 'views/eventlog.html?<%= revision %>'
|
||||
}).when('/support', {
|
||||
controller: 'SupportController',
|
||||
templateUrl: 'views/support.html?<%= revision %>'
|
||||
}).when('/system', {
|
||||
controller: 'SystemController',
|
||||
templateUrl: 'views/system.html?<%= revision %>'
|
||||
}).when('/services', {
|
||||
controller: 'ServicesController',
|
||||
templateUrl: 'views/services.html?<%= revision %>'
|
||||
}).when('/volumes', {
|
||||
controller: 'VolumesController',
|
||||
templateUrl: 'views/volumes.html?<%= revision %>'
|
||||
}).otherwise({ redirectTo: '/'});
|
||||
}]);
|
||||
|
||||
app.filter('notificadtionTypeToColor', function () {
|
||||
return function (n) {
|
||||
switch (n.type) {
|
||||
case NOTIFICATION_TYPES.ALERT_REBOOT:
|
||||
case NOTIFICATION_TYPES.ALERT_APP_OOM:
|
||||
case NOTIFICATION_TYPES.ALERT_MAIL_STATUS:
|
||||
case NOTIFICATION_TYPES.ALERT_CERTIFICATE_RENEWAL_FAILED:
|
||||
case NOTIFICATION_TYPES.ALERT_DISK_SPACE:
|
||||
case NOTIFICATION_TYPES.ALERT_BACKUP_CONFIG:
|
||||
return '#ff4c4c';
|
||||
case NOTIFICATION_TYPES.ALERT_BOX_UPDATE:
|
||||
case NOTIFICATION_TYPES.ALERT_MANUAL_APP_UPDATE:
|
||||
return '#f0ad4e';
|
||||
default:
|
||||
return '#2196f3';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('capitalize', function () {
|
||||
return function (s) {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('activeTask', function () {
|
||||
return function (app) {
|
||||
if (!app) return false;
|
||||
return app.taskId !== null;
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('installSuccess', function () {
|
||||
return function (app) {
|
||||
if (!app) return false;
|
||||
return app.installationState === ISTATES.INSTALLED;
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('appIsInstalledAndHealthy', function () {
|
||||
return function (app) {
|
||||
if (!app) return false;
|
||||
return (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && app.runState === RSTATES.RUNNING);
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('applicationLink', function () {
|
||||
return function(app) {
|
||||
if (!app) return '';
|
||||
|
||||
// app links have http already in the fqdn
|
||||
if (app.fqdn.indexOf('http') !== 0) return 'https://' + app.fqdn;
|
||||
|
||||
return app.fqdn;
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('userManagementFilter', function () {
|
||||
return function(apps, option) {
|
||||
return apps.filter(function (app) {
|
||||
if (option.id === '') return true;
|
||||
if (option.id === 'sso') return !!(app.manifest.optionalSso || app.manifest.addons.ldap || app.manifest.addons.proxyAuth);
|
||||
if (option.id === 'nosso') return app.manifest.optionalSso || (!app.manifest.addons.ldap && !app.manifest.addons.proxyAuth);
|
||||
if (option.id === 'email') return !!app.manifest.addons.email;
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// this appears when an item in app grid is clicked
|
||||
app.filter('prettyAppErrorMessage', function () {
|
||||
return function (app) {
|
||||
if (!app) return '';
|
||||
|
||||
if (app.installationState === ISTATES.INSTALLED) {
|
||||
// app.health can also be null to indicate insufficient data
|
||||
if (app.health === HSTATES.UNHEALTHY) return 'The app is not responding to health checks. Check the logs for any error messages.';
|
||||
}
|
||||
|
||||
if (app.error.reason === 'Access Denied') {
|
||||
if (app.error.domain) return 'The DNS record for this location is not setup correctly. Please verify your DNS settings and repair this app.';
|
||||
} else if (app.error.reason === 'Already Exists') {
|
||||
if (app.error.domain) return 'The DNS record for this location already exists. Cloudron does not remove existing DNS records. Manually remove the DNS record and then click on repair.';
|
||||
}
|
||||
|
||||
return app.error.message;
|
||||
};
|
||||
});
|
||||
|
||||
// this appears as tool tip in app grid
|
||||
app.filter('appProgressMessage', function () {
|
||||
return function (app) {
|
||||
var message = app.message || (app.error ? app.error.message : '');
|
||||
return message;
|
||||
};
|
||||
});
|
||||
|
||||
// see apps.js $scope.states
|
||||
app.filter('selectedStateFilter', ['Client', function (Client) {
|
||||
return function selectedStateFilter(apps, selectedState) {
|
||||
return apps.filter(function (app) {
|
||||
if (!selectedState || !selectedState.state) return true;
|
||||
|
||||
if (selectedState.state === 'running') return app.runState === RSTATES.RUNNING && app.health === HSTATES.HEALTHY && app.installationState === ISTATES.INSTALLED;
|
||||
if (selectedState.state === 'stopped') return app.runState === RSTATES.STOPPED;
|
||||
if (selectedState.state === 'update_available') return !!(Client.getConfig().update[app.id] && Client.getConfig().update[app.id].manifest.version && Client.getConfig().update[app.id].manifest.version !== app.manifest.version);
|
||||
|
||||
return app.runState === RSTATES.RUNNING && (app.health !== HSTATES.HEALTHY || app.installationState !== ISTATES.INSTALLED); // not responding
|
||||
});
|
||||
};
|
||||
}]);
|
||||
|
||||
app.filter('selectedGroupAccessFilter', function () {
|
||||
return function selectedGroupAccessFilter(apps, group) {
|
||||
return apps.filter(function (app) {
|
||||
if (!group.id) return true; // case for no filter entry
|
||||
if (!app.accessRestriction) return true;
|
||||
|
||||
if (!app.accessRestriction.groups) return false;
|
||||
|
||||
if (app.accessRestriction.groups.indexOf(group.id) !== -1) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('selectedTagFilter', function () {
|
||||
return function selectedTagFilter(apps, selectedTags) {
|
||||
return apps.filter(function (app) {
|
||||
if (selectedTags.length === 0) return true;
|
||||
if (!app.tags) return false;
|
||||
|
||||
for (var i = 0; i < selectedTags.length; i++) {
|
||||
if (app.tags.indexOf(selectedTags[i]) === -1) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('selectedDomainFilter', function () {
|
||||
return function selectedDomainFilter(apps, selectedDomain) {
|
||||
return apps.filter(function (app) {
|
||||
if (selectedDomain._alldomains) return true; // magic domain for single select, see apps.js ALL_DOMAINS_DOMAIN
|
||||
if (app.type === APP_TYPES.LINK) return false;
|
||||
|
||||
if (selectedDomain.domain === app.domain) return true;
|
||||
if (app.aliasDomains.find(function (ad) { return ad.domain === selectedDomain.domain; })) return true;
|
||||
if (app.redirectDomains.find(function (ad) { return ad.domain === selectedDomain.domain; })) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('appSearchFilter', function () {
|
||||
return function appSearchFilter(apps, appSearch) {
|
||||
return apps.filter(function (app) {
|
||||
if (!appSearch) return true;
|
||||
appSearch = appSearch.toLowerCase();
|
||||
return app.fqdn.indexOf(appSearch) !== -1
|
||||
|| (app.label && app.label.toLowerCase().indexOf(appSearch) !== -1)
|
||||
|| (app.manifest.title && app.manifest.title.toLowerCase().indexOf(appSearch) !== -1)
|
||||
|| (appSearch.length >=6 && app.id.indexOf(appSearch) !== -1);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyDomains', function () {
|
||||
return function prettyDomains(domains) {
|
||||
return domains.map(function (d) { return d.domain; }).join(', ');
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('installationActive', function () {
|
||||
return function (app) {
|
||||
if (app.installationState === ISTATES.ERROR) return false;
|
||||
if (app.installationState === ISTATES.INSTALLED) return false;
|
||||
return true;
|
||||
};
|
||||
});
|
||||
|
||||
// this appears in the app grid
|
||||
app.filter('installationStateLabel', function () {
|
||||
return function(app) {
|
||||
if (!app) return '';
|
||||
|
||||
var waiting = app.progress === 0 ? ' (Queued)' : '';
|
||||
|
||||
switch (app.installationState) {
|
||||
case ISTATES.PENDING_INSTALL:
|
||||
return 'Installing' + waiting;
|
||||
case ISTATES.PENDING_CLONE:
|
||||
return 'Cloning' + waiting;
|
||||
case ISTATES.PENDING_LOCATION_CHANGE:
|
||||
case ISTATES.PENDING_CONFIGURE:
|
||||
case ISTATES.PENDING_RECREATE_CONTAINER:
|
||||
case ISTATES.PENDING_SERVICES_CHANGE:
|
||||
case ISTATES.PENDING_DEBUG:
|
||||
return 'Configuring' + waiting;
|
||||
case ISTATES.PENDING_RESIZE:
|
||||
return 'Resizing' + waiting;
|
||||
case ISTATES.PENDING_DATA_DIR_MIGRATION:
|
||||
return 'Migrating data' + waiting;
|
||||
case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting;
|
||||
case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting;
|
||||
case ISTATES.PENDING_IMPORT: return 'Importing' + waiting;
|
||||
case ISTATES.PENDING_UPDATE: return 'Updating' + waiting;
|
||||
case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting;
|
||||
case ISTATES.PENDING_START: return 'Starting' + waiting;
|
||||
case ISTATES.PENDING_STOP: return 'Stopping' + waiting;
|
||||
case ISTATES.PENDING_RESTART: return 'Restarting' + waiting;
|
||||
case ISTATES.ERROR: {
|
||||
if (app.error && app.error.message === 'ETRYAGAIN') return 'DNS Error';
|
||||
return 'Error';
|
||||
}
|
||||
case ISTATES.INSTALLED: {
|
||||
if (app.debugMode) {
|
||||
return 'Recovery Mode';
|
||||
} else if (app.runState === RSTATES.RUNNING) {
|
||||
if (!app.health) return 'Starting...'; // no data yet
|
||||
if (app.type === APP_TYPES.LINK) return '';
|
||||
if (app.health === HSTATES.HEALTHY) return 'Running';
|
||||
return 'Not responding'; // dead/exit/unhealthy
|
||||
} else if (app.runState === RSTATES.STOPPED) return 'Stopped';
|
||||
else return app.runState;
|
||||
}
|
||||
default: return app.installationState;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('taskName', function () {
|
||||
return function(installationState) {
|
||||
switch (installationState) {
|
||||
case ISTATES.PENDING_INSTALL: return 'install';
|
||||
case ISTATES.PENDING_CLONE: return 'clone';
|
||||
case ISTATES.PENDING_LOCATION_CHANGE: return 'location change';
|
||||
case ISTATES.PENDING_CONFIGURE: return 'configure';
|
||||
case ISTATES.PENDING_RECREATE_CONTAINER: return 'create container';
|
||||
case ISTATES.PENDING_DEBUG: return 'debug';
|
||||
case ISTATES.PENDING_RESIZE: return 'resize';
|
||||
case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'data migration';
|
||||
case ISTATES.PENDING_UNINSTALL: return 'uninstall';
|
||||
case ISTATES.PENDING_RESTORE: return 'restore';
|
||||
case ISTATES.PENDING_IMPORT: return 'import';
|
||||
case ISTATES.PENDING_UPDATE: return 'update';
|
||||
case ISTATES.PENDING_BACKUP: return 'backup';
|
||||
case ISTATES.PENDING_START: return 'start app';
|
||||
case ISTATES.PENDING_STOP: return 'stop app';
|
||||
case ISTATES.PENDING_RESTART: return 'restart app';
|
||||
default: return installationState || '';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('errorSuggestion', function () {
|
||||
return function (error) {
|
||||
if (!error) return '';
|
||||
|
||||
switch (error.reason) {
|
||||
case ERROR.ACCESS_DENIED:
|
||||
if (error.domain) return 'Check the DNS credentials of ' + error.domain.domain + ' in the Domains & Certs view';
|
||||
return '';
|
||||
case ERROR.COLLECTD_ERROR: return 'Check if collectd is running on the server';
|
||||
case ERROR.DATABASE_ERROR: return 'Check if MySQL database is running on the server';
|
||||
case ERROR.DOCKER_ERROR: return 'Check if docker is running on the server';
|
||||
case ERROR.DNS_ERROR: return 'Check if the DNS service of the domain is running';
|
||||
case ERROR.LOGROTATE_ERROR: return 'Check if logrotate is running on the server';
|
||||
case ERROR.NETWORK_ERROR: return 'Check if there are any network issues on the server';
|
||||
case ERROR.REVERSEPROXY_ERROR: return 'Check if nginx is running on the server';
|
||||
default: return '';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('readyToUpdate', function () {
|
||||
return function (apps) {
|
||||
return apps.every(function (app) {
|
||||
return (app.installationState === ISTATES.ERROR) || (app.installationState === ISTATES.INSTALLED);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('inProgressApps', function () {
|
||||
return function (apps) {
|
||||
return apps.filter(function (app) {
|
||||
return app.installationState !== ISTATES.ERROR && app.installationState !== ISTATES.INSTALLED;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyHref', function () {
|
||||
return function (input) {
|
||||
if (!input) return input;
|
||||
if (input.indexOf('http://') === 0) return input.slice('http://'.length);
|
||||
if (input.indexOf('https://') === 0) return input.slice('https://'.length);
|
||||
return input;
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyEmailAddresses', function () {
|
||||
return function prettyEmailAddresses(addresses) {
|
||||
if (!addresses) return '';
|
||||
if (addresses === '<>') return '<>';
|
||||
if (Array.isArray(addresses)) return addresses.map(function (a) { return a.slice(1, -1); }).join(', ');
|
||||
return addresses.slice(1, -1);
|
||||
};
|
||||
});
|
||||
|
||||
// custom directive for dynamic names in forms
|
||||
// See http://stackoverflow.com/questions/23616578/issue-registering-form-control-with-interpolated-name#answer-23617401
|
||||
app.directive('laterName', function () { // (2)
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: ['?ngModel', '^?form'], // (3)
|
||||
link: function postLink(scope, elem, attrs, ctrls) {
|
||||
attrs.$set('name', attrs.laterName);
|
||||
|
||||
var modelCtrl = ctrls[0]; // (3)
|
||||
var formCtrl = ctrls[1]; // (3)
|
||||
if (modelCtrl && formCtrl) {
|
||||
modelCtrl.$name = attrs.name; // (4)
|
||||
formCtrl.$addControl(modelCtrl); // (2)
|
||||
scope.$on('$destroy', function () {
|
||||
formCtrl.$removeControl(modelCtrl); // (5)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) {
|
||||
var original = $location.path;
|
||||
$location.path = function (path, reload) {
|
||||
if (reload === false) {
|
||||
var lastRoute = $route.current;
|
||||
var un = $rootScope.$on('$locationChangeSuccess', function () {
|
||||
$route.current = lastRoute;
|
||||
un();
|
||||
});
|
||||
}
|
||||
return original.apply($location, [path]);
|
||||
};
|
||||
}]);
|
||||
|
||||
app.directive('ngClickSelect', function () {
|
||||
return {
|
||||
restrict: 'AC',
|
||||
link: function (scope, element/*, attrs */) {
|
||||
element.bind('click', function () {
|
||||
var selection = window.getSelection();
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(this);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// handles various states and triggers a href or configure/repair view
|
||||
// used by attaching to controller $scope
|
||||
// if $scope.appPostInstallConfirm.show(app); exists it will be called if not yet confirmed
|
||||
function onAppClick(app, $event, isOperator, $scope) {
|
||||
function stopEvent() {
|
||||
$event.originalEvent.stopPropagation();
|
||||
$event.originalEvent.preventDefault();
|
||||
}
|
||||
|
||||
if (app.installationState !== ISTATES.INSTALLED) {
|
||||
if (app.installationState === ISTATES.ERROR && isOperator) $scope.showAppConfigure(app, 'repair');
|
||||
return stopEvent();
|
||||
}
|
||||
|
||||
// app.health can also be null to indicate insufficient data
|
||||
if (!app.health) return stopEvent();
|
||||
if (app.runState === RSTATES.STOPPED) return stopEvent();
|
||||
if (app.runState === RSTATES.STOPPED) return stopEvent();
|
||||
|
||||
if (app.health === HSTATES.UNHEALTHY || app.health === HSTATES.ERROR || app.health === HSTATES.DEAD) {
|
||||
if (isOperator) $scope.showAppConfigure(app, 'repair');
|
||||
return stopEvent();
|
||||
}
|
||||
|
||||
if (app.pendingPostInstallConfirmation && $scope.appPostInstallConfirm) {
|
||||
$scope.appPostInstallConfirm.show(app);
|
||||
return stopEvent();
|
||||
}
|
||||
}
|
||||
|
||||
app.directive('ngClickReveal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs) {
|
||||
element.addClass('hand');
|
||||
|
||||
var value = '';
|
||||
|
||||
scope.$watch(attrs.ngClickReveal, function (newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
element.html('<i>hidden</i>');
|
||||
value = newValue;
|
||||
}
|
||||
});
|
||||
|
||||
element.bind('click', function () {
|
||||
element.text(value);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// https://codepen.io/webmatze/pen/isuHh
|
||||
app.directive('tagInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
inputTags: '=taglist'
|
||||
},
|
||||
require: '^form',
|
||||
link: function ($scope, element, attrs, formCtrl) {
|
||||
$scope.defaultWidth = 200;
|
||||
$scope.tagText = ''; // current tag being edited
|
||||
$scope.placeholder = attrs.placeholder;
|
||||
$scope.tagArray = function () {
|
||||
if ($scope.inputTags === undefined) {
|
||||
return [];
|
||||
}
|
||||
return $scope.inputTags.split(' ').filter(function (tag) {
|
||||
return tag !== '';
|
||||
});
|
||||
};
|
||||
$scope.addTag = function () {
|
||||
var tagArray = $scope.tagArray();
|
||||
|
||||
// prevent adding empty or existing items
|
||||
if ($scope.tagText.length === 0 || tagArray.indexOf($scope.tagText) !== -1) {
|
||||
return $scope.tagText = '';
|
||||
}
|
||||
|
||||
tagArray.push($scope.tagText);
|
||||
$scope.inputTags = tagArray.join(' ');
|
||||
return $scope.tagText = '';
|
||||
};
|
||||
$scope.deleteTag = function (key) {
|
||||
var tagArray;
|
||||
tagArray = $scope.tagArray();
|
||||
if (tagArray.length > 0 && $scope.tagText.length === 0 && key === undefined) {
|
||||
tagArray.pop();
|
||||
} else {
|
||||
if (key !== undefined) {
|
||||
tagArray.splice(key, 1);
|
||||
}
|
||||
}
|
||||
formCtrl.$setDirty();
|
||||
return $scope.inputTags = tagArray.join(' ');
|
||||
};
|
||||
$scope.$watch('tagText', function (newVal, oldVal) {
|
||||
var tempEl;
|
||||
if (!(newVal === oldVal && newVal === undefined)) {
|
||||
tempEl = $('<span>' + newVal + '</span>').appendTo('body');
|
||||
$scope.inputWidth = tempEl.width() + 5;
|
||||
if ($scope.inputWidth < $scope.defaultWidth) {
|
||||
$scope.inputWidth = $scope.defaultWidth;
|
||||
}
|
||||
return tempEl.remove();
|
||||
}
|
||||
});
|
||||
element.bind('click', function () {
|
||||
element[0].firstChild.lastChild.focus();
|
||||
});
|
||||
element.bind('keydown', function (e) {
|
||||
var key = e.which;
|
||||
if (key === 9 || key === 13) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (key === 8) {
|
||||
return $scope.$apply('deleteTag()');
|
||||
}
|
||||
});
|
||||
element.bind('keyup', function (e) {
|
||||
var key = e.which;
|
||||
if (key === 9 || key === 13 || key === 32) {
|
||||
e.preventDefault();
|
||||
return $scope.$apply('addTag()');
|
||||
}
|
||||
});
|
||||
},
|
||||
template:
|
||||
'<div class="tag-input-container">' +
|
||||
'<div class="btn-group input-tag" data-ng-repeat="tag in tagArray()">' +
|
||||
'<button type="button" class="btn btn-xs btn-primary" disabled>{{ tag }}</button>' +
|
||||
'<button type="button" class="btn btn-xs btn-primary" data-ng-click="deleteTag($index)">×</button>' +
|
||||
'</div>' +
|
||||
'<input type="text" data-ng-model="tagText" ng-blur="addTag()" placeholder="{{placeholder}}"/>' +
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
app.config(['fitTextConfigProvider', function (fitTextConfigProvider) {
|
||||
fitTextConfigProvider.config = {
|
||||
loadDelay: 250,
|
||||
compressor: 0.9,
|
||||
min: 8,
|
||||
max: 24
|
||||
};
|
||||
}]);
|
||||
|
||||
app.controller('MainController', ['$scope', '$route', '$timeout', '$location', '$interval', 'Notification', 'Client', function ($scope, $route, $timeout, $location, $interval, Notification, Client) {
|
||||
$scope.initialized = false; // used to animate the UI
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.config = {};
|
||||
$scope.client = Client;
|
||||
$scope.subscription = {};
|
||||
$scope.notificationCount = 0;
|
||||
$scope.hideNavBarActions = $location.path() === '/logs';
|
||||
$scope.backgroundImageUrl = '';
|
||||
|
||||
$scope.reboot = {
|
||||
busy: false,
|
||||
|
||||
show: function () {
|
||||
$scope.reboot.busy = false;
|
||||
$('#rebootModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.reboot.busy = true;
|
||||
|
||||
Client.reboot(function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$('#rebootModal').modal('hide');
|
||||
|
||||
// trigger refetch to show offline banner
|
||||
$timeout(function () { Client.getStatus(function () {}); }, 5000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isActive = function (url) {
|
||||
if (!$route.current) return false;
|
||||
return $route.current.$$route.originalPath.indexOf(url) === 0;
|
||||
};
|
||||
|
||||
$scope.logout = function (event) {
|
||||
event.stopPropagation();
|
||||
$scope.initialized = false;
|
||||
Client.logout();
|
||||
};
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.subscription);
|
||||
};
|
||||
|
||||
// NOTE: this function is exported and called from the appstore.js
|
||||
$scope.updateSubscriptionStatus = function () {
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error && error.statusCode === 412) return; // not yet registered
|
||||
if (error && error.statusCode === 402) return; // invalid appstore token
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.subscription = subscription;
|
||||
});
|
||||
};
|
||||
|
||||
function refreshNotifications() {
|
||||
if (!Client.getUserInfo().isAtLeastAdmin) return;
|
||||
|
||||
Client.getNotifications({ acknowledged: false }, 1, 100, function (error, results) { // counter maxes out at 100
|
||||
if (error) console.error(error);
|
||||
else $scope.notificationCount = results.length;
|
||||
});
|
||||
}
|
||||
|
||||
// update state of acknowledged notification
|
||||
$scope.notificationAcknowledged = function () {
|
||||
refreshNotifications();
|
||||
};
|
||||
|
||||
function redirectOnMandatory2FA() {
|
||||
if (Client.getConfig().mandatory2FA && !Client.getUserInfo().twoFactorAuthenticationEnabled) {
|
||||
$location.path('/profile').search({ setup2fa: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Make it redirect if the browser URL is changed directly - https://forum.cloudron.io/topic/7510/bug-in-2fa-force
|
||||
$scope.$on('$routeChangeStart', function (/* event */) {
|
||||
if ($scope.initialized) redirectOnMandatory2FA();
|
||||
});
|
||||
|
||||
var gPlatformStatusNotification = null;
|
||||
function trackPlatformStatus() {
|
||||
Client.getPlatformStatus(function (error, result) {
|
||||
if (error) return console.error('Failed to get platform status.', error);
|
||||
|
||||
// see box/src/platform.js
|
||||
if (result.message === 'Ready') {
|
||||
if (gPlatformStatusNotification) {
|
||||
gPlatformStatusNotification.kill();
|
||||
gPlatformStatusNotification = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gPlatformStatusNotification) {
|
||||
var options = { title: 'Platform status', message: result.message, delay: 'notimeout', replaceMessage: true, closeOnClick: false };
|
||||
|
||||
Notification.primary(options).then(function (result) {
|
||||
gPlatformStatusNotification = result;
|
||||
$timeout(trackPlatformStatus, 5000);
|
||||
});
|
||||
} else {
|
||||
gPlatformStatusNotification.message = result.message;
|
||||
$timeout(trackPlatformStatus, 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function redirectIfNeeded(status) {
|
||||
if (!status.activated) {
|
||||
console.log('Not activated yet, redirecting', status);
|
||||
if (status.restore.active || status.restore.errorMessage) { // show the error message in restore page
|
||||
window.location.href = '/restore.html' + window.location.search;
|
||||
} else if (status.adminFqdn) {
|
||||
window.location.href = 'https://' + status.adminFqdn + '/setup.html' + (window.location.search);
|
||||
} else {
|
||||
window.location.href = '/setupdns.html' + window.location.search;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// support local development with localhost check
|
||||
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost' && !window.location.hostname.startsWith('192.')) {
|
||||
// user is accessing by IP or by the old admin location (pre-migration)
|
||||
window.location.href = '/setupdns.html' + window.location.search;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// this loads the very first thing when accessing via IP or domain
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (redirectIfNeeded(status)) return;
|
||||
|
||||
// check version and force reload if needed
|
||||
if (!localStorage.version) {
|
||||
localStorage.version = status.version;
|
||||
} else if (localStorage.version !== status.version) {
|
||||
localStorage.version = status.version;
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
console.log('Running dashboard version ', localStorage.version);
|
||||
|
||||
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
|
||||
Client.refreshUserInfo(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
Client.refreshConfig(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
Client.refreshAvailableLanguages(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
Client.refreshInstalledApps(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
if (Client.getUserInfo().hasBackgroundImage) {
|
||||
document.getElementById('mainContentContainer').style.backgroundImage = 'url("' + Client.getBackgroundImageUrl() + '")';
|
||||
document.getElementById('mainContentContainer').classList.add('has-background');
|
||||
}
|
||||
|
||||
$scope.initialized = true;
|
||||
|
||||
redirectOnMandatory2FA();
|
||||
|
||||
$interval(refreshNotifications, 60 * 1000);
|
||||
refreshNotifications();
|
||||
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error && error.statusCode === 412) return; // not yet registered
|
||||
if (error && error.statusCode === 402) return; // invalid appstore token
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.subscription = subscription;
|
||||
|
||||
// only track platform status if we are registered
|
||||
trackPlatformStatus();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Client.onConfig(function (config) {
|
||||
if (config.cloudronName) {
|
||||
document.title = config.cloudronName;
|
||||
}
|
||||
});
|
||||
|
||||
init();
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['updateModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
@@ -1,158 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular, $, showdown */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies']);
|
||||
|
||||
app.filter('markdown2html', function () {
|
||||
var converter = new showdown.Converter({
|
||||
simplifiedAutoLink: true,
|
||||
strikethrough: true,
|
||||
tables: true,
|
||||
openLinksInNewWindow: true
|
||||
});
|
||||
|
||||
return function (text) {
|
||||
return converter.makeHtml(text);
|
||||
};
|
||||
});
|
||||
|
||||
// disable sce for footer https://code.angularjs.org/1.5.8/docs/api/ng/service/$sce
|
||||
app.config(function ($sceProvider) {
|
||||
$sceProvider.enabled(false);
|
||||
});
|
||||
|
||||
app.config(['$translateProvider', function ($translateProvider) {
|
||||
$translateProvider.useStaticFilesLoader({
|
||||
prefix: 'translation/',
|
||||
suffix: '.json'
|
||||
});
|
||||
$translateProvider.preferredLanguage('en');
|
||||
$translateProvider.fallbackLanguage('en');
|
||||
}]);
|
||||
|
||||
// Add shorthand "tr" filter to avoid having ot use "translate"
|
||||
// This is a copy of the code at https://github.com/angular-translate/angular-translate/blob/master/src/filter/translate.js
|
||||
// If we find out how to get that function handle somehow dynamically we can use that, otherwise the copy is required
|
||||
function translateFilterFactory($parse, $translate) {
|
||||
var translateFilter = function (translationId, interpolateParams, interpolation, forceLanguage) {
|
||||
if (!angular.isObject(interpolateParams)) {
|
||||
var ctx = this || {
|
||||
'__SCOPE_IS_NOT_AVAILABLE': 'More info at https://github.com/angular/angular.js/commit/8863b9d04c722b278fa93c5d66ad1e578ad6eb1f'
|
||||
};
|
||||
interpolateParams = $parse(interpolateParams)(ctx);
|
||||
}
|
||||
|
||||
return $translate.instant(translationId, interpolateParams, interpolation, forceLanguage);
|
||||
};
|
||||
|
||||
if ($translate.statefulFilter()) {
|
||||
translateFilter.$stateful = true;
|
||||
}
|
||||
|
||||
return translateFilter;
|
||||
}
|
||||
translateFilterFactory.displayName = 'translateFilterFactory';
|
||||
app.filter('tr', translateFilterFactory);
|
||||
|
||||
|
||||
app.controller('PasswordResetController', ['$scope', '$translate', '$http', function ($scope, $translate, $http) {
|
||||
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.indexOf('=') === -1 ? [item, true] : [item.slice(0, item.indexOf('=')), item.slice(item.indexOf('=')+1)]; }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.initialized = false;
|
||||
$scope.mode = '';
|
||||
$scope.busy = false;
|
||||
$scope.error = false;
|
||||
$scope.branding = null;
|
||||
$scope.username = '';
|
||||
$scope.password = '';
|
||||
$scope.totpToken = '';
|
||||
$scope.passwordResetIdentifier = '';
|
||||
$scope.newPassword = '';
|
||||
$scope.newPasswordRepeat = '';
|
||||
var API_ORIGIN = '<%= apiOrigin %>' || window.location.origin;
|
||||
|
||||
$scope.onPasswordReset = function () {
|
||||
$scope.busy = true;
|
||||
|
||||
var data = {
|
||||
identifier: $scope.passwordResetIdentifier
|
||||
};
|
||||
|
||||
function done() {
|
||||
$scope.busy = false;
|
||||
$scope.mode = 'passwordResetDone';
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/auth/password_reset_request', data).success(done).error(done);
|
||||
};
|
||||
|
||||
$scope.onNewPassword = function () {
|
||||
$scope.busy = true;
|
||||
|
||||
var data = {
|
||||
resetToken: search.resetToken,
|
||||
password: $scope.newPassword,
|
||||
totpToken: $scope.totpToken
|
||||
};
|
||||
|
||||
function error(data, status) {
|
||||
console.log('error', status);
|
||||
$scope.busy = false;
|
||||
|
||||
if (status === 401) $scope.error = data.message;
|
||||
else if (status === 409) $scope.error = 'Ask your admin for an invite link first';
|
||||
else $scope.error = 'Unknown error';
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/auth/password_reset', data).success(function (data, status) {
|
||||
if (status !== 202) return error(data, status);
|
||||
|
||||
// set token to autologin
|
||||
localStorage.token = data.accessToken;
|
||||
|
||||
$scope.mode = 'newPasswordDone';
|
||||
}).error(function (data, status) {
|
||||
error(data, status);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showPasswordReset = function () {
|
||||
window.document.title = 'Password Reset Request';
|
||||
$scope.mode = 'passwordReset';
|
||||
$scope.passwordResetIdentifier = '';
|
||||
setTimeout(function () { $('#inputPasswordResetIdentifier').focus(); }, 200);
|
||||
};
|
||||
|
||||
$scope.showNewPassword = function () {
|
||||
window.document.title = 'Set New Password';
|
||||
$scope.mode = 'newPassword';
|
||||
setTimeout(function () { $('#inputNewPassword').focus(); }, 200);
|
||||
};
|
||||
|
||||
$http.get(API_ORIGIN + '/api/v1/auth/branding').success(function (data, status) {
|
||||
$scope.initialized = true;
|
||||
|
||||
if (status !== 200) return;
|
||||
|
||||
if (data.language) $translate.use(data.language);
|
||||
|
||||
$scope.branding = data;
|
||||
}).error(function () {
|
||||
$scope.initialized = false;
|
||||
});
|
||||
|
||||
// Init into the correct view
|
||||
if (search.passwordReset) {
|
||||
$scope.showPasswordReset();
|
||||
} else if (search.resetToken) {
|
||||
$scope.showNewPassword();
|
||||
} else if (search.accessToken || search.access_token) { // auto-login feature
|
||||
localStorage.token = search.accessToken || search.access_token;
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
$scope.showPasswordReset();
|
||||
}
|
||||
}]);
|
||||
@@ -1,397 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, angular, tld, SECRET_PLACEHOLDER, STORAGE_PROVIDERS, BACKUP_FORMATS */
|
||||
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR, REGIONS_CONTABO */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
|
||||
app.filter('zoneName', function () {
|
||||
return function (domain) {
|
||||
return tld.getDomain(domain);
|
||||
};
|
||||
});
|
||||
|
||||
app.controller('RestoreController', ['$scope', 'Client', function ($scope, Client) {
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.client = Client;
|
||||
$scope.busy = false;
|
||||
$scope.error = {};
|
||||
$scope.message = ''; // progress
|
||||
|
||||
// variables here have to match the import config logic!
|
||||
$scope.provider = '';
|
||||
$scope.bucket = '';
|
||||
$scope.prefix = '';
|
||||
$scope.mountPoint = '';
|
||||
$scope.accessKeyId = '';
|
||||
$scope.secretAccessKey = '';
|
||||
$scope.gcsKey = { keyFileName: '', content: '' };
|
||||
$scope.region = '';
|
||||
$scope.endpoint = '';
|
||||
$scope.backupFolder = '';
|
||||
$scope.remotePath = '';
|
||||
$scope.instanceId = '';
|
||||
$scope.acceptSelfSignedCerts = false;
|
||||
$scope.format = 'tgz';
|
||||
$scope.advancedVisible = false;
|
||||
$scope.password = '';
|
||||
$scope.encryptedFilenames = true;
|
||||
$scope.encrypted = false; // only used if a backup config contains that flag
|
||||
$scope.setupToken = '';
|
||||
$scope.skipDnsSetup = false;
|
||||
$scope.disk = null;
|
||||
$scope.blockDevices = [];
|
||||
|
||||
$scope.mountOptions = {
|
||||
host: '',
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
diskPath: '',
|
||||
user: '',
|
||||
seal: false,
|
||||
port: 22,
|
||||
privateKey: ''
|
||||
};
|
||||
|
||||
$scope.$watch('disk', function (newValue) {
|
||||
if (!newValue) return;
|
||||
$scope.mountOptions.diskPath = '/dev/disk/by-uuid/' + newValue.uuid;
|
||||
});
|
||||
|
||||
$scope.sysinfo = {
|
||||
provider: 'generic',
|
||||
ipv4: '',
|
||||
ifname: ''
|
||||
};
|
||||
|
||||
$scope.sysinfoProvider = [
|
||||
{ name: 'Public IP', value: 'generic' },
|
||||
{ name: 'Static IP Address', value: 'fixed' },
|
||||
{ name: 'Network Interface', value: 'network-interface' }
|
||||
];
|
||||
|
||||
$scope.prettySysinfoProviderName = function (provider) {
|
||||
switch (provider) {
|
||||
case 'generic': return 'Public IP';
|
||||
case 'fixed': return 'Static IP Address';
|
||||
case 'network-interface': return 'Network Interface';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.s3Regions = REGIONS_S3;
|
||||
$scope.wasabiRegions = REGIONS_WASABI;
|
||||
$scope.doSpacesRegions = REGIONS_DIGITALOCEAN;
|
||||
$scope.exoscaleSosRegions = REGIONS_EXOSCALE;
|
||||
$scope.scalewayRegions = REGIONS_SCALEWAY;
|
||||
$scope.linodeRegions = REGIONS_LINODE;
|
||||
$scope.ovhRegions = REGIONS_OVH;
|
||||
$scope.ionosRegions = REGIONS_IONOS;
|
||||
$scope.upcloudRegions = REGIONS_UPCLOUD;
|
||||
$scope.vultrRegions = REGIONS_VULTR;
|
||||
$scope.contaboRegions = REGIONS_CONTABO;
|
||||
|
||||
$scope.storageProviders = STORAGE_PROVIDERS;
|
||||
|
||||
$scope.formats = BACKUP_FORMATS;
|
||||
|
||||
$scope.s3like = function (provider) {
|
||||
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos'
|
||||
|| provider === 'digitalocean-spaces' || provider === 'wasabi' || provider === 'scaleway-objectstorage'
|
||||
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2'
|
||||
|| provider === 'ionos-objectstorage' || provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2'
|
||||
|| provider === 'contabo-objectstorage';
|
||||
};
|
||||
|
||||
$scope.mountlike = function (provider) {
|
||||
return provider === 'disk' || provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs';
|
||||
};
|
||||
|
||||
$scope.restore = function () {
|
||||
$scope.error = {};
|
||||
$scope.busy = true;
|
||||
|
||||
var backupConfig = {
|
||||
provider: $scope.provider,
|
||||
format: $scope.format,
|
||||
};
|
||||
if ($scope.password) {
|
||||
backupConfig.password = $scope.password;
|
||||
backupConfig.encryptedFilenames = $scope.encryptedFilenames;
|
||||
}
|
||||
|
||||
// only set provider specific fields, this will clear them in the db
|
||||
if ($scope.s3like(backupConfig.provider)) {
|
||||
backupConfig.bucket = $scope.bucket;
|
||||
backupConfig.prefix = $scope.prefix;
|
||||
backupConfig.accessKeyId = $scope.accessKeyId;
|
||||
backupConfig.secretAccessKey = $scope.secretAccessKey;
|
||||
|
||||
if ($scope.endpoint) backupConfig.endpoint = $scope.endpoint;
|
||||
|
||||
if (backupConfig.provider === 's3') {
|
||||
if ($scope.region) backupConfig.region = $scope.region;
|
||||
delete backupConfig.endpoint;
|
||||
} else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') {
|
||||
backupConfig.region = backupConfig.region || 'us-east-1';
|
||||
backupConfig.acceptSelfSignedCerts = $scope.acceptSelfSignedCerts;
|
||||
backupConfig.s3ForcePathStyle = true; // might want to expose this in the UI
|
||||
} else if (backupConfig.provider === 'exoscale-sos') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'wasabi') {
|
||||
backupConfig.region = $scope.wasabiRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'scaleway-objectstorage') {
|
||||
backupConfig.region = $scope.scalewayRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'linode-objectstorage') {
|
||||
backupConfig.region = $scope.linodeRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'ovh-objectstorage') {
|
||||
backupConfig.region = $scope.ovhRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'ionos-objectstorage') {
|
||||
backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'vultr-objectstorage') {
|
||||
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'contabo-objectstorage') {
|
||||
backupConfig.region = $scope.contaboRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
backupConfig.s3ForcePathStyle = true; // https://docs.contabo.com/docs/products/Object-Storage/technical-description (no virtual buckets)
|
||||
} else if (backupConfig.provider === 'upcloud-objectstorage') {
|
||||
var m = /^.*\.(.*)\.upcloudobjects.com$/.exec(backupConfig.endpoint);
|
||||
backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'digitalocean-spaces') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
}
|
||||
} else if (backupConfig.provider === 'gcs') {
|
||||
backupConfig.bucket = $scope.bucket;
|
||||
backupConfig.prefix = $scope.prefix;
|
||||
try {
|
||||
var serviceAccountKey = JSON.parse($scope.gcsKey.content);
|
||||
backupConfig.projectId = serviceAccountKey.project_id;
|
||||
backupConfig.credentials = {
|
||||
client_email: serviceAccountKey.client_email,
|
||||
private_key: serviceAccountKey.private_key
|
||||
};
|
||||
|
||||
if (!backupConfig.projectId || !backupConfig.credentials || !backupConfig.credentials.client_email || !backupConfig.credentials.private_key) {
|
||||
throw 'fields_missing';
|
||||
}
|
||||
} catch (e) {
|
||||
$scope.error.generic = 'Cannot parse Google Service Account Key: ' + e.message;
|
||||
$scope.error.gcsKeyInput = true;
|
||||
$scope.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if ($scope.mountlike(backupConfig.provider)) {
|
||||
backupConfig.prefix = $scope.prefix;
|
||||
backupConfig.mountOptions = {};
|
||||
|
||||
if (backupConfig.provider === 'cifs' || backupConfig.provider === 'sshfs' || backupConfig.provider === 'nfs') {
|
||||
backupConfig.mountOptions.host = $scope.mountOptions.host;
|
||||
backupConfig.mountOptions.remoteDir = $scope.mountOptions.remoteDir;
|
||||
|
||||
if (backupConfig.provider === 'cifs') {
|
||||
backupConfig.mountOptions.username = $scope.mountOptions.username;
|
||||
backupConfig.mountOptions.password = $scope.mountOptions.password;
|
||||
backupConfig.mountOptions.seal = $scope.mountOptions.seal;
|
||||
} else if (backupConfig.provider === 'sshfs') {
|
||||
backupConfig.mountOptions.user = $scope.mountOptions.user;
|
||||
backupConfig.mountOptions.port = $scope.mountOptions.port;
|
||||
backupConfig.mountOptions.privateKey = $scope.mountOptions.privateKey;
|
||||
}
|
||||
} else if (backupConfig.provider === 'disk' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') {
|
||||
backupConfig.mountOptions.diskPath = $scope.mountOptions.diskPath;
|
||||
} else if (backupConfig.provider === 'mountpoint') {
|
||||
backupConfig.mountPoint = $scope.mountPoint;
|
||||
}
|
||||
} else if (backupConfig.provider === 'filesystem') {
|
||||
backupConfig.backupFolder = $scope.backupFolder;
|
||||
}
|
||||
|
||||
if ($scope.remotePath.indexOf('/') === -1) {
|
||||
$scope.error.generic = 'Backup id must include the directory path';
|
||||
$scope.error.remotePath = true;
|
||||
$scope.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.remotePath.indexOf('box') === -1) {
|
||||
$scope.error.generic = 'Backup id must contain "box"';
|
||||
$scope.error.remotePath = true;
|
||||
$scope.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var version = $scope.remotePath.match(/_v(\d+.\d+.\d+)/);
|
||||
if (!version) {
|
||||
$scope.error.generic = 'Backup id is missing version information';
|
||||
$scope.error.remotePath = true;
|
||||
$scope.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var sysinfoConfig = {
|
||||
provider: $scope.sysinfo.provider
|
||||
};
|
||||
if ($scope.sysinfo.provider === 'fixed') {
|
||||
sysinfoConfig.ipv4 = $scope.sysinfo.ipv4;
|
||||
} else if ($scope.sysinfo.provider === 'network-interface') {
|
||||
sysinfoConfig.ifname = $scope.sysinfo.ifname;
|
||||
}
|
||||
|
||||
Client.restore(backupConfig, $scope.remotePath.replace(/\.tar\.gz(\.enc)?$/, ''), version ? version[1] : '', sysinfoConfig, $scope.skipDnsSetup, $scope.setupToken, function (error) {
|
||||
$scope.busy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 424) {
|
||||
$scope.error.generic = error.message;
|
||||
|
||||
if (error.message.indexOf('AWS Access Key Id') !== -1) {
|
||||
$scope.error.accessKeyId = true;
|
||||
$scope.accessKeyId = '';
|
||||
$scope.configureBackupForm.accessKeyId.$setPristine();
|
||||
$('#inputConfigureBackupAccessKeyId').focus();
|
||||
} else if (error.message.indexOf('not match the signature') !== -1 ) {
|
||||
$scope.error.secretAccessKey = true;
|
||||
$scope.secretAccessKey = '';
|
||||
$scope.configureBackupForm.secretAccessKey.$setPristine();
|
||||
$('#inputConfigureBackupSecretAccessKey').focus();
|
||||
} else if (error.message.toLowerCase() === 'access denied') {
|
||||
$scope.error.bucket = true;
|
||||
$scope.bucket = '';
|
||||
$scope.configureBackupForm.bucket.$setPristine();
|
||||
$('#inputConfigureBackupBucket').focus();
|
||||
} else if (error.message.indexOf('ECONNREFUSED') !== -1) {
|
||||
$scope.error.generic = 'Unknown region';
|
||||
$scope.error.region = true;
|
||||
$scope.configureBackupForm.region.$setPristine();
|
||||
$('#inputConfigureBackupDORegion').focus();
|
||||
} else if (error.message.toLowerCase() === 'wrong region') {
|
||||
$scope.error.generic = 'Wrong S3 Region';
|
||||
$scope.error.region = true;
|
||||
$scope.configureBackupForm.region.$setPristine();
|
||||
$('#inputConfigureBackupS3Region').focus();
|
||||
} else {
|
||||
$('#inputConfigureBackupBucket').focus();
|
||||
}
|
||||
} else {
|
||||
$scope.error.generic = error.message;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
waitForRestore();
|
||||
});
|
||||
};
|
||||
|
||||
function waitForRestore() {
|
||||
$scope.busy = true;
|
||||
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (!error && !status.restore.active) { // restore finished
|
||||
if (status.restore.errorMessage) {
|
||||
$scope.busy = false;
|
||||
$scope.error.generic = status.restore.errorMessage;
|
||||
} else { // restore worked, redirect to admin page
|
||||
window.location.href = '/';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!error) $scope.message = status.restore.message;
|
||||
|
||||
setTimeout(waitForRestore, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function readFileLocally(obj, file, fileName) {
|
||||
return function (event) {
|
||||
$scope.$apply(function () {
|
||||
obj[file] = null;
|
||||
obj[fileName] = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
obj[file] = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('gcsKeyFileInput').onchange = readFileLocally($scope.gcsKey, 'content', 'keyFileName');
|
||||
|
||||
document.getElementById('backupConfigFileInput').onchange = function (event) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read backup config');
|
||||
|
||||
var backupConfig;
|
||||
try {
|
||||
backupConfig = JSON.parse(result.target.result);
|
||||
} catch (e) {
|
||||
console.error('Unable to parse backup config');
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.$apply(function () {
|
||||
// we assume property names match here, this does not yet work for gcs keys
|
||||
Object.keys(backupConfig).forEach(function (k) {
|
||||
if (k in $scope) $scope[k] = backupConfig[k];
|
||||
});
|
||||
|
||||
// this allows the config to potentially have a raw password (though our UI sets it to placeholder)
|
||||
if ($scope.mountOptions.password === SECRET_PLACEHOLDER) $scope.mountOptions.password = '';
|
||||
});
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
};
|
||||
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (status.restore.active) return waitForRestore();
|
||||
|
||||
if (status.restore.errorMessage) $scope.error.generic = status.restore.errorMessage;
|
||||
|
||||
if (status.activated) {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
Client.getProvisionBlockDevices(function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to list blockdevices:', error);
|
||||
} else {
|
||||
// only offer non /, /boot or /home disks
|
||||
result = result.filter(function (d) { return d.mountpoint !== '/' && d.mountpoint !== '/home' && d.mountpoint !== '/boot'; });
|
||||
// only offer xfs and ext4 disks
|
||||
result = result.filter(function (d) { return d.type === 'xfs' || d.type === 'ext4'; });
|
||||
|
||||
// amend label for UI
|
||||
result.forEach(function (d) { d.label = d.path; });
|
||||
}
|
||||
|
||||
$scope.blockDevices = result;
|
||||
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.initialized = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
}]);
|
||||
@@ -1,120 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
|
||||
app.controller('SetupController', ['$scope', 'Client', function ($scope, Client) {
|
||||
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.client = Client;
|
||||
$scope.view = '';
|
||||
$scope.initialized = false;
|
||||
$scope.setupToken = '';
|
||||
|
||||
$scope.owner = {
|
||||
error: null,
|
||||
busy: false,
|
||||
|
||||
email: '',
|
||||
displayName: '',
|
||||
username: '',
|
||||
password: '',
|
||||
|
||||
submit: function () {
|
||||
$scope.owner.busy = true;
|
||||
$scope.owner.error = null;
|
||||
|
||||
var data = {
|
||||
username: $scope.owner.username,
|
||||
password: $scope.owner.password,
|
||||
email: $scope.owner.email,
|
||||
displayName: $scope.owner.displayName,
|
||||
setupToken: $scope.setupToken
|
||||
};
|
||||
|
||||
Client.createAdmin(data, function (error) {
|
||||
if (error && error.statusCode === 400) {
|
||||
$scope.owner.busy = false;
|
||||
|
||||
if (error.message === 'Invalid email') {
|
||||
$scope.owner.error = { email: error.message };
|
||||
$scope.owner.email = '';
|
||||
$scope.ownerForm.email.$setPristine();
|
||||
setTimeout(function () { $('#inputEmail').focus(); }, 200);
|
||||
} else {
|
||||
$scope.owner.error = { username: error.message };
|
||||
$scope.owner.username = '';
|
||||
$scope.ownerForm.username.$setPristine();
|
||||
setTimeout(function () { $('#inputUsername').focus(); }, 200);
|
||||
}
|
||||
return;
|
||||
} else if (error) {
|
||||
$scope.owner.busy = false;
|
||||
console.error('Internal error', error);
|
||||
$scope.owner.error = { generic: error.message };
|
||||
return;
|
||||
}
|
||||
|
||||
setView('finished');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function redirectIfNeeded(status) {
|
||||
if ('develop' in search || localStorage.getItem('develop')) {
|
||||
console.warn('Cloudron develop mode on. To disable run localStorage.removeItem(\'develop\')');
|
||||
localStorage.setItem('develop', true);
|
||||
return;
|
||||
}
|
||||
|
||||
// if we are here from https://ip/setup.html ,go to https://admin/setup.html
|
||||
if (status.adminFqdn && status.adminFqdn !== window.location.hostname) {
|
||||
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
|
||||
return true;
|
||||
}
|
||||
|
||||
// if we don't have a domain yet, first go to domain setup
|
||||
if (!status.adminFqdn) {
|
||||
window.location.href = '/setupdns.html';
|
||||
return true;
|
||||
}
|
||||
|
||||
if (status.activated) {
|
||||
window.location.href = '/';
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function setView(view) {
|
||||
if (view === 'finished') {
|
||||
$scope.view = 'finished';
|
||||
} else {
|
||||
$scope.view = 'owner';
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (redirectIfNeeded(status)) return;
|
||||
setView(search.view);
|
||||
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.initialized = true;
|
||||
|
||||
// Ensure we have a good autofocus
|
||||
setTimeout(function () {
|
||||
$(document).find("[autofocus]:first").focus();
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
}]);
|
||||
@@ -1,148 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular, $, showdown */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies']);
|
||||
|
||||
app.filter('markdown2html', function () {
|
||||
var converter = new showdown.Converter({
|
||||
simplifiedAutoLink: true,
|
||||
strikethrough: true,
|
||||
tables: true,
|
||||
openLinksInNewWindow: true
|
||||
});
|
||||
|
||||
return function (text) {
|
||||
return converter.makeHtml(text);
|
||||
};
|
||||
});
|
||||
|
||||
// disable sce for footer https://code.angularjs.org/1.5.8/docs/api/ng/service/$sce
|
||||
app.config(function ($sceProvider) {
|
||||
$sceProvider.enabled(false);
|
||||
});
|
||||
|
||||
app.config(['$translateProvider', function ($translateProvider) {
|
||||
$translateProvider.useStaticFilesLoader({
|
||||
prefix: 'translation/',
|
||||
suffix: '.json'
|
||||
});
|
||||
$translateProvider.useLocalStorage();
|
||||
$translateProvider.preferredLanguage('en');
|
||||
$translateProvider.fallbackLanguage('en');
|
||||
}]);
|
||||
|
||||
// Add shorthand "tr" filter to avoid having ot use "translate"
|
||||
// This is a copy of the code at https://github.com/angular-translate/angular-translate/blob/master/src/filter/translate.js
|
||||
// If we find out how to get that function handle somehow dynamically we can use that, otherwise the copy is required
|
||||
function translateFilterFactory($parse, $translate) {
|
||||
|
||||
'use strict';
|
||||
|
||||
var translateFilter = function (translationId, interpolateParams, interpolation, forceLanguage) {
|
||||
if (!angular.isObject(interpolateParams)) {
|
||||
var ctx = this || {
|
||||
'__SCOPE_IS_NOT_AVAILABLE': 'More info at https://github.com/angular/angular.js/commit/8863b9d04c722b278fa93c5d66ad1e578ad6eb1f'
|
||||
};
|
||||
interpolateParams = $parse(interpolateParams)(ctx);
|
||||
}
|
||||
|
||||
return $translate.instant(translationId, interpolateParams, interpolation, forceLanguage);
|
||||
};
|
||||
|
||||
if ($translate.statefulFilter()) {
|
||||
translateFilter.$stateful = true;
|
||||
}
|
||||
|
||||
return translateFilter;
|
||||
}
|
||||
translateFilterFactory.displayName = 'translateFilterFactory';
|
||||
app.filter('tr', translateFilterFactory);
|
||||
|
||||
app.controller('SetupAccountController', ['$scope', '$translate', '$http', function ($scope, $translate, $http) {
|
||||
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.indexOf('=') === -1 ? [item, true] : [item.slice(0, item.indexOf('=')), item.slice(item.indexOf('=')+1)]; }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
var API_ORIGIN = '<%= apiOrigin %>' || window.location.origin;
|
||||
|
||||
$scope.initialized = false;
|
||||
$scope.busy = false;
|
||||
$scope.error = null;
|
||||
$scope.view = 'setup';
|
||||
$scope.branding = null;
|
||||
|
||||
$scope.profileLocked = !!search.profileLocked;
|
||||
$scope.existingUsername = !!search.username;
|
||||
$scope.username = search.username || '';
|
||||
$scope.displayName = search.displayName || '';
|
||||
$scope.password = '';
|
||||
$scope.passwordRepeat = '';
|
||||
|
||||
$scope.onSubmit = function () {
|
||||
$scope.busy = true;
|
||||
$scope.error = null;
|
||||
|
||||
var data = {
|
||||
inviteToken: search.inviteToken,
|
||||
password: $scope.password
|
||||
};
|
||||
|
||||
if (!$scope.profileLocked) {
|
||||
if (!$scope.existingUsername) data.username = $scope.username;
|
||||
data.displayName = $scope.displayName;
|
||||
}
|
||||
|
||||
function error(data, status) {
|
||||
$scope.busy = false;
|
||||
|
||||
if (status === 401) {
|
||||
$scope.view = 'invalidToken';
|
||||
} else if (status === 409) {
|
||||
$scope.error = {
|
||||
username: true,
|
||||
message: 'Username already taken'
|
||||
};
|
||||
$scope.setupAccountForm.username.$setPristine();
|
||||
setTimeout(function () { $('#inputUsername').focus(); }, 200);
|
||||
} else if (status === 400) {
|
||||
$scope.error = {
|
||||
message: data.message
|
||||
};
|
||||
if (data.message.indexOf('Username') === 0) {
|
||||
$scope.setupAccountForm.username.$setPristine();
|
||||
$scope.error.username = true;
|
||||
}
|
||||
} else {
|
||||
$scope.error = { message: 'Unknown error. Please try again later.' };
|
||||
console.error(status, data);
|
||||
}
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/auth/setup_account', data).success(function (data, status) {
|
||||
if (status !== 201) return error(data, status);
|
||||
|
||||
// set token to autologin
|
||||
localStorage.token = data.accessToken;
|
||||
|
||||
$scope.view = 'done';
|
||||
}).error(error);
|
||||
};
|
||||
|
||||
if (!$scope.existingUsername && $scope.profileLocked) {
|
||||
$scope.view = 'noUsername';
|
||||
$scope.initialized = true;
|
||||
} else {
|
||||
$http.get(API_ORIGIN + '/api/v1/auth/branding').success(function (data, status) {
|
||||
$scope.initialized = true;
|
||||
|
||||
if (status !== 200) return;
|
||||
|
||||
if (data.language) $translate.use(data.language);
|
||||
|
||||
$scope.branding = data;
|
||||
}).error(function () {
|
||||
$scope.initialized = false;
|
||||
});
|
||||
}
|
||||
}]);
|
||||
@@ -1,337 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, tld, angular, Clipboard */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
|
||||
app.filter('zoneName', function () {
|
||||
return function (domain) {
|
||||
return tld.getDomain(domain);
|
||||
};
|
||||
});
|
||||
|
||||
app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', function ($scope, $http, $timeout, Client) {
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.state = null; // 'initialized', 'waitingForDnsSetup', 'waitingForBox'
|
||||
$scope.error = {};
|
||||
$scope.provider = '';
|
||||
$scope.showDNSSetup = false;
|
||||
$scope.instanceId = '';
|
||||
$scope.isDomain = false;
|
||||
$scope.isSubdomain = false;
|
||||
$scope.advancedVisible = false;
|
||||
$scope.clipboardDone = false;
|
||||
$scope.search = window.location.search;
|
||||
$scope.setupToken = '';
|
||||
|
||||
$scope.tlsProvider = [
|
||||
{ name: 'Let\'s Encrypt Prod', value: 'letsencrypt-prod' },
|
||||
{ name: 'Let\'s Encrypt Prod - Wildcard', value: 'letsencrypt-prod-wildcard' },
|
||||
{ name: 'Let\'s Encrypt Staging', value: 'letsencrypt-staging' },
|
||||
{ name: 'Let\'s Encrypt Staging - Wildcard', value: 'letsencrypt-staging-wildcard' },
|
||||
{ name: 'Self-Signed', value: 'fallback' }, // this is not 'Custom' because we don't allow user to upload certs during setup phase
|
||||
];
|
||||
|
||||
$scope.sysinfo = {
|
||||
provider: 'generic',
|
||||
ipv4: '',
|
||||
ifname: ''
|
||||
};
|
||||
|
||||
$scope.sysinfoProvider = [
|
||||
{ name: 'Public IP', value: 'generic' },
|
||||
{ name: 'Static IP Address', value: 'fixed' },
|
||||
{ name: 'Network Interface', value: 'network-interface' }
|
||||
];
|
||||
|
||||
$scope.prettySysinfoProviderName = function (provider) {
|
||||
switch (provider) {
|
||||
case 'generic': return 'Public IP';
|
||||
case 'fixed': return 'Static IP Address';
|
||||
case 'network-interface': return 'Network Interface';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.needsPort80 = function (dnsProvider, tlsProvider) {
|
||||
return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') &&
|
||||
(tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging'));
|
||||
};
|
||||
|
||||
// If we migrate the api origin we have to poll the new location
|
||||
if (search.admin_fqdn) Client.apiOrigin = 'https://' + search.admin_fqdn;
|
||||
|
||||
$scope.$watch('dnsCredentials.domain', function (newVal) {
|
||||
if (!newVal) {
|
||||
$scope.isDomain = false;
|
||||
$scope.isSubdomain = false;
|
||||
} else if (!tld.getDomain(newVal) || newVal[newVal.length-1] === '.') {
|
||||
$scope.isDomain = false;
|
||||
$scope.isSubdomain = false;
|
||||
} else {
|
||||
$scope.isDomain = true;
|
||||
$scope.isSubdomain = tld.getDomain(newVal) !== newVal;
|
||||
}
|
||||
});
|
||||
|
||||
// keep in sync with domains.js
|
||||
$scope.dnsProvider = [
|
||||
{ name: 'AWS Route53', value: 'route53' },
|
||||
{ name: 'Bunny', value: 'bunny' },
|
||||
{ name: 'Cloudflare', value: 'cloudflare' },
|
||||
{ name: 'DigitalOcean', value: 'digitalocean' },
|
||||
{ name: 'Gandi LiveDNS', value: 'gandi' },
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
{ name: 'Hetzner', value: 'hetzner' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Netcup', value: 'netcup' },
|
||||
{ name: 'Porkbun', value: 'porkbun' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
{ name: 'Manual (not recommended)', value: 'manual' },
|
||||
{ name: 'No-op (only for development)', value: 'noop' }
|
||||
];
|
||||
$scope.dnsCredentials = {
|
||||
busy: false,
|
||||
domain: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
gcdnsKey: { keyFileName: '', content: '' },
|
||||
digitalOceanToken: '',
|
||||
gandiApiKey: '',
|
||||
cloudflareEmail: '',
|
||||
cloudflareToken: '',
|
||||
cloudflareTokenType: 'GlobalApiKey',
|
||||
cloudflareDefaultProxyStatus: false,
|
||||
godaddyApiKey: '',
|
||||
godaddyApiSecret: '',
|
||||
linodeToken: '',
|
||||
bunnyAccessKey: '',
|
||||
hetznerToken: '',
|
||||
vultrToken: '',
|
||||
nameComUsername: '',
|
||||
nameComToken: '',
|
||||
namecheapUsername: '',
|
||||
namecheapApiKey: '',
|
||||
netcupCustomerNumber: '',
|
||||
netcupApiKey: '',
|
||||
netcupApiPassword: '',
|
||||
porkbunSecretapikey: '',
|
||||
porkbunApikey: '',
|
||||
|
||||
provider: 'route53',
|
||||
zoneName: '',
|
||||
tlsConfig: {
|
||||
provider: 'letsencrypt-prod-wildcard'
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setDefaultTlsProvider = function () {
|
||||
var dnsProvider = $scope.dnsCredentials.provider;
|
||||
// wildcard LE won't work without automated DNS
|
||||
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') {
|
||||
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod';
|
||||
} else {
|
||||
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod-wildcard';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function readFileLocally(obj, file, fileName) {
|
||||
return function (event) {
|
||||
$scope.$apply(function () {
|
||||
obj[file] = null;
|
||||
obj[fileName] = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
obj[file] = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.dnsCredentials.gcdnsKey, 'content', 'keyFileName');
|
||||
|
||||
$scope.setDnsCredentials = function () {
|
||||
$scope.dnsCredentials.busy = true;
|
||||
$scope.error = {};
|
||||
|
||||
var provider = $scope.dnsCredentials.provider;
|
||||
|
||||
var config = {};
|
||||
|
||||
if (provider === 'route53') {
|
||||
config.accessKeyId = $scope.dnsCredentials.accessKeyId;
|
||||
config.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
|
||||
} else if (provider === 'gcdns') {
|
||||
try {
|
||||
var serviceAccountKey = JSON.parse($scope.dnsCredentials.gcdnsKey.content);
|
||||
config.projectId = serviceAccountKey.project_id;
|
||||
config.credentials = {
|
||||
client_email: serviceAccountKey.client_email,
|
||||
private_key: serviceAccountKey.private_key
|
||||
};
|
||||
|
||||
if (!config.projectId || !config.credentials || !config.credentials.client_email || !config.credentials.private_key) {
|
||||
throw new Error('One or more fields are missing in the JSON');
|
||||
}
|
||||
} catch (e) {
|
||||
$scope.error.dnsCredentials = 'Cannot parse Google Service Account Key: ' + e.message;
|
||||
$scope.dnsCredentials.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if (provider === 'digitalocean') {
|
||||
config.token = $scope.dnsCredentials.digitalOceanToken;
|
||||
} else if (provider === 'gandi') {
|
||||
config.token = $scope.dnsCredentials.gandiApiKey;
|
||||
} else if (provider === 'godaddy') {
|
||||
config.apiKey = $scope.dnsCredentials.godaddyApiKey;
|
||||
config.apiSecret = $scope.dnsCredentials.godaddyApiSecret;
|
||||
} else if (provider === 'cloudflare') {
|
||||
config.email = $scope.dnsCredentials.cloudflareEmail;
|
||||
config.token = $scope.dnsCredentials.cloudflareToken;
|
||||
config.tokenType = $scope.dnsCredentials.cloudflareTokenType;
|
||||
config.defaultProxyStatus = $scope.dnsCredentials.cloudflareDefaultProxyStatus;
|
||||
} else if (provider === 'linode') {
|
||||
config.token = $scope.dnsCredentials.linodeToken;
|
||||
} else if (provider === 'bunny') {
|
||||
config.token = $scope.dnsCredentials.bunnyAccessKey;
|
||||
} else if (provider === 'hetzner') {
|
||||
config.token = $scope.dnsCredentials.hetznerToken;
|
||||
} else if (provider === 'vultr') {
|
||||
config.token = $scope.dnsCredentials.vultrToken;
|
||||
} else if (provider === 'namecom') {
|
||||
config.username = $scope.dnsCredentials.nameComUsername;
|
||||
config.token = $scope.dnsCredentials.nameComToken;
|
||||
} else if (provider === 'namecheap') {
|
||||
config.token = $scope.dnsCredentials.namecheapApiKey;
|
||||
config.username = $scope.dnsCredentials.namecheapUsername;
|
||||
} else if (provider === 'netcup') {
|
||||
config.customerNumber = $scope.dnsCredentials.netcupCustomerNumber;
|
||||
config.apiKey = $scope.dnsCredentials.netcupApiKey;
|
||||
config.apiPassword = $scope.dnsCredentials.netcupApiPassword;
|
||||
} else if (provider === 'porkbun') {
|
||||
config.apikey = $scope.dnsCredentials.porkbunApikey;
|
||||
config.secretapikey = $scope.dnsCredentials.porkbunSecretapikey;
|
||||
}
|
||||
|
||||
var tlsConfig = {
|
||||
provider: $scope.dnsCredentials.tlsConfig.provider,
|
||||
wildcard: false
|
||||
};
|
||||
if ($scope.dnsCredentials.tlsConfig.provider.indexOf('-wildcard') !== -1) {
|
||||
tlsConfig.provider = tlsConfig.provider.replace('-wildcard', '');
|
||||
tlsConfig.wildcard = true;
|
||||
}
|
||||
|
||||
var sysinfoConfig = {
|
||||
provider: $scope.sysinfo.provider
|
||||
};
|
||||
if ($scope.sysinfo.provider === 'fixed') {
|
||||
sysinfoConfig.ipv4 = $scope.sysinfo.ipv4;
|
||||
} else if ($scope.sysinfo.provider === 'network-interface') {
|
||||
sysinfoConfig.ifname = $scope.sysinfo.ifname;
|
||||
}
|
||||
|
||||
var data = {
|
||||
domainConfig: {
|
||||
domain: $scope.dnsCredentials.domain,
|
||||
zoneName: $scope.dnsCredentials.zoneName,
|
||||
provider: provider,
|
||||
config: config,
|
||||
tlsConfig: tlsConfig
|
||||
},
|
||||
ipv4Config: sysinfoConfig,
|
||||
providerToken: $scope.instanceId,
|
||||
setupToken: $scope.setupToken
|
||||
};
|
||||
|
||||
Client.setup(data, function (error) {
|
||||
if (error) {
|
||||
$scope.dnsCredentials.busy = false;
|
||||
if (error.statusCode === 422) {
|
||||
if (provider === 'ami') {
|
||||
$scope.error.ami = error.message;
|
||||
} else {
|
||||
$scope.error.setup = error.message;
|
||||
}
|
||||
} else {
|
||||
$scope.error.dnsCredentials = error.message;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
waitForDnsSetup();
|
||||
});
|
||||
};
|
||||
|
||||
function waitForDnsSetup() {
|
||||
$scope.state = 'waitingForDnsSetup';
|
||||
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (!error && !status.setup.active) {
|
||||
if (!status.adminFqdn || status.setup.errorMessage) { // setup reset or errored. start over
|
||||
$scope.error.setup = status.setup.errorMessage;
|
||||
$scope.state = 'initialized';
|
||||
$scope.dnsCredentials.busy = false;
|
||||
} else { // proceed to activation
|
||||
window.location.href = 'https://' + status.adminFqdn + '/setup.html' + (window.location.search);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.message = status.setup.message;
|
||||
|
||||
setTimeout(waitForDnsSetup, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (error) {
|
||||
// During domain migration, the box code restarts and can result in getStatus() failing temporarily
|
||||
console.error(error);
|
||||
$scope.state = 'waitingForBox';
|
||||
return $timeout(initialize, 3000);
|
||||
}
|
||||
|
||||
// domain is currently like a lock flag
|
||||
if (status.adminFqdn) return waitForDnsSetup();
|
||||
|
||||
if (status.provider === 'digitalocean' || status.provider === 'digitalocean-mp') {
|
||||
$scope.dnsCredentials.provider = 'digitalocean';
|
||||
} else if (status.provider === 'linode' || status.provider === 'linode-oneclick' || status.provider === 'linode-stackscript') {
|
||||
$scope.dnsCredentials.provider = 'linode';
|
||||
} else if (status.provider === 'vultr' || status.provider === 'vultr-mp') {
|
||||
$scope.dnsCredentials.provider = 'vultr';
|
||||
} else if (status.provider === 'gce') {
|
||||
$scope.dnsCredentials.provider = 'gcdns';
|
||||
} else if (status.provider === 'ami') {
|
||||
$scope.dnsCredentials.provider = 'route53';
|
||||
}
|
||||
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.provider = status.provider;
|
||||
$scope.state = 'initialized';
|
||||
|
||||
setTimeout(function () { $("[autofocus]:first").focus(); }, 100);
|
||||
});
|
||||
}
|
||||
|
||||
var clipboard = new Clipboard('.clipboard');
|
||||
clipboard.on('success', function () {
|
||||
$scope.$apply(function () { $scope.clipboardDone = true; });
|
||||
$timeout(function () { $scope.clipboardDone = false; }, 5000);
|
||||
});
|
||||
|
||||
initialize();
|
||||
}]);
|
||||
@@ -1,40 +0,0 @@
|
||||
/* This file contains helpers which should not be part of client.js */
|
||||
|
||||
angular.module('Application').directive('passwordReveal', function () {
|
||||
var svgEye = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>';
|
||||
var svgEyeSlash = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>';
|
||||
|
||||
return {
|
||||
link: function (scope, elements) {
|
||||
var element = elements[0];
|
||||
|
||||
if (!element.parentNode) {
|
||||
console.error('Wrong password-reveal directive usage. Element has no parent.');
|
||||
return;
|
||||
}
|
||||
|
||||
var eye = document.createElement('i');
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
eye.style.width = '18px';
|
||||
eye.style.height = '18px';
|
||||
eye.style.position = 'relative';
|
||||
eye.style.float = 'right';
|
||||
eye.style.marginTop = '-24px';
|
||||
eye.style.marginRight = '10px';
|
||||
eye.style.cursor = 'pointer';
|
||||
|
||||
eye.addEventListener('click', function () {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
eye.innerHTML = svgEye;
|
||||
} else {
|
||||
element.type = 'password';
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
}
|
||||
});
|
||||
|
||||
element.parentNode.style.position = 'relative';
|
||||
element.parentNode.insertBefore(eye, element.nextSibling);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Cloudron - Not Found</title>
|
||||
<meta name="description" content="Cloudron - Not Found">
|
||||
|
||||
<!-- Use static style as we can't include local stylesheets -->
|
||||
<style>
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: white;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.846;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #2196f3;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0a6ebd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('load', (event) => {
|
||||
document.getElementById('message').innerHTML =
|
||||
'You are seeing this page because the DNS record of <b>' + window.location.hostname + '</b> is set to this server\'s IP'
|
||||
+ ' but Cloudron has no app configured for this domain.';
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="content">
|
||||
<p id="message"></p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,170 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src <%= apiOrigin %> 'unsafe-inline' 'unsafe-eval' 'self'; img-src <%= apiOrigin %> 'self'" />
|
||||
|
||||
<!-- this gets changed once we get the status (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title>Cloudron Password Reset</title>
|
||||
<meta name="description" content="Cloudron Password Reset">
|
||||
|
||||
<link id="favicon" href="<%= apiOrigin %>/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
|
||||
<!-- <script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script> -->
|
||||
<!-- <script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script> -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/passwordreset.js?<%= revision %>"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body ng-app="Application" ng-controller="PasswordResetController">
|
||||
|
||||
<div class="layout-root ng-cloak" ng-show="initialized">
|
||||
|
||||
<div class="layout-content" ng-show="mode === 'passwordReset'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2>{{ 'passwordReset.title' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form name="passwordResetForm" ng-submit="onPasswordReset()">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPasswordResetIdentifier">{{ 'passwordReset.usernameOrEmail' | tr }}</label>
|
||||
<input type="text" class="form-control" id="inputPasswordResetIdentifier" name="passwordResetIdentifier" ng-model="passwordResetIdentifier" ng-disabled="busy" autofocus required>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="card-form-bottom-bar">
|
||||
<a href="/" class="hand">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
<button class="btn btn-primary btn-outline" type="submit" ng-disabled="busy || passwordResetForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.resetAction' | tr }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-content" ng-show="mode === 'passwordResetDone'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2>{{ 'passwordReset.emailSent.title' | tr }}</h2>
|
||||
<br/>
|
||||
<a href="/" class="btn btn-primary">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-content" ng-show="mode === 'newPassword'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2>{{ 'passwordReset.newPassword.title' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 class="has-error" ng-show="error">{{ error }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form name="newPasswordForm" ng-submit="onNewPassword()">
|
||||
<input type="password" style="display: none;"/>
|
||||
<div class="form-group" ng-class="{ 'has-error': newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid }">
|
||||
<label class="control-label" for="inputNewPassword">{{ 'passwordReset.newPassword.password' | tr }}</label>
|
||||
<div class="control-label" ng-show="newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid">
|
||||
<small ng-show="newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid">{{ 'passwordReset.newPassword.errorLength' | tr }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputNewPassword" ng-model="newPassword" name="newPassword" ng-minlength="8" ng-maxlength="256" autofocus required password-reveal>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat) }">
|
||||
<label class="control-label" for="inputNewPasswordRepeat">{{ 'passwordReset.newPassword.passwordRepeat' | tr }}</label>
|
||||
<div class="control-label" ng-show="newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat)">
|
||||
<small ng-show="newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat)">{{ 'passwordReset.newPassword.errorMismatch' | tr }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputNewPasswordRepeat" ng-model="newPasswordRepeat" name="newPasswordRepeat" required password-reveal>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPasswordResetTotpToken">{{ 'login.2faToken' | tr }}</label>
|
||||
<input type="text" class="form-control" name="passwordResetTotpToken" id="inputPasswordResetTotpToken" ng-model="totpToken" ng-disabled="busy" value="">
|
||||
</div>
|
||||
<div class="card-form-bottom-bar">
|
||||
<a href="/" class="hand">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
<button class="btn btn-primary btn-outline" type="submit" ng-disabled="busy || newPasswordForm.$invalid || newPassword !== newPasswordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.passwordChanged.submitAction' | tr }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-content" ng-show="mode === 'newPasswordDone'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2>{{ 'passwordReset.success.title' | tr }}</h2>
|
||||
<br/>
|
||||
<a href="/" class="btn btn-primary">{{ 'passwordReset.success.openDashboardAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted" ng-bind-html="branding.footer | markdown2html"></span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,355 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Cloudron Restore</title>
|
||||
<meta name="description" content="Cloudron Restore">
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
|
||||
|
||||
<!-- Angular directives for tldjs -->
|
||||
<script type="text/javascript" src="/3rdparty/js/tld.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/restore.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="setup" ng-app="Application" ng-controller="RestoreController">
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://docs.cloudron.io/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> Cloudron is offline. Reconnecting...</a>
|
||||
|
||||
<div class="main-container ng-cloak text-center" ng-show="busy">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<i class="fa fa-circle-notch fa-spin fa-5x"></i><br/>
|
||||
<h3>{{ message }} ...</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container ng-cloak" ng-show="initialized && !busy">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px;">
|
||||
<form name="configureBackupForm" role="form" novalidate ng-submit="restore()" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<h2>Cloudron Restore</h2>
|
||||
<p>Provide the backup to restore from</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col-md-8 col-md-offset-2 text-center">
|
||||
<input type="file" id="backupConfigFileInput" style="display:none"/>
|
||||
<button type="button" class="btn btn-default" onclick="getElementById('backupConfigFileInput').click();">Upload Backup Config</button>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<p class="has-error text-center" ng-show="error">{{ error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProviderProvider">Storage provider <sup><a ng-href="https://docs.cloudron.io/backups/#storage-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="storageProviderProvider" ng-model="provider" ng-options="a.value as a.name for a in storageProviders" ng-change=clearForm()></select>
|
||||
</div>
|
||||
|
||||
<!-- mountpoint -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.mountPoint }" ng-show="provider === 'mountpoint'">
|
||||
<label class="control-label" for="inputConfigureMountPoint">Mountpoint</label>
|
||||
<input type="text" class="form-control" ng-model="mountPoint" id="inputConfigureMountPoint" name="mountPoint" ng-disabled="busy" placeholder="Folder where filesystem is mounted" ng-required="provider === 'mountpoint'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs' || provider === 'nfs' || provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupHost">Server IP or Hostname</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.host" id="configureBackupHost" name="host" ng-disabled="busy" placeholder="Server IP or hostname" ng-required="provider === 'cifs' || provider === 'nfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="checkbox" ng-show="provider === 'cifs'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mountOptions.seal">Use seal encryption. Requires at least SMB v3</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs' || provider === 'nfs' || provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupRemoteDir">Remote Directory</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.remoteDir" id="configureBackupRemoteDir" name="remoteDir" ng-disabled="busy" placeholder="/share" ng-required="provider === 'cifs' || provider === 'nfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupUsername">Username ({{ provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.username" id="configureBackupUsername" name="cifsUsername" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupPassword">Password ({{ provider }})</label>
|
||||
<input type="password" class="form-control" ng-model="mountOptions.password" id="configureBackupPassword" name="cifsPassword" ng-disabled="busy" password-reveal>
|
||||
</div>
|
||||
|
||||
<!-- EXT4/XFS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.diskPath }" ng-show="provider === 'ext4' || provider === 'xfs'">
|
||||
<label class="control-label" for="inputConfigureDiskPath">Disk Path</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="busy" placeholder="Directory for backups" ng-required="provider === 'ext4' || provider === 'xfs'">
|
||||
</div>
|
||||
|
||||
<!-- Disk -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.diskPath }" ng-show="provider === 'disk'">
|
||||
<label class="control-label">Device</label>
|
||||
<select class="form-control" ng-model="disk" ng-options="item as item.label for item in blockDevices track by item.path" ng-required="provider === 'disk'"></select>
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPort">SSH Port</label>
|
||||
<input type="number" class="form-control" ng-model="mountOptions.port" id="configureBackupPort" name="port" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupUser">SSH User</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.user" id="configureBackupUser" name="user" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPrivateKey">SSH Private Key</label>
|
||||
<textarea class="form-control" ng-model="mountOptions.privateKey" id="configureBackupPrivateKey" name="privateKey" ng-disabled="busy"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.backupFolder }" ng-show="provider === 'filesystem'">
|
||||
<label class="control-label" for="inputConfigureBackupFolder">Local backup directory</label>
|
||||
<input type="text" class="form-control" ng-model="backupFolder" id="inputConfigureBackupFolder" name="backupFolder" ng-disabled="busy" placeholder="Directory for backups" ng-required="provider === 'filesystem'">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.endpoint }" ng-show="provider === 'minio' || provider === 'upcloud-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2' || provider === 's3-v4-compat' || provider === 'idrive-e2'">
|
||||
<label class="control-label" for="inputConfigureBackupEndpoint">Endpoint</label>
|
||||
<input type="text" class="form-control" ng-model="endpoint" id="inputConfigureBackupEndpoint" name="endpoint" ng-disabled="busy" placeholder="URL" ng-required="provider === 'minio' || provider === 'upcloud-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2' || provider === 's3-v4-compat' || provider === 'idrive-e2'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="provider === 'minio' || provider === 's3-v4-compat'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="acceptSelfSignedCerts" id="inputConfigureBackupSelfSigned">
|
||||
Accept Self-signed certificate
|
||||
</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.bucket }" ng-show="s3like(provider) || provider === 'gcs'">
|
||||
<label class="control-label" for="inputConfigureBackupBucket">Bucket name</label>
|
||||
<input type="text" class="form-control" ng-model="bucket" id="inputConfigureBackupBucket" name="bucket" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.prefix }" ng-show="provider !== 'filesystem' && provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPrefix">Prefix</label>
|
||||
<input type="text" class="form-control" ng-model="prefix" id="inputConfigureBackupPrefix" name="prefix" ng-disabled="busy" placeholder="Prefix for backup file names">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 's3'">
|
||||
<label class="control-label" for="inputConfigureBackupS3Region">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupS3Region" ng-model="region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="busy" ng-required="provider === 's3'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 's3-v4-compat'">
|
||||
<label class="control-label" for="inputConfigureBackupS3V4CompatRegion">Region</label>
|
||||
<input class="form-control" type="text" name="region" id="inputConfigureBackupS3V4CompatRegion" ng-model="region" ng-disabled="busy" placeholder="Leave empty to use us-east-1 as default"></input>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'digitalocean-spaces'">
|
||||
<label class="control-label" for="inputConfigureBackupDORegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupDORegion" ng-model="endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="busy" ng-required="provider === 'digitalocean-spaces'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'exoscale-sos'">
|
||||
<label class="control-label" for="inputConfigureBackupExoscaleRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupExoscaleRegion" ng-model="endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="busy" ng-required="provider === 'exoscale-sos'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'wasabi'">
|
||||
<label class="control-label" for="inputConfigureBackupWasabiRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupWasabiRegion" ng-model="endpoint" ng-options="a.value as a.name for a in wasabiRegions" ng-disabled="busy" ng-required="provider === 'wasabi'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'scaleway-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupScalewayRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupScalewayRegion" ng-model="endpoint" ng-options="a.value as a.name for a in scalewayRegions" ng-disabled="busy" ng-required="provider === 'scaleway-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'linode-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupLinodeRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupLinodeRegion" ng-model="endpoint" ng-options="a.value as a.name for a in linodeRegions" ng-disabled="busy" ng-required="provider === 'linode-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'ovh-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupOvhRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupOvhRegion" ng-model="endpoint" ng-options="a.value as a.name for a in ovhRegions" ng-disabled="busy" ng-required="provider === 'ovh-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'ionos-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupIonosRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupIonosRegion" ng-model="endpoint" ng-options="a.value as a.name for a in ionosRegions" ng-disabled="busy" ng-required="provider === 'ionos-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'vultr-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupVultrRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupVultrRegion" ng-model="endpoint" ng-options="a.value as a.name for a in vultrRegions" ng-disabled="busy" ng-required="provider === 'vultr-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'contabo-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupContaboRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupContaboRegion" ng-model="endpoint" ng-options="a.value as a.name for a in contaboRegions" ng-disabled="busy" ng-required="provider === 'contabo-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.accessKeyId }" ng-show="s3like(provider)">
|
||||
<label class="control-label" for="inputConfigureBackupAccessKeyId">Access key id</label>
|
||||
<input type="text" class="form-control" ng-model="accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.secretAccessKey }" ng-show="s3like(provider)">
|
||||
<label class="control-label" for="inputConfigureBackupSecretAccessKey">Secret access key</label>
|
||||
<input type="text" class="form-control" ng-model="secretAccessKey" id="inputConfigureBackupSecretAccessKey" name="secretAccessKey" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.gcsKeyInput }" ng-show="provider === 'gcs'">
|
||||
<label class="control-label" for="gcsKeyInput">Service Account Key</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="gcsKey.keyFileName" id="gcsKeyInput" name="cert" onclick="getElementById('gcsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="busy" ng-required="provider === 'gcs'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageFormat">Storage Format</label>
|
||||
<select class="form-control" id="storageFormat" ng-change="key = ''" ng-model="format" ng-options="a.value as a.name for a in formats"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.remotePath }">
|
||||
<label class="control-label" for="inputConfigureRemotePath">Backup Path</label>
|
||||
|
||||
<input type="text" class="form-control" ng-model="remotePath" name="inputConfigureBackupId" placeholder="Backup Path" required ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.key }">
|
||||
<label class="control-label" for="inputConfigureBackupPassword">Encryption password <span ng-hide="encrypted">(optional)</span></label>
|
||||
<input type="text" class="form-control" ng-model="password" id="inputConfigureBackupPassword" name="prefix" ng-disabled="busy" placeholder="Passphrase used to encrypt the backups" ng-required="encrypted">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="format === 'rsync' && password.length !== 0">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="encryptedFilenames">Decrypt Filenames</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="skipDnsSetup"><b>Dry run</b></sup>
|
||||
</label>
|
||||
<br/>
|
||||
<small>When enabled, apps are restored but the DNS records are not updated to point to this server. To access the dashboard, this browser's host must have an entry in <code>/etc/hosts</code> for the dashboard domain to this server's IP.
|
||||
See the <a href="https://docs.cloudron.io/backups/#dry-run" target="_blank">docs</a> for more information.</small>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="configureBackupForm.$invalid"/>
|
||||
|
||||
<div uib-collapse="!advancedVisible">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">IP Configuration <sup><a ng-href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="sysinfo.provider" ng-options="a.value as a.name for a in sysinfoProvider"></select>
|
||||
</div>
|
||||
|
||||
<!-- Fixed -->
|
||||
<div class="form-group" ng-show="sysinfo.provider === 'fixed'" ng-class="{ 'has-error': error.ipv4 }">
|
||||
<label class="control-label">IP Address</label>
|
||||
<input type="text" class="form-control" ng-model="sysinfo.ipv4" name="ipv4" ng-disabled="sysinfo.busy" ng-required="sysinfo.provider === 'fixed'">
|
||||
<p class="has-error" ng-show="error.ipv4">{{ error.ipv4 }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Network Interface -->
|
||||
<div class="form-group" ng-show="sysinfo.provider === 'network-interface'" ng-class="{ 'has-error': error.ifname }">
|
||||
<label class="control-label">Interface Name</label>
|
||||
<input type="text" class="form-control" ng-model="sysinfo.ifname" name="ifname" ng-disabled="sysinfo.busy" ng-required="sysinfo.provider === 'network-interface'">
|
||||
<p class="has-error" ng-show="error.ifname">{{ error.ifname }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="" ng-click="advancedVisible = true" ng-hide="advancedVisible">Advanced settings...</a>
|
||||
<a href="" ng-click="advancedVisible = false" ng-show="advancedVisible">Hide Advanced settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="configureBackupForm.$invalid"/><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> Restore</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2022 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,155 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Cloudron Setup</title>
|
||||
<meta name="description" content="Cloudron Setup">
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/setup.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="setup" ng-app="Application" ng-controller="SetupController">
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://docs.cloudron.io/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> Cloudron is offline. Reconnecting...</a>
|
||||
|
||||
<div class="main-container" ng-show="initialized">
|
||||
<div class="row" ng-show="view === 'owner'">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px;">
|
||||
<form role="form" name="ownerForm" ng-submit="owner.submit()" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1>Welcome to Cloudron</h1>
|
||||
<h3>Set up Admin Account</h3>
|
||||
<p class="has-error text-center" ng-show="owner.error.generic">{{ owner.error.generic }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="form-group" ng-class="{ 'has-error': ownerForm.displayName.$dirty && ownerForm.displayName.$invalid }">
|
||||
<label class="control-label" for="inputDisplayName">Full Name</label>
|
||||
<input type="text" class="form-control" ng-model="owner.displayName" id="inputDisplayName" name="displayName" placeholder="Full Name" required autocomplete="off" ng-disabled="owner.busy" autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (ownerForm.email.$dirty && ownerForm.email.$invalid) || (!ownerForm.email.$dirty && owner.error.email) }">
|
||||
<label class="control-label" for="inputEmail">Email <sup><a ng-href="https://docs.cloudron.io/installation/#admin-account" class="help" target="_blank" tabIndex="-1"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="email" class="form-control" ng-model="owner.email" id="inputEmail" name="email" placeholder="Email" required autocomplete="off" ng-disabled="owner.busy">
|
||||
<small>A valid email is required for Let's Encrypt certificates. This email is local to your Cloudron. </small>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (ownerForm.username.$dirty && ownerForm.username.$invalid) || (!ownerForm.username.$dirty && owner.error.username) }">
|
||||
<label class="control-label" for="inputUsername">Username</label>
|
||||
<input type="text" class="form-control" ng-model="owner.username" id="inputUsername" name="username" placeholder="Username" ng-maxlength="512" ng-minlength="1" required autocomplete="off" ng-disabled="owner.busy">
|
||||
<small>{{ owner.error.username }}</small>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': ownerForm.password.$dirty && ownerForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" ng-model="owner.password" id="inputPassword" name="password" placeholder="Password" ng-pattern="/^.{8,}$/" required autocomplete="off" ng-disabled="owner.busy" password-reveal>
|
||||
<small><span ng-show="ownerForm.password.$dirty && ownerForm.password.$invalid">Password must be at least 8 characters</span> </small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" class="btn btn-success" ng-disabled="ownerForm.$invalid || owner.busy"><i class="fa fa-circle-notch fa-spin" ng-show="owner.busy"></i> Create Admin</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="view === 'finished'">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px 40px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1>Cloudron is ready to use</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Before you start:
|
||||
<ul class="fa-ul">
|
||||
<li><i class="fa-li fa fa-users"></i>
|
||||
<b>User management</b>: Cloudron has a central user directory. When installing an app,
|
||||
you can set it up to authenticate against this directory.
|
||||
</li>
|
||||
<br/>
|
||||
<li><i class="fa-li fa fa-envelope-open"></i>
|
||||
<b>Email Configuration</b>: Apps are configured to send email based on the settings in the Email view.
|
||||
This saves you the trouble of having to configure mail settings inside each app.
|
||||
</li>
|
||||
<br/>
|
||||
<li><i class="fa-li fa fa-archive"></i>
|
||||
<b>Backups</b>: Store your backups on storage services completely independent from your server.
|
||||
You can use backups to seamlessly migrate your setup to another server.
|
||||
</li>
|
||||
<br/>
|
||||
<li><i class="fa-li fa fa-birthday-cake"></i>
|
||||
<b>Updates</b>: The Cloudron team tracks upstream releases and publishes app updates after testing.
|
||||
Your apps are kept fresh & secure.
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<a class="btn btn-success" href="/">Proceed to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2022 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,163 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src <%= apiOrigin %> 'unsafe-inline' 'unsafe-eval' 'self'; img-src <%= apiOrigin %> 'self'" />
|
||||
|
||||
<title>Cloudron Account Setup</title>
|
||||
<meta name="description" content="Cloudron Account Setup">
|
||||
|
||||
<link id="favicon" href="<%= apiOrigin %>/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/setupaccount.js?<%= revision %>"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body ng-app="Application" ng-controller="SetupAccountController">
|
||||
|
||||
<div class="layout-root" ng-show="initialized">
|
||||
|
||||
<div class="layout-content" ng-show="view === 'setup'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h1><small>{{ 'setupAccount.welcomeTo' | tr }}</small> {{ branding.cloudronName || 'Cloudron' }}</h1>
|
||||
<h3>{{ 'setupAccount.description' | tr }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 class="has-error" ng-show="error">{{ error.message }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form name="setupAccountForm" ng-submit="onSubmit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': ((setupAccountForm.username.$dirty && setupAccountForm.username.$invalid) || (!setupAccountForm.username.$dirty && error.username))}">
|
||||
<label class="control-label" for="inputUsername">{{ 'setupAccount.username' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" id="inputUsername" ng-disabled="profileLocked || existingUsername" required autofocus>
|
||||
<div class="control-label" ng-show="setupAccountForm.username.$dirty && setupAccountForm.username.$invalid">
|
||||
<small ng-show="setupAccountForm.username.$error.minlength">{{ 'setupAccount.errorUsernameTooShort' | tr }}</small>
|
||||
<small ng-show="setupAccountForm.username.$error.maxlength">{{ 'setupAccount.errorUsernameTooLong' | tr }}</small>
|
||||
<small ng-show="setupAccountForm.username.$dirty && setupAccountForm.username.$invalid">{{ 'setupAccount.errorUsernameInvalid' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputDisplayName">{{ 'setupAccount.fullName' | tr }}</label>
|
||||
<input type="displayName" class="form-control" ng-model="displayName" name="displayName" id="inputDisplayName" ng-disabled="profileLocked" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupAccountForm.password.$dirty && setupAccountForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputPassword">{{ 'setupAccount.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" id="inputPassword" ng-pattern="/^.{8,}$/" required password-reveal>
|
||||
<div class="control-label" ng-show="setupAccountForm.password.$dirty && setupAccountForm.password.$invalid">
|
||||
<small ng-show="setupAccountForm.password.$dirty && setupAccountForm.password.$invalid">{{ 'setupAccount.errorPassword' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupAccountForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">{{ 'setupAccount.passwordRepeat' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" id="inputPasswordRepeat" required password-reveal>
|
||||
<div class="control-label" ng-show="setupAccountForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="setupAccountForm.passwordRepeat.$dirty && (password !== passwordRepeat)">{{ 'setupAccount.errorPasswordNoMatch' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || setupAccountForm.$invalid || password !== passwordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'setupAccount.setupAction' | tr }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-content" ng-show="view === 'noUsername'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2>{{ 'setupAccount.noUsername.title' | tr }}</h2>
|
||||
<br/>
|
||||
<p>{{ 'setupAccount.noUsername.description' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-content" ng-show="view === 'invalidToken'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2>{{ 'setupAccount.invalidToken.title' | tr }}</h2>
|
||||
<br/>
|
||||
<p>{{ 'setupAccount.invalidToken.description' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-content" ng-show="view === 'done'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2>{{ 'setupAccount.success.title' | tr }}</h2>
|
||||
<br/>
|
||||
<a href="/" class="btn btn-primary">{{ 'setupAccount.success.openDashboardAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center ng-cloak">
|
||||
<span class="text-muted" ng-bind-html="branding.footer | markdown2html"></span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,335 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Cloudron Domain Setup</title>
|
||||
<meta name="description" content="Cloudron Domain Setup">
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
|
||||
|
||||
<!-- Angular directives for tldjs -->
|
||||
<script type="text/javascript" src="/3rdparty/js/tld.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/clipboard.min.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/setupdns.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="setup" ng-app="Application" ng-controller="SetupDNSController">
|
||||
|
||||
<div class="main-container ng-cloak text-center" ng-show="state === 'waitingForDnsSetup' || state === 'waitingForBox'">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3 text-center">
|
||||
<i class="fa fa-circle-notch fa-spin fa-5x"></i><br/>
|
||||
<h3>{{ message }} ...</h3>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<p>
|
||||
Please wait while Cloudron is setting up the dashboard at my.{{dnsCredentials.domain}}.<br/>
|
||||
You can follow the logs on the server at <code class="clipboard hand" data-clipboard-text="/home/yellowtent/platformdata/logs/box.log" uib-tooltip="{{ clipboardDone ? 'Copied' : 'Click to copy' }}" tooltip-placement="right">/home/yellowtent/platformdata/logs/box.log</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container ng-cloak" ng-show="state === 'initialized'">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px;">
|
||||
<form name="dnsCredentialsForm" role="form" novalidate ng-submit="setDnsCredentials()" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<h1>Domain Setup</h1>
|
||||
<p class="has-error text-center" ng-show="error.setup">{{ error.setup }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.domain.$dirty && dnsCredentialsForm.domain.$invalid }">
|
||||
<label class="control-label">Domain <sup><a ng-href="https://docs.cloudron.io/installation/#domain-setup" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
|
||||
<div class="text-danger" ng-show="dnsCredentials.domain.indexOf('my.') === 0 && dnsCredentials.domain.length > 3">Are you sure about this domain? The dashboard will be at <b>my.{{ dnsCredentials.domain }}</b></div>
|
||||
<p style="margin-top: 5px; font-size: 13px;">
|
||||
Apps will be installed on subdomains of this domain. The dashboard will be available on the <b>my</b> subdomain. You can add more domains later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<h3 class="text-center">Domain Configuration <sup><a ng-href="https://docs.cloudron.io/domains/#dns-providers" class="help" target="_blank" tabindex="-1"><i class="fa fa-question-circle"></i></a></sup> </h3>
|
||||
<p class="has-error text-center" ng-show="error.dnsCredentials">{{ error.dnsCredentials }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">DNS Provider</label>
|
||||
<select class="form-control" ng-model="dnsCredentials.provider" ng-options="a.value as a.name for a in dnsProvider" ng-disabled="dnsCredentials.busy" ng-change="setDefaultTlsProvider()"></select>
|
||||
</div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.accessKeyId.$dirty && dnsCredentialsForm.accessKeyId.$invalid }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label">Access Key Id</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.accessKeyId" name="accessKeyId" placeholder="Access Key Id" ng-minlength="16" ng-maxlength="32" ng-required="dnsCredentials.provider === 'route53'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.secretAccessKey.$dirty && dnsCredentialsForm.secretAccessKey.$invalid }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label">Secret Access Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" name="secretAccessKey" placeholder="Secret Access Key" ng-required="dnsCredentials.provider === 'route53'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud DNS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'gcdns'">
|
||||
<label class="control-label">Service Account Key</label>
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcdnsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="dnsCredentials.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-required="dnsCredentials.provider === 'gcdns'" ng-disabled="dnsCredentials.busy">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcdnsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.digitalOceanToken.$dirty && dnsCredentialsForm.digitalOceanToken.$invalid }" ng-show="dnsCredentials.provider === 'digitalocean'">
|
||||
<label class="control-label">DigitalOcean Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" name="digitalOceanToken" placeholder="API Token" ng-required="dnsCredentials.provider === 'digitalocean'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Gandi -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.gandiApiKey.$dirty && dnsCredentialsForm.gandiApiKey.$invalid }" ng-show="dnsCredentials.provider === 'gandi'">
|
||||
<label class="control-label">Gandi API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.gandiApiKey" name="gandiApiKey" placeholder="API Key" ng-required="dnsCredentials.provider === 'gandi'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- GoDaddy -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.godaddyApiKey.$dirty && dnsCredentialsForm.godaddyApiKey.$invalid }" ng-show="dnsCredentials.provider === 'godaddy'">
|
||||
<label class="control-label">API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.godaddyApiKey" name="godaddyApiKey" placeholder="API Key" ng-minlength="1" ng-required="dnsCredentials.provider === 'godaddy'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.godaddyApiSecret.$dirty && dnsCredentialsForm.godaddyApiSecret.$invalid }" ng-show="dnsCredentials.provider === 'godaddy'">
|
||||
<label class="control-label">API Secret</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.godaddyApiSecret" name="godaddyApiSecret" placeholder="API Secret" ng-required="dnsCredentials.provider === 'godaddy'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Netcup -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.netcupCustomerNumber.$dirty && dnsCredentialsForm.netcupCustomerNumber.$invalid }" ng-show="dnsCredentials.provider === 'netcup'">
|
||||
<label class="control-label">Customer Number</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.netcupCustomerNumber" name="netcupCustomerNumber" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'netcup'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.netcupApiKey.$dirty && dnsCredentialsForm.netcupApiKey.$invalid }" ng-show="dnsCredentials.provider === 'netcup'">
|
||||
<label class="control-label">API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.netcupApiKey" name="netcupApiKey" ng-disabled="dnsCredentials.busy" ng-minlength="1" ng-required="dnsCredentials.provider === 'netcup'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.netcupApiPassword.$dirty && dnsCredentialsForm.netcupApiPassword.$invalid }" ng-show="dnsCredentials.provider === 'netcup'">
|
||||
<label class="control-label">API Password</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.netcupApiPassword" name="netcupApiPassword" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'netcup'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareToken.$dirty && dnsCredentialsForm.cloudflareToken.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label class="control-label">Token Type</label>
|
||||
<select class="form-control" ng-model="dnsCredentials.cloudflareTokenType">
|
||||
<option value="GlobalApiKey">Global API Key</option>
|
||||
<option value="ApiToken">API Token</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareToken.$dirty && dnsCredentialsForm.cloudflareToken.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label class="control-label" ng-show="dnsCredentials.cloudflareTokenType === 'GlobalApiKey'">Global API Key</label>
|
||||
<label class="control-label" ng-show="dnsCredentials.cloudflareTokenType === 'ApiToken'">Api Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.cloudflareToken" name="cloudflareToken" placeholder="API Key/Token" ng-required="dnsCredentials.provider === 'cloudflare'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareEmail.$dirty && dnsCredentialsForm.cloudflareEmail.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare' && dnsCredentials.cloudflareTokenType === 'GlobalApiKey'">
|
||||
<label class="control-label">Cloudflare Email</label>
|
||||
<input type="email" class="form-control" ng-model="dnsCredentials.cloudflareEmail" name="cloudflareEmail" placeholder="Cloudflare Account Email" ng-required="dnsCredentials.provider === 'cloudflare' && dnsCredentials.cloudflareTokenType === 'GlobalApiKey'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="dnsCredentials.cloudflareDefaultProxyStatus"> Enable proxying for new DNS records
|
||||
<sup><a ng-href="https://docs.cloudron.io/domains/#cloudflare-dns" class="help" target="_blank" tabIndex="-1"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Name.com -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.nameComUsername.$dirty && dnsCredentialsForm.nameComUsername.$invalid }" ng-show="dnsCredentials.provider === 'namecom'">
|
||||
<label class="control-label">Name.com Username</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.nameComUsername" name="nameComUsername" placeholder="Name.com Username" ng-required="dnsCredentials.provider === 'namecom'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.nameComToken.$dirty && dnsCredentialsForm.nameComToken.$invalid }" ng-show="dnsCredentials.provider === 'namecom'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.nameComToken" name="nameComToken" placeholder="Name.com API Token" ng-required="dnsCredentials.provider === 'namecom'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Namecheap -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.namecheapUsername.$dirty && dnsCredentialsForm.namecheapUsername.$invalid }" ng-show="dnsCredentials.provider === 'namecheap'">
|
||||
<label class="control-label">Namecheap Username</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.namecheapUsername" name="namecheapUsername" placeholder="Namecheap Username" ng-required="dnsCredentials.provider === 'namecheap'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.namecheapApiKey.$dirty && dnsCredentialsForm.namecheapApiKey.$invalid }" ng-show="dnsCredentials.provider === 'namecheap'">
|
||||
<label class="control-label">API Key</label>
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'namecheap'"><b>The server IP needs to be whitelisted for this API Key.</b></p>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.namecheapApiKey" name="namecheapApiKey" placeholder="Namecheap API Key" ng-required="dnsCredentials.provider === 'namecheap'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Linode -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'linode'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.linodeToken" name="linodeToken" ng-required="dnsCredentials.provider === 'linode'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Bunny -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'bunny'">
|
||||
<label class="control-label">Access Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.bunnyAccessKey" name="bunnyAccessKey" ng-required="dnsCredentials.provider === 'bunny'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Porkbun -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.porkbunApikey.$dirty && dnsCredentialsForm.porkbunApikey.$invalid }" ng-show="dnsCredentials.provider === 'porkbun'">
|
||||
<label class="control-label">API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.porkbunApikey" name="porkbunApikey" placeholder="API Key" ng-minlength="1" ng-required="dnsCredentials.provider === 'porkbun'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.porkbunSecretapikey.$dirty && dnsCredentialsForm.porkbunSecretapikey.$invalid }" ng-show="dnsCredentials.provider === 'porkbun'">
|
||||
<label class="control-label">API Secret</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.porkbunSecretapikey" name="porkbunSecretapikey" placeholder="API Secret" ng-required="dnsCredentials.provider === 'porkbun'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Hetzner -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'hetzner'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.hetznerToken" name="hetznerToken" ng-required="dnsCredentials.provider === 'hetzner'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Vultr -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'vultr'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.vultrToken" name="vultrToken" ng-required="dnsCredentials.provider === 'vultr'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Wildcard -->
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'wildcard'">
|
||||
<span>Set up A records for <b>*.{{ dnsCredentials.domain || 'example.com' }}.</b> and <b>{{ dnsCredentials.domain || 'example.com' }}.</b> to this server's IP.</span>
|
||||
</p>
|
||||
|
||||
<!-- Manual -->
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'manual'">
|
||||
<span>Set up an A record for <b>my.{{ dnsCredentials.domain || 'example.com' }}.</b> to this server's IP.<br/></span>
|
||||
</p>
|
||||
|
||||
<p class="small text-info" ng-show="needsPort80(dnsCredentials.provider, dnsCredentials.tlsConfig.provider)">Let's Encrypt requires your server to be reachable on port 80</p>
|
||||
|
||||
<div ng-show="provider === 'ami'">
|
||||
<h3 class="text-center">Owner verification</h3>
|
||||
<p class="has-error text-center" ng-show="error.ami">{{ error.ami }}</p>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.instanceId.$dirty && (dnsCredentialsForm.instanceId.$invalid || error.ami) }">
|
||||
<label class="control-label">EC2 Instance Id</label>
|
||||
<input type="text" class="form-control" ng-model="instanceId" id="inputInstanceId" name="instanceId" placeholder="i-0123456789abcdefg" ng-minlength="1" ng-required="provider === 'ami'" autocomplete="off">
|
||||
</div>
|
||||
<p style="margin-top: 5px; font-size: 13px;">Provide the EC2 instance id to verify you have access to this server.</p>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div uib-collapse="!advancedVisible">
|
||||
<div class="form-group">
|
||||
<label class="control-label">Zone Name (Optional) <sup><a ng-href="https://docs.cloudron.io/domains/#zone-name" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.zoneName" name="zoneName" placeholder="{{dnsCredentials.domain | zoneName}}" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Certificate Provider <sup><a ng-href="https://docs.cloudron.io/certificates/#certificate-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="dnsCredentials.tlsConfig.provider" ng-options="a.value as a.name for a in tlsProvider" ng-disabled="dnsCredentials.busy"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">IP Configuration <sup><a ng-href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="sysinfo.provider" ng-options="a.value as a.name for a in sysinfoProvider"></select>
|
||||
</div>
|
||||
|
||||
<!-- Fixed -->
|
||||
<div class="form-group" ng-show="sysinfo.provider === 'fixed'" ng-class="{ 'has-error': error.ipv4 }">
|
||||
<label class="control-label">IP Address</label>
|
||||
<input type="text" class="form-control" ng-model="sysinfo.ipv4" name="ipv4" ng-disabled="sysinfo.busy" ng-required="sysinfo.provider === 'fixed'">
|
||||
<p class="has-error" ng-show="error.ipv4">{{ error.ipv4 }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Network Interface -->
|
||||
<div class="form-group" ng-show="sysinfo.provider === 'network-interface'" ng-class="{ 'has-error': error.ifname }">
|
||||
<label class="control-label">Interface Name</label>
|
||||
<input type="text" class="form-control" ng-model="sysinfo.ifname" name="ifname" ng-disabled="sysinfo.busy" ng-required="sysinfo.provider === 'network-interface'">
|
||||
<p class="has-error" ng-show="error.ifname">{{ error.ifname }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a href="" ng-click="advancedVisible = true" ng-hide="advancedVisible">Advanced settings...</a>
|
||||
<a href="" ng-click="advancedVisible = false" ng-show="advancedVisible">Hide Advanced settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="dnsCredentialsForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="dnsCredentials.busy"></i> Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center"><small>Looking to <a ng-href="/restore.html{{ search }}">restore?</a></small></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2022 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,120 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title> Login to <%= title %> </title>
|
||||
|
||||
<link href="<%= icon %>" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="<%= dashboardOrigin %>/theme.css">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="<%= dashboardOrigin %>/3rdparty/fontawesome/css/all.css"/>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="layout-root">
|
||||
|
||||
<div class="layout-content">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= icon %>"/>
|
||||
<br/>
|
||||
<h1><small>{{ login.loginTo }}</small> <%= title %></h1>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 class="has-error" id="message"></h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form name="loginForm" onsubmit="return onLogin(event)">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputUsername">{{ login.username }}</label>
|
||||
<input type="text" class="form-control" id="inputUsername" name="username" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPassword">{{ login.password }}</label>
|
||||
<input type="password" class="form-control" name="password" id="inputPassword" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputTotpToken">{{ login.2faToken }}</label>
|
||||
<input type="text" class="form-control" name="totpToken" id="inputTotpToken" value="">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" id="login">{{ login.signInAction }}</button>
|
||||
</form>
|
||||
<!-- <a ng-href="" class="hand" ng-click="showPasswordReset()">Reset password</a> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
function onLogin(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var username = document.getElementById('inputUsername').value;
|
||||
var password = document.getElementById('inputPassword').value;
|
||||
var totpToken = document.getElementById('inputTotpToken').value;
|
||||
|
||||
fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
redirect: 'manual',
|
||||
body: JSON.stringify({ username: username, password: password, totpToken: totpToken })
|
||||
}).then(function (response) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
document.getElementById('message').innerText = "{{ login.errorIncorrectCredentials }}"; // FIXME this needs proper escaping for translated strings, single quotes break easily!
|
||||
return;
|
||||
}
|
||||
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
window.location.href = search.redirect || '/';
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// patch up for password reveal see dashboard/js/utils.js
|
||||
var element = document.getElementById('inputPassword');
|
||||
|
||||
var svgEye = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>';
|
||||
var svgEyeSlash = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>';
|
||||
|
||||
var eye = document.createElement('i');
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
eye.style.width = '18px';
|
||||
eye.style.height = '18px';
|
||||
eye.style.position = 'relative';
|
||||
eye.style.float = 'right';
|
||||
eye.style.marginTop = '-24px';
|
||||
eye.style.marginRight = '10px';
|
||||
eye.style.cursor = 'pointer';
|
||||
|
||||
eye.addEventListener('click', function () {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
eye.innerHTML = svgEye;
|
||||
} else {
|
||||
element.type = 'password';
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
}
|
||||
});
|
||||
|
||||
element.parentNode.style.position = 'relative';
|
||||
element.parentNode.insertBefore(eye, element.nextSibling);
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,156 +0,0 @@
|
||||
{
|
||||
"apps": {
|
||||
"title": "As Minhas Aplicações",
|
||||
"noApps": {
|
||||
"description": "Que tal instalar algumas? Vê a <a href=\"{{ appStoreLink }}\">Loja de Aplicações</a>",
|
||||
"title": "Sem aplicações instaladas!"
|
||||
},
|
||||
"groupsFilterHeader": "Selecionar Grupo",
|
||||
"addApplinkAction": "Adicionar Applink",
|
||||
"noAccess": {
|
||||
"title": "Não tem acesso a nenhuma aplicação.",
|
||||
"description": "Assim que tiver, elas vão aparecer aqui."
|
||||
},
|
||||
"configActionTooltip": "Configurar",
|
||||
"logsActionTooltip": "Eventos",
|
||||
"infoActionTooltip": "Informação",
|
||||
"adminPageActionTooltip": "Página de Adminstração",
|
||||
"searchPlaceholder": "Pesquisar Aplicações",
|
||||
"stateFilterHeader": "Todos os Estados",
|
||||
"tagsFilterHeader": "Etiquetas: {{ tags }}",
|
||||
"tagsFilterHeaderAll": "Todas as Etiquetas",
|
||||
"domainsFilterHeader": "Todos os Domínios",
|
||||
"auth": {
|
||||
"sso": "Entrar com as credenciais Cloudron",
|
||||
"nosso": "Entrar com conta dedicada",
|
||||
"email": "Entrar com endereço de email"
|
||||
},
|
||||
"addAppAction": "Adicionar Aplicação",
|
||||
"addAppproxyAction": "Adicionar Appproxy",
|
||||
"filter": {
|
||||
"clearAll": "Limpar Tudo"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"displayName": "Nome de Apresentação",
|
||||
"rebootDialog": {
|
||||
"warning": "Reiniciar o servidor irá causar que todas as aplicações instaladas neste Cloudron fiquem indisponíveis temporariamente!",
|
||||
"description": "Utilize isto para aplicar atualizações de segurança ou se experienciar comportamento inesperado. Todas as aplicações e serviços em execução neste Cloudron vão iniciar automaticamente quando o reinício estiver completo.",
|
||||
"title": "Realmente reiniciar o servidor?",
|
||||
"rebootAction": "Reiniciar agora"
|
||||
},
|
||||
"offline": "O Cloudron está offline. A ligar novamente…",
|
||||
"dialog": {
|
||||
"cancel": "Cancelar",
|
||||
"save": "Guardar",
|
||||
"close": "Fechar",
|
||||
"no": "Não",
|
||||
"yes": "Sim"
|
||||
},
|
||||
"logout": "Terminar Sessão",
|
||||
"username": "Nome de Utilizador",
|
||||
"actions": "Ações",
|
||||
"table": {
|
||||
"date": "Data"
|
||||
},
|
||||
"pagination": {
|
||||
"next": "seguinte",
|
||||
"prev": "anterior",
|
||||
"perPageSelector": "Mostrar {{ n }} por página"
|
||||
},
|
||||
"action": {
|
||||
"reboot": "Reiniciar",
|
||||
"logs": "Eventos"
|
||||
},
|
||||
"clipboard": {
|
||||
"copied": "Copiado para a área de transferência",
|
||||
"clickToCopy": "Clique para copiar",
|
||||
"clickToCopyBackupId": "Clique para copiar o ID da cópia de segurança"
|
||||
},
|
||||
"searchPlaceholder": "Pesquisar",
|
||||
"multiselect": {
|
||||
"selected": "{{ n }} selecionados",
|
||||
"select": "Selecionar",
|
||||
"filterPlaceholder": "Escreva para filtrar opções"
|
||||
},
|
||||
"prettyDate": {
|
||||
"justNow": "agora mesmo",
|
||||
"yeserday": "Ontem",
|
||||
"minutesAgo": "{{ m }} minutos atrás",
|
||||
"hoursAgo": "{{ h }} horas atrás"
|
||||
},
|
||||
"navbar": {
|
||||
"users": "Utilizadores"
|
||||
},
|
||||
"disableAction": "Desativar",
|
||||
"enableAction": "Ativar",
|
||||
"statusEnabled": "Ativado",
|
||||
"statusDisabled": "Desativado"
|
||||
},
|
||||
"appstore": {
|
||||
"category": {
|
||||
"analytics": "Estatísticas",
|
||||
"game": "Jogos",
|
||||
"project": "Gestão de Projetos",
|
||||
"all": "Tudo",
|
||||
"popular": "Popular",
|
||||
"newApps": "Novas Aplicações",
|
||||
"chat": "Chat",
|
||||
"blog": "Blog",
|
||||
"document": "Documentos",
|
||||
"crm": "CRM",
|
||||
"forum": "Fórum",
|
||||
"gallery": "Galeria",
|
||||
"finance": "Finanças",
|
||||
"git": "Alojamento de Código",
|
||||
"email": "Email",
|
||||
"hosting": "Alojamento Web",
|
||||
"media": "Multimédia",
|
||||
"learning": "Aprendizagem",
|
||||
"notes": "Notas",
|
||||
"sync": "Sincronização de Ficheiros",
|
||||
"wiki": "Wiki",
|
||||
"vpn": "VPN",
|
||||
"federated": "Federados"
|
||||
},
|
||||
"installDialog": {
|
||||
"lastUpdated": "Última atualização a {{ date }}",
|
||||
"locationPlaceholder": "Deixe em branco para usar o domínio de raiz",
|
||||
"userManagementNone": "Esta aplicação tem a sua própria gestão de utilizadores. Esta definição determina se a aplicação está ou não visível no painel do utilizador.",
|
||||
"memoryRequirement": "Requere pelo menos {{ size }} de memória",
|
||||
"location": "Localização",
|
||||
"manualWarning": "Adicione um registo A manualmente para <b>{{ location }}</b> apontando para o endereço IP público deste Cloudron",
|
||||
"userManagement": "Gestão de utilizadores",
|
||||
"userManagementMailbox": "Todos os utilizadores com uma caixa de correio neste Cloudron têm acesso.",
|
||||
"userManagementLeaveToApp": "Deixar a gestão de utilizadores para a aplicação",
|
||||
"userManagementAllUsers": "Permitir todos os utilizadores deste Cloudron",
|
||||
"installAnywayAction": "Instalar na mesma",
|
||||
"doInstallAction": "Instalar {{ dnsOverwrite ? 'e sobrescrever DNS' : '' }}",
|
||||
"userManagementSelectUsers": "Apenas permitir os seguintes utilizadores e grupos",
|
||||
"errorUserManagementSelectAtLeastOne": "Selecione pelo menos um utilizador ou grupo",
|
||||
"users": "Utilizadores",
|
||||
"groups": "Grupos",
|
||||
"configuredForCloudronEmail": "Esta aplicação está pré-configurada para ser utilizada com o <a href=\"{{ emailDocsLink }}\" target=\"_blank\">Email do Cloudron</a>.",
|
||||
"lowOnResources": "Este Cloudron está baixo em recursos.",
|
||||
"pleaseUpgradeServer": "Por favor, atualize para um servidor com mais memória. Em alternativa, liberte recursos removendo aplicações que não utiliza.",
|
||||
"subscriptionRequired": "Para instalar mais aplicação, uma subscrição paga é necessária.",
|
||||
"setupSubscriptionAction": "Configurar Subscrição",
|
||||
"installAction": "Instalar",
|
||||
"cloudflarePortWarning": "O proxy do Cloudflare deve estar desativado para o domínio da aplicação para que possa aceder a esta porta",
|
||||
"titleAndVersion": "Esta aplicação inclui {{ title }} {{ version }}"
|
||||
},
|
||||
"title": "Loja de Aplicações",
|
||||
"searchPlaceholder": "Pesquise por alternativas como Github, Dropbox, Slack, Trello, …",
|
||||
"noAppsFound": "Nenhuma aplicação encontrada.",
|
||||
"appMissing": "Falta uma aplicação? Contacte-nos.",
|
||||
"unstable": "Instável",
|
||||
"appNotFoundDialog": {
|
||||
"description": "Não existe nenhuma aplicação <b>{{ appId }}</b> com a versão <b>{{ version }}</b>.",
|
||||
"title": "Aplicação não encontrada"
|
||||
},
|
||||
"accountDialog": {
|
||||
"titleSignUp": "Registar com Cloudron.io",
|
||||
"titleLogin": "Entrar com Cloudron.io"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
<!-- Modal postinstall confirm -->
|
||||
<div class="modal fade" id="appPostInstallConfirmModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{appPostInstallConfirm.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon"/>
|
||||
<h5 class="app-info-title">
|
||||
{{ appPostInstallConfirm.app.manifest.title }}
|
||||
<span class="app-info-meta text-small">{{ 'app.appInfo.package' | tr }} <a ng-href="/#/appstore/{{appPostInstallConfirm.app.manifest.id}}?version={{appPostInstallConfirm.app.manifest.version}}">v{{ appPostInstallConfirm.app.manifest.version }}</a> </span>
|
||||
<br/>
|
||||
<span ng-show="appPostInstallConfirm.app.manifest.documentationUrl"><a target="_blank" ng-href="{{appPostInstallConfirm.app.manifest.documentationUrl}}">{{ 'app.docsAction' | tr }}</a> </span>
|
||||
<br/>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-show="appPostInstallConfirm.app.manifest.addons.email">{{ 'app.appInfo.ssoEmail' | tr }}</p>
|
||||
<p ng-show="appPostInstallConfirm.app.sso && !appPostInstallConfirm.app.manifest.addons.email">{{ 'app.appInfo.sso' | tr }}</p>
|
||||
|
||||
<div ng-bind-html="appPostInstallConfirm.app.manifest.postInstallMessage | markdown2html"></div>
|
||||
<div ng-show="appPostInstallConfirm.app.manifest.documentationUrl" ng-bind-html="'app.appInfo.appDocsUrl' | tr:{ docsUrl: appPostInstallConfirm.app.manifest.documentationUrl, title: appPostInstallConfirm.app.manifest.title, forumUrl: (appPostInstallConfirm.app.manifest.forumUrl || 'https://forum.cloudron.io') }"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-group pull-left">
|
||||
<input type="checkbox" id="appPostInstallConfirmCheckbox" ng-model="appPostInstallConfirm.confirmed">
|
||||
<label class="control-label" for="appPostInstallConfirmCheckbox">{{ 'app.appInfo.postInstallConfirmCheckbox' | tr }}</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<a class="btn btn-success" ng-href="{{ appPostInstallConfirm.confirmed ? ('https://' + appPostInstallConfirm.app.fqdn) : '' }}" target="_blank" ng-disabled="!appPostInstallConfirm.confirmed" ng-click="appPostInstallConfirm.submit()">{{ 'app.appInfo.openAction' | tr:{ app: appPostInstallConfirm.app.manifest.title } }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal applinks edit -->
|
||||
<div class="modal fade" id="applinksEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'app.editApplinkDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="applinksEditForm" role="form" ng-submit="applinksEdit.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (applinksEditForm.upstreamUri.$dirty && applinksEditForm.upstreamUri.$invalid) || (!applinksEditForm.upstreamUri.$dirty && applinksEdit.error.upstreamUri) }">
|
||||
<label class="control-label">{{ 'app.applinks.upstreamUri' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="applinksEdit.upstreamUri" name="upstreamUri" id="inputUpstreamUri" autofocus autocomplete="off" required>
|
||||
<span class="text-danger" ng-show="applinksEdit.error.upstreamUri">{{ applinksEdit.error.upstreamUri }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'app.applinks.label' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="applinksEdit.label" name="label" id="inputLabel" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label class="control-label">{{ 'app.display.icon' | tr }}</label>
|
||||
</div>
|
||||
<div id="previewIcon" class="app-custom-icon" ng-click="applinksEdit.showCustomIconSelector()">
|
||||
<img ng-src="{{ applinksEdit.iconUrl() || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)"/>
|
||||
<div class="overlay"></div>
|
||||
</div>
|
||||
<a href="" style="font-weight: normal;" ng-click="applinksEdit.resetCustomIcon()">{{ 'app.applinks.clearIconAction' | tr }}</a> - <span class="text-small">{{ 'app.applinks.clearIconDescription' | tr }}</span>
|
||||
<input type="file" id="applinksEditIconFileInput" style="display: none" accept="image/png"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'app.display.tags' | tr }}</label>
|
||||
<tag-input class="form-control" placeholder="{{ 'app.display.tagsPlaceholder' | tr }}" taglist="applinksEdit.tags" name="tags" uib-tooltip="{{ 'app.display.tagsTooltip' | tr }}"></tag-input>
|
||||
</div>
|
||||
|
||||
<label class="control-label">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="applinksEdit.accessRestrictionOption" value="any">
|
||||
<span>{{ 'app.accessControl.userManagement.visibleForAllUsers' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="applinksEdit.accessRestrictionOption" value="groups">
|
||||
<span>{{ 'app.accessControl.userManagement.visibleForSelected' | tr }}</span>
|
||||
<span class="label label-danger" ng-show="applinksEdit.accessRestrictionOption === 'groups' && !applinksEdit.isAccessRestrictionValid()">{{ 'appstore.installDialog.errorUserManagementSelectAtLeastOne' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-left: 20px; display: flex;">
|
||||
<div>
|
||||
{{ 'appstore.installDialog.users' | tr }}: <multiselect name="accessUsersSelect" class="input-sm stretch" ng-model="applinksEdit.accessRestriction.users" ng-disabled="applinksEdit.accessRestrictionOption !== 'groups'" options="(user.username || user.email) for user in allUsers" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ 'appstore.installDialog.groups' | tr }}: <multiselect name="accessGroupsSelect" class="input-sm stretch" ng-model="applinksEdit.accessRestriction.groups" ng-disabled="applinksEdit.accessRestrictionOption !== 'groups'" options="group.name for group in allGroups" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="applinksEditForm.$invalid || applinksEdit.busyEdit || applinks.busyRemove"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger pull-left" ng-click="applinksEdit.remove()" ng-disabled="applinksEdit.busyRemove || applinksEdit.busyEdit"><i class="fa fa-circle-notch fa-spin" ng-show="applinksEdit.busyRemove"></i> {{ 'app.editApplinkDialog.deleteAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="applinksEdit.submit()" ng-disabled="applinksEditForm.$invalid || applinksEdit.busyRemove || applinksEdit.busyEdit"><i class="fa fa-circle-notch fa-spin" ng-show="applinksEdit.busyEdit"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content content-large">
|
||||
|
||||
<!-- Workaround for select-all issue, see commit message -->
|
||||
<div style="font-size: 1px;"> </div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && user.isAtLeastAdmin">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<br/><br/><br/><br/>
|
||||
<h1><i class="fa fa-cloud-download fa-fw"></i> {{ 'apps.noApps.title' | tr }}</h1>
|
||||
<br/></br>
|
||||
<h3 ng-bind-html="'apps.noApps.description' | tr:{ appStoreLink: '#/appstore' }"></h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && !user.isAtLeastAdmin">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<br/><br/><br/><br/>
|
||||
<h1>{{ 'apps.noAccess.title' | tr }}</h1>
|
||||
<br/></br>
|
||||
<h3>{{ 'apps.noAccess.description' | tr }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="view-header" ng-show="installedApps.length > 0">
|
||||
{{ 'apps.title' | tr }}
|
||||
<div class="view-header-search-bar">
|
||||
<form class="form-inline">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" style="width: 300px" placeholder="{{ 'apps.searchPlaceholder' | tr }}" id="appSearch" ng-model="appSearch"/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-class="{ 'active': showFilter, 'btn-warning': showFilter || selectedTags.length || selectedState.state || !selectedGroup._unset || !selectedDomain._alldomains }" ng-click="showFilter = !showFilter"><i class="fas fa-filter"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div ng-show="showFilter" class="view-header-filter-bar">
|
||||
<form class="form-inline">
|
||||
<multiselect ng-model="selectedGroup" ng-show="user.isAtLeastAdmin && groups.length > 1" ms-header="{{ selectedGroup.name }}" options="group.name for group in groups" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<multiselect ng-model="selectedState" ng-show="user.isAtLeastAdmin" ms-header="{{ 'apps.stateFilterHeader' | tr }}" ms-selected="{{ selectedState }}" options="state.label for state in states" data-multiple="false"></multiselect>
|
||||
<multiselect ng-model="selectedTags" ng-show="user.isAtLeastAdmin && tags.length > 0" ms-header="{{ 'apps.tagsFilterHeaderAll' | tr }}" ms-selected="{{ 'apps.tagsFilterHeader' | tr:{ tags: selectedTags.join(', ') } }}" options="tag for tag in tags" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<multiselect ng-model="selectedDomain" data-compare-by="domain" ms-selected="{{ selectedDomain.domain }}" options="domain.domain for domain in filterDomains" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<button class="btn btn-warning" ng-disabled="!selectedTags.length && !selectedState.state && selectedGroup._unset && selectedDomain._alldomains" ng-click="clearAllFilter()">{{ 'apps.filter.clearAll' | tr }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
||||
<div class="app-grid">
|
||||
<div class="grid-item" ng-class="{ 'stopped': app.runState === 'stopped' }" ng-repeat="app in installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch | orderBy:'fqdn'">
|
||||
<div class="grid-item-content" uib-tooltip="{{ app.fqdn }}">
|
||||
<a ng-show="app.type !== APP_TYPES.LINK && isOperator(app)" ng-href="#/app/{{ app.id}}/display" class="btn btn-lg btn-default grid-item-action"><i class="fas fa-cog"></i></a>
|
||||
<div ng-show="app.type === APP_TYPES.LINK && isOperator(app)" ng-click="applinksEdit.show(app)" class="btn btn-lg btn-default grid-item-action"><i class="fas fa-cog"></i></div>
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="onAppClick(app, $event)" target="_blank">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
|
||||
<img ng-src="{{ app.iconUrl || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)" class="app-icon"/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center">
|
||||
<div class="grid-item-top-title" data-fittext>{{ app.label || app.subdomain || app.fqdn }}</div>
|
||||
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden" uib-tooltip="{{ app | appProgressMessage }}">
|
||||
{{ app | installationStateLabel }}
|
||||
</div>
|
||||
<div class="status" ng-style="{ 'visibility': isOperator(app) && (app | installationActive) ? 'visible' : 'hidden' }">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="usermanagement-indicator" ng-show="app.type !== APP_TYPES.LINK">
|
||||
<i class="fa-brands fa-openid" ng-show="app.ssoAuth && app.manifest.addons.oidc" uib-tooltip="{{ 'apps.auth.openid' | tr }}" tooltip-placement="right"></i>
|
||||
<i class="fas fa-user" ng-show="app.ssoAuth && (!app.manifest.addons.oidc && !app.manifest.addons.email)" uib-tooltip="{{ 'apps.auth.sso' | tr }}" tooltip-placement="right"></i>
|
||||
<i class="far fa-user" ng-show="!app.ssoAuth && !app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.nosso' | tr }}" tooltip-placement="right"></i>
|
||||
<i class="fas fa-envelope" ng-show="app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.email' | tr }}" tooltip-placement="right"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<div class="app-update-badge" ng-click="showAppConfigure(app, 'updates')" ng-show="config.update[app.id].manifest.version && config.update[app.id].manifest.version !== app.manifest.version && (app | installSuccess) && !(app.error || app.runState === 'stopped')">
|
||||
<i class="fa fa-arrow-up fa-inverse"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,314 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global APP_TYPES */
|
||||
/* global onAppClick */
|
||||
|
||||
angular.module('Application').controller('AppsController', ['$scope', '$translate', '$interval', '$location', 'Client', function ($scope, $translate, $interval, $location, Client) {
|
||||
var ALL_DOMAINS_DOMAIN = { _alldomains: true, domain: 'All Domains' }; // dummy record for the single select filter
|
||||
var GROUP_ACCESS_UNSET = { _unset: true, name: 'Select Group' }; // dummy record for the single select filter
|
||||
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.tags = Client.getAppTags();
|
||||
$scope.states = [
|
||||
{ state: '', label: 'All States' },
|
||||
{ state: 'running', label: 'Running' },
|
||||
{ state: 'stopped', label: 'Stopped' },
|
||||
{ state: 'update_available', label: 'Update Available' },
|
||||
{ state: 'not_responding', label: 'Not Responding' }
|
||||
];
|
||||
$scope.selectedState = $scope.states[0];
|
||||
$scope.selectedTags = [];
|
||||
$scope.selectedGroup = GROUP_ACCESS_UNSET;
|
||||
$scope.selectedDomain = ALL_DOMAINS_DOMAIN;
|
||||
$scope.filterDomains = [ ALL_DOMAINS_DOMAIN ];
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.domains = [];
|
||||
$scope.appSearch = '';
|
||||
$scope.groups = [ GROUP_ACCESS_UNSET ];
|
||||
$scope.APP_TYPES = APP_TYPES;
|
||||
$scope.showFilter = false;
|
||||
$scope.filterActive = false;
|
||||
|
||||
$scope.allUsers = [];
|
||||
$scope.allGroups = [];
|
||||
|
||||
$translate(['apps.stateFilterHeader', 'apps.domainsFilterHeader', 'apps.groupsFilterHeader', 'app.states.running', 'app.states.stopped', 'app.states.notResponding', 'app.states.updateAvailable']).then(function (tr) {
|
||||
if (tr['apps.domainsFilterHeader']) ALL_DOMAINS_DOMAIN.domain = tr['apps.domainsFilterHeader'];
|
||||
if (tr['apps.groupsFilterHeader']) GROUP_ACCESS_UNSET.name = tr['apps.groupsFilterHeader'];
|
||||
if (tr['apps.stateFilterHeader']) $scope.states[0].label = tr['apps.stateFilterHeader'];
|
||||
if (tr['app.states.running']) $scope.states[1].label = tr['app.states.running'];
|
||||
if (tr['app.states.stopped']) $scope.states[2].label = tr['app.states.stopped'];
|
||||
if (tr['app.states.notResponding']) $scope.states[4].label = tr['app.states.notResponding'];
|
||||
if (tr['app.states.updateAvailable']) $scope.states[3].label = tr['app.states.updateAvailable'];
|
||||
});
|
||||
|
||||
$scope.$watch('selectedTags', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
localStorage.selectedTags = newVal.join(',');
|
||||
});
|
||||
|
||||
$scope.$watch('selectedState', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
if (newVal === $scope.states[0]) localStorage.removeItem('selectedState');
|
||||
else localStorage.selectedState = newVal.state;
|
||||
});
|
||||
|
||||
$scope.$watch('selectedGroup', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
if (newVal === GROUP_ACCESS_UNSET) localStorage.removeItem('selectedGroup');
|
||||
else localStorage.selectedGroup = newVal.id;
|
||||
});
|
||||
|
||||
$scope.$watch('selectedDomain', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
if (newVal._alldomains) localStorage.removeItem('selectedDomain');
|
||||
else localStorage.selectedDomain = newVal.domain;
|
||||
});
|
||||
|
||||
$scope.onAppClick = function (app, $event) { onAppClick(app, $event, $scope.isOperator(app), $scope); };
|
||||
|
||||
$scope.clearAllFilter = function () {
|
||||
$scope.selectedState = $scope.states[0];
|
||||
$scope.selectedTags = [];
|
||||
$scope.selectedGroup = GROUP_ACCESS_UNSET;
|
||||
$scope.selectedDomain = ALL_DOMAINS_DOMAIN;
|
||||
};
|
||||
|
||||
$scope.appPostInstallConfirm = {
|
||||
app: {},
|
||||
message: '',
|
||||
confirmed: false,
|
||||
|
||||
show: function (app) {
|
||||
$scope.appPostInstallConfirm.app = app;
|
||||
$scope.appPostInstallConfirm.message = app.manifest.postInstallMessage;
|
||||
$scope.appPostInstallConfirm.confirmed = false;
|
||||
|
||||
$('#appPostInstallConfirmModal').modal('show');
|
||||
|
||||
return false; // prevent propagation and default
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
if (!$scope.appPostInstallConfirm.confirmed) return;
|
||||
|
||||
$scope.appPostInstallConfirm.app.pendingPostInstallConfirmation = false;
|
||||
delete localStorage['confirmPostInstall_' + $scope.appPostInstallConfirm.app.id];
|
||||
|
||||
$('#appPostInstallConfirmModal').modal('hide');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.applinksEdit = {
|
||||
error: {},
|
||||
busyEdit: false,
|
||||
busyRemove: false,
|
||||
applink: {},
|
||||
id: '',
|
||||
upstreamUri: '',
|
||||
label: '',
|
||||
tags: '',
|
||||
accessRestrictionOption: '',
|
||||
accessRestriction: { users: [], groups: [] },
|
||||
icon: { data: null },
|
||||
|
||||
iconUrl: function () {
|
||||
if ($scope.applinksEdit.icon.data === '__original__') { // user clicked reset
|
||||
// https://png-pixel.com/ white pixel placeholder
|
||||
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=';
|
||||
} else if ($scope.applinksEdit.icon.data) { // user uploaded icon
|
||||
return $scope.applinksEdit.icon.data;
|
||||
} else { // current icon
|
||||
return $scope.applinksEdit.applink.iconUrl;
|
||||
}
|
||||
},
|
||||
|
||||
resetCustomIcon: function () {
|
||||
$scope.applinksEdit.icon.data = '__original__';
|
||||
},
|
||||
|
||||
showCustomIconSelector: function () {
|
||||
$('#applinksEditIconFileInput').click();
|
||||
},
|
||||
|
||||
isAccessRestrictionValid: function () {
|
||||
return !!($scope.applinksEdit.accessRestriction.users.length || $scope.applinksEdit.accessRestriction.groups.length);
|
||||
},
|
||||
|
||||
show: function (applink) {
|
||||
$scope.applinksEdit.error = {};
|
||||
$scope.applinksEdit.busyEdit = false;
|
||||
$scope.applinksEdit.busyRemove = false;
|
||||
$scope.applinksEdit.applink = applink;
|
||||
$scope.applinksEdit.id = applink.id;
|
||||
$scope.applinksEdit.upstreamUri = applink.upstreamUri;
|
||||
$scope.applinksEdit.label = applink.label;
|
||||
$scope.applinksEdit.accessRestrictionOption = applink.accessRestriction ? 'groups' : 'any';
|
||||
$scope.applinksEdit.accessRestriction = { users: [], groups: [] };
|
||||
$scope.applinksEdit.icon = { data: null };
|
||||
|
||||
var userSet, groupSet;
|
||||
if (applink.accessRestriction) {
|
||||
userSet = {};
|
||||
applink.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; });
|
||||
$scope.allUsers.forEach(function (u) { if (userSet[u.id] === true) $scope.applinksEdit.accessRestriction.users.push(u); });
|
||||
|
||||
groupSet = {};
|
||||
if (applink.accessRestriction.groups) applink.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; });
|
||||
$scope.allGroups.forEach(function (g) { if (groupSet[g.id] === true) $scope.applinksEdit.accessRestriction.groups.push(g); });
|
||||
}
|
||||
|
||||
// translate for tag-input
|
||||
$scope.applinksEdit.tags = applink.tags ? applink.tags.join(' ') : '';
|
||||
|
||||
$scope.applinksEditForm.$setUntouched();
|
||||
$scope.applinksEditForm.$setPristine();
|
||||
|
||||
$('#applinksEditModal').modal('show');
|
||||
|
||||
return false; // prevent propagation and default
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.applinksEdit.busyEdit = true;
|
||||
$scope.applinksEdit.error = {};
|
||||
|
||||
var accessRestriction = null;
|
||||
if ($scope.applinksEdit.accessRestrictionOption === 'groups') {
|
||||
accessRestriction = { users: [], groups: [] };
|
||||
accessRestriction.users = $scope.applinksEdit.accessRestriction.users.map(function (u) { return u.id; });
|
||||
accessRestriction.groups = $scope.applinksEdit.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var data = {
|
||||
upstreamUri: $scope.applinksEdit.upstreamUri,
|
||||
label: $scope.applinksEdit.label,
|
||||
accessRestriction: accessRestriction,
|
||||
tags: $scope.applinksEdit.tags.split(' ').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; })
|
||||
};
|
||||
|
||||
if ($scope.applinksEdit.icon.data === '__original__') { // user reset the icon
|
||||
data.icon = '';
|
||||
} else if ($scope.applinksEdit.icon.data) { // user loaded custom icon
|
||||
data.icon = $scope.applinksEdit.icon.data.replace(/^data:image\/[a-z]+;base64,/, '');
|
||||
}
|
||||
|
||||
Client.updateApplink($scope.applinksEdit.id, data, function (error) {
|
||||
$scope.applinksEdit.busyEdit = false;
|
||||
|
||||
if (error && error.statusCode === 400 && error.message.includes('upstreamUri')) {
|
||||
$scope.applinksEdit.error.upstreamUri = error.message;
|
||||
$scope.applinksEditForm.$setUntouched();
|
||||
$scope.applinksEditForm.$setPristine();
|
||||
return;
|
||||
}
|
||||
if (error) return console.error('Failed to update applink', error);
|
||||
|
||||
Client.refreshInstalledApps();
|
||||
|
||||
$('#applinksEditModal').modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
$scope.applinksEdit.busyRemove = true;
|
||||
|
||||
Client.removeApplink($scope.applinksEdit.id, function (error) {
|
||||
$scope.applinksEdit.busyRemove = false;
|
||||
|
||||
if (error) return console.error('Failed to remove applink', error);
|
||||
|
||||
Client.refreshInstalledApps();
|
||||
|
||||
$('#applinksEditModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showAppConfigure = function (app, view) {
|
||||
$location.path('/app/' + app.id + '/' + view);
|
||||
};
|
||||
|
||||
$scope.isOperator = function (app) {
|
||||
return app.accessLevel === 'operator' || app.accessLevel === 'admin';
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
setTimeout(function () { $('#appSearch').focus(); }, 1);
|
||||
|
||||
// refresh the new list immediately when switching from another view (appstore)
|
||||
Client.refreshInstalledApps(function () {
|
||||
var refreshAppsTimer = $interval(Client.refreshInstalledApps.bind(Client, function () {}), 5000);
|
||||
$scope.$on('$destroy', function () {
|
||||
$interval.cancel(refreshAppsTimer);
|
||||
});
|
||||
});
|
||||
|
||||
if (!$scope.user.isAtLeastAdmin) return;
|
||||
|
||||
// load local settings and apply tag filter
|
||||
if (localStorage.selectedTags) {
|
||||
if (!$scope.tags.length) localStorage.removeItem('selectedTags');
|
||||
else $scope.selectedTags = localStorage.selectedTags.split(',');
|
||||
}
|
||||
|
||||
if (localStorage.selectedState) $scope.selectedState = $scope.states.find(function (s) { return s.state === localStorage.selectedState; }) || $scope.states[0];
|
||||
|
||||
Client.getGroups(function (error, result) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.groups = [ GROUP_ACCESS_UNSET ].concat(result);
|
||||
$scope.allGroups = result;
|
||||
|
||||
if (localStorage.selectedGroup) $scope.selectedGroup = $scope.groups.find(function (g) { return g.id === localStorage.selectedGroup; }) || GROUP_ACCESS_UNSET;
|
||||
});
|
||||
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.domains = result;
|
||||
$scope.filterDomains = [ALL_DOMAINS_DOMAIN].concat(result);
|
||||
|
||||
if (localStorage.selectedDomain) $scope.selectedDomain = $scope.filterDomains.find(function (d) { return d.domain === localStorage.selectedDomain; }) || ALL_DOMAINS_DOMAIN;
|
||||
});
|
||||
|
||||
Client.getAllUsers(function (error, users) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.allUsers = users;
|
||||
});
|
||||
});
|
||||
|
||||
$('#applinksEditIconFileInput').get(0).onchange = function (event) {
|
||||
var fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
$scope.$apply(function () {
|
||||
// var file = event.target.files[0];
|
||||
$scope.applinksEdit.icon.data = fr.result;
|
||||
});
|
||||
};
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
};
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['applinksAddModal', 'applinksEditModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.collapse').on('shown.bs.collapse', function(){
|
||||
$(this).parent().find('.fa-angle-right').removeClass('fa-angle-right').addClass('fa-angle-down');
|
||||
}).on('hidden.bs.collapse', function(){
|
||||
$(this).parent().find('.fa-angle-down').removeClass('fa-angle-down').addClass('fa-angle-right');
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,440 +0,0 @@
|
||||
|
||||
<!-- Modal install app -->
|
||||
<div class="modal fade appstore-install" id="appInstallModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{appInstall.app.iconUrl}}" onerror="this.onerror=null; this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
<h3 class="appstore-install-title">{{ appInstall.app.manifest.title }}</h3>
|
||||
<br/>
|
||||
<span class="appstore-install-meta"><a href="{{ appInstall.app.manifest.website }}" target="_blank">{{ appInstall.app.manifest.author }}</a></span>
|
||||
<br/>
|
||||
<span class="appstore-install-meta">{{ 'appstore.installDialog.lastUpdated' | tr:{ date: (appInstall.app.creationDate | prettyDate) } }}</span>
|
||||
<br/>
|
||||
<span class="appstore-install-meta">{{ 'appstore.installDialog.memoryRequirement' | tr:{ size: (appInstall.app.manifest.memoryLimit | prettyBinarySize:'256 MB') } }}</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="collapse" id="collapseInstallForm" data-toggle="false">
|
||||
<form role="form" name="appInstallForm" ng-submit="appInstall.submit()" autocomplete="off">
|
||||
<div class="has-error text-center" ng-show="appInstall.error.other" ng-bind-html="appInstall.error.other"></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appInstallForm.location.$dirty && appInstallForm.location.$invalid) || (!appInstallForm.location.$dirty && appInstall.error.location) }">
|
||||
<label class="control-label" for="appInstallLocationInput">{{ 'appstore.installDialog.location' | tr }}</label>
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appInstall.subdomain" id="appInstallLocationInput" name="location" placeholder="{{ 'appstore.installDialog.locationPlaceholder' | tr }}" autofocus>
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>{{ '.' + appInstall.domain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="appInstall.domain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="appInstall.error.location" class="text-small">{{ appInstall.error.location }}</div>
|
||||
</div>
|
||||
|
||||
<p class="text-small text-warning" ng-show="appInstall.domain.provider === 'noop' || appInstall.domain.provider === 'manual'" ng-bind-html="'appstore.installDialog.manualWarning' | tr:{ location: ((appInstall.subdomain ? appInstall.subdomain + '.' : '') + appInstall.domain.domain) }"></p>
|
||||
|
||||
<div class="has-error text-center" ng-show="appInstall.error.secondaryDomain">{{ appInstall.error.secondaryDomain }}</div>
|
||||
<div ng-repeat="(env, info) in appInstall.app.manifest.httpPorts">
|
||||
<ng-form name="secondaryDomainInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!secondaryDomainInfo_form.itemName{{$index}}.$dirty && appInstall.error.secondaryDomain) || (secondaryDomainInfo_form.itemName{{$index}}.$dirty && secondaryDomainInfo_form.itemName{{$index}}.$invalid) }">
|
||||
<label class="control-label" for="secondaryDomainInput{{env}}">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
</label>
|
||||
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appInstall.secondaryDomains[env].subdomain" name="location{{$index}}" placeholder="{{ 'appstore.installDialog.locationPlaceholder' | tr }}" autofocus>
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>.{{ appInstall.secondaryDomains[env].domain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="appInstall.secondaryDomains[env].domain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
|
||||
<div class="has-error text-center" ng-show="appInstall.error.port">{{ appInstall.error.port }}</div>
|
||||
<div ng-repeat="(env, info) in appInstall.portBindingsInfo">
|
||||
<ng-form name="portInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appInstallForm.itemName{{$index}}.$dirty && appInstall.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
|
||||
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appInstall.portBindingsEnabled[env]">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
<small style="padding-left: 5px;" ng-show="info.readOnly">{{ 'appstore.installDialog.portReadOnly' | tr }}</small>
|
||||
</label>
|
||||
<input type="number" class="form-control" ng-model="appInstall.portBindings[env]" ng-disabled="!appInstall.portBindingsEnabled[env]" ng-readonly="info.readOnly" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{hostPortMin}}" max="{{hostPortMax}}" required>
|
||||
<p class="text-small text-warning text-bold" ng-show="appInstall.domain.provider === 'cloudflare'">{{ 'appstore.installDialog.cloudflarePortWarning' | tr }} </p>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="isProxyApp(appInstall.app)">
|
||||
<label class="control-label" for="appInstallUpstreamUriInput">Upstream URI</label>
|
||||
<input type="text" class="form-control" ng-model="appInstall.upstreamUri" id="appInstallUpstreamUriInput" name="upstreamUri" ng-required="isProxyApp(appInstall.app)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="appInstall.app.manifest.addons.email">
|
||||
<label class="control-label">{{ 'appstore.installDialog.userManagement' | tr }}</label>
|
||||
<p>{{ 'appstore.installDialog.userManagementMailbox' | tr }}
|
||||
<span ng-bind-html="'appstore.installDialog.configuredForCloudronEmail' | tr:{ emailDocsLink: 'https://docs.cloudron.io/email/' }">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" ng-show="!appInstall.customAuth && !appInstall.app.manifest.addons.email">{{ 'appstore.installDialog.userManagement' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#access-restriction" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<label class="control-label" ng-show="appInstall.customAuth || appInstall.app.manifest.addons.email">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<p ng-show="appInstall.customAuth || appInstall.app.manifest.addons.email">{{ 'appstore.installDialog.userManagementNone' | tr }}</p>
|
||||
<div class="radio" ng-show="appInstall.optionalSso">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="nosso"> {{ 'appstore.installDialog.userManagementLeaveToApp' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="any">
|
||||
<span ng-show="!appInstall.customAuth">{{ 'appstore.installDialog.userManagementAllUsers' | tr }}</span>
|
||||
<span ng-show="appInstall.customAuth">{{ 'app.accessControl.userManagement.visibleForAllUsers' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="groups">
|
||||
<span ng-show="!appInstall.customAuth">{{ 'appstore.installDialog.userManagementSelectUsers' | tr }}</span>
|
||||
<span ng-show="appInstall.customAuth">{{ 'app.accessControl.userManagement.visibleForSelected' | tr }}</span>
|
||||
<span class="label label-danger" ng-show="appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()">{{ 'appstore.installDialog.errorUserManagementSelectAtLeastOne' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-left: 20px;">
|
||||
<div class="col-md-5">
|
||||
{{ 'appstore.installDialog.users' | tr }}:
|
||||
<multiselect ng-model="appInstall.accessRestriction.users" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="(user.username || user.email) for user in users" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
{{ 'appstore.installDialog.groups' | tr }}:
|
||||
<multiselect ng-model="appInstall.accessRestriction.groups" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="group.name for group in groups" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div class="hide">
|
||||
<label class="control-label" for="appInstallCertificateInput">Certificate (optional)</label>
|
||||
<div class="has-error text-center" ng-show="appInstall.error.cert">{{ appInstall.error.cert }}</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appInstallForm.certificate.$dirty && appInstall.error.cert }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appInstallCertificateFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="appInstall.certificateFileName" id="appInstallCertificateInput" name="certificate" onclick="getElementById('appInstallCertificateFileInput').click();" style="cursor: pointer;" ng-required="appInstall.keyFileName">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('appInstallCertificateFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appInstallForm.key.$dirty && appInstall.error.cert }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appInstallKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="appInstall.keyFileName" id="appInstallKeyInput" name="key" onclick="getElementById('appInstallKeyFileInput').click();" style="cursor: pointer;" ng-required="appInstall.certificateFileName">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('appInstallKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="(appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()) || !appInstall.accessRestrictionOption || appInstallForm.$invalid || busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="collapse" id="collapseMediaLinksCarousel" data-toggle="false">
|
||||
<div ng-repeat="mediaLink in appInstall.mediaLinks" class="slick-item" style="background-image: url('{{mediaLink}}');" ng-show="appInstall.mediaLinks.length == 1"></div>
|
||||
<slick init-onload="true" current-index="0" autoplay="true" arrows="false" autoplay-speed="2000" data="appInstall.mediaLinks" ng-show="appInstall.mediaLinks.length > 1">
|
||||
<div ng-repeat="mediaLink in appInstall.mediaLinks" class="slick-item" style="background-image: url('{{mediaLink}}');"></div>
|
||||
</slick>
|
||||
<br/>
|
||||
<div class="appstore-install-description">
|
||||
<p ng-show="appInstall.app.manifest.upstreamVersion">{{ 'appstore.installDialog.titleAndVersion' | tr:{ title: appInstall.app.manifest.title, version: appInstall.app.manifest.upstreamVersion } }}</p>
|
||||
<div ng-bind-html="appInstall.app.manifest.description | markdown2html"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="collapseResourceConstraint" data-toggle="false">
|
||||
<h4 class="text-danger">{{ 'appstore.installDialog.lowOnResources' | tr }}<sup><a ng-href="https://docs.cloudron.io/apps/#low-resource-warning" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></h4>
|
||||
<p>{{ 'appstore.installDialog.pleaseUpgradeServer' | tr }}</p>
|
||||
</div>
|
||||
<div class="collapse" id="collapseSubscriptionRequired" data-toggle="false">
|
||||
<p>{{ 'appstore.installDialog.subscriptionRequired' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="openSubscriptionSetup()" ng-show="appInstall.state === 'subscriptionRequired'">{{ 'appstore.installDialog.setupSubscriptionAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-show="appInstall.state === 'resourceConstraint'" ng-click="appInstall.showForm(true)">{{ 'appstore.installDialog.installAnywayAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'appInfo'" ng-click="appInstall.showForm()">{{ 'appstore.installDialog.installAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'installForm'" ng-click="appInstall.submit()" ng-disabled="(appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()) || !appInstall.accessRestrictionOption || appInstallForm.$invalid || appInstall.busy"><i class="fa fa-circle-notch fa-spin" ng-show="appInstall.busy"></i> {{ 'appstore.installDialog.doInstallAction' | tr:{ dnsOverwrite: appInstall.needsOverwrite } }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal app not found -->
|
||||
<div class="modal fade" id="appNotFoundModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'appstore.appNotFoundDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body" ng-bind-html="'appstore.appNotFoundDialog.description' | tr:{ appId: appNotFound.appId, version: appNotFound.version }"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal applinks add -->
|
||||
<div class="modal fade" id="applinksAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'app.addApplinkDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="applinksAddForm" role="form" ng-submit="applinksAdd.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (applinksAddForm.upstreamUri.$dirty && applinksAddForm.upstreamUri.$invalid) || (!applinksAddForm.upstreamUri.$dirty && applinksAdd.error.upstreamUri) }">
|
||||
<label class="control-label">{{ 'app.applinks.upstreamUri' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="applinksAdd.upstreamUri" name="upstreamUri" id="inputUpstreamUri" autofocus autocomplete="off" required>
|
||||
<span class="text-danger" ng-show="applinksAdd.error.upstreamUri">{{ applinksAdd.error.upstreamUri }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'app.applinks.label' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="applinksAdd.label" name="label" id="inputLabel" autocomplete="off" placeholder="Leave empty for autodetection">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'app.display.tags' | tr }}</label>
|
||||
<tag-input class="form-control" placeholder="{{ 'app.display.tagsPlaceholder' | tr }}" taglist="applinksAdd.tags" name="tags" uib-tooltip="{{ 'app.display.tagsTooltip' | tr }}"></tag-input>
|
||||
</div>
|
||||
|
||||
<label class="control-label">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="applinksAdd.accessRestrictionOption" value="any">
|
||||
<span>{{ 'app.accessControl.userManagement.visibleForAllUsers' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="applinksAdd.accessRestrictionOption" value="groups">
|
||||
<span>{{ 'app.accessControl.userManagement.visibleForSelected' | tr }}</span>
|
||||
<span class="label label-danger" ng-show="applinksAdd.accessRestrictionOption === 'groups' && !applinksAdd.isAccessRestrictionValid()">{{ 'appstore.installDialog.errorUserManagementSelectAtLeastOne' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-left: 20px; display: flex;">
|
||||
<div>
|
||||
{{ 'appstore.installDialog.users' | tr }}: <multiselect name="accessUsersSelect" class="input-sm stretch" ng-model="applinksAdd.accessRestriction.users" ng-disabled="applinksAdd.accessRestrictionOption !== 'groups'" options="(user.username || user.email) for user in users" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ 'appstore.installDialog.groups' | tr }}: <multiselect name="accessGroupsSelect" class="input-sm stretch" ng-model="applinksAdd.accessRestriction.groups" ng-disabled="applinksAdd.accessRestrictionOption !== 'groups'" options="group.name for group in groups" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="applinksAddForm.$invalid || applinksAdd.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="applinksAdd.submit()" ng-disabled="applinksAddForm.$invalid || applinksAdd.busy"><i class="fa fa-circle-notch fa-spin" ng-show="applinksAdd.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="!ready" class="loading-banner">
|
||||
<h1><i class="fa fa-circle-notch fa-spin"></i></h1>
|
||||
</div>
|
||||
|
||||
<!-- appstore login -->
|
||||
<div ng-show="ready && !validSubscription" class="container card card-small appstore-login ng-cloak">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1 ng-show="appstoreLogin.register">{{ 'appstore.accountDialog.titleSignUp' | tr }}</h1>
|
||||
<h1 ng-hide="appstoreLogin.register">{{ 'appstore.accountDialog.titleLogin' | tr }}</h1>
|
||||
</div>
|
||||
<div class="col-md-12 text-center">
|
||||
<p>{{ 'appstore.accountDialog.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.generic">{{ appstoreLogin.error.generic }}</small>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<br/>
|
||||
|
||||
<form name="appstoreLoginForm" role="form" novalidate ng-submit="appstoreLogin.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (appstoreLoginForm.email.$dirty && appstoreLoginForm.email.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.email' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="appstoreLogin.email" id="inputAppstoreLoginEmail" name="email" required autofocus>
|
||||
<div class="control-label" ng-show="(!appstoreLoginForm.email.$dirty && appstoreLogin.error.email) || (appstoreLoginForm.email.$dirty && appstoreLoginForm.email.$invalid) || appstoreLogin.error.email">
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.email">{{ appstoreLogin.error.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appstoreLoginForm.password.$dirty && appstoreLogin.error.password) || (appstoreLoginForm.password.$dirty && appstoreLoginForm.password.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="appstoreLogin.password" id="inputAppstoreLoginPassword" name="password" required password-reveal>
|
||||
<div class="control-label" ng-show="(!appstoreLoginForm.password.$dirty && appstoreLogin.error.password) || (appstoreLoginForm.password.$dirty && appstoreLoginForm.password.$invalid)">
|
||||
<small ng-show="!appstoreLoginForm.password.$dirty && appstoreLogin.error.password">{{ 'appstore.accountDialog.errorWrongPassword' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-hide="appstoreLogin.register" ng-class="{ 'has-error': appstoreLogin.error.totpToken }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.2faToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="appstoreLogin.totpToken" id="inputAppstoreLoginTotpToken" name="totpToken">
|
||||
<div class="control-label" ng-show="appstoreLogin.error.totpToken">
|
||||
<small ng-show="appstoreLogin.error.totpToken">{{ appstoreLogin.error.totpToken }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appstoreLogin.termsAccepted"><span ng-bind-html="'appstore.accountDialog.licenseCheckbox' | tr:{ licenseLink: 'https://cloudron.io/legal/license.html' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<button type="submit" class="btn btn-lg btn-success" ng-disabled="appstoreLoginForm.$invalid || appstoreLogin.busy || !appstoreLogin.termsAccepted">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appstoreLogin.busy"></i> <span ng-hide="appstoreLogin.register">{{ 'appstore.accountDialog.loginAction' | tr }}</span><span ng-show="appstoreLogin.register">{{ 'appstore.accountDialog.createAccountAction' | tr }}</span>
|
||||
</button>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<a href="" ng-click="appstoreLogin.register = true" ng-hide="appstoreLogin.register">{{ 'appstore.accountDialog.switchToSignUpAction' | tr }}</a>
|
||||
<a href="" ng-click="appstoreLogin.register = false" ng-show="appstoreLogin.register">{{ 'appstore.accountDialog.switchToLoginAction' | tr }}</a>
|
||||
</center>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- give more vertical spacing so the login form does not appear clipped -->
|
||||
<div ng-show="ready && !validSubscription">
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div class="appstore-layout">
|
||||
|
||||
<div ng-show="ready && validSubscription" class="ng-cloak appstore-toolbar">
|
||||
<div class="appstore-toolbar-content">
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" data-toggle="dropdown" ng-class="{ 'btn-primary': '' !== category && 'recent' !== category && 'new' !== category }">
|
||||
{{ categoryButtonLabel(category) }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="" ng-click="showCategory('');"><i class="fas fa-home fa-fw"></i> {{ 'appstore.category.all' | tr }}</a></li>
|
||||
<li><a href="" ng-click="showCategory('new');"><i class="fas fa-rss fa-fw"></i> {{ 'appstore.category.newApps' | tr }}</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li ng-repeat="category in categories | orderBy:'label'"><a href="" ng-click="showCategory(category.id);"><i class="{{ category.icon }} fa-fw"></i> {{ category.label }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
<i class="{{ userManagementFilterOption.icon }} fa-fw"></i>
|
||||
{{ 'appstore.ssofilter.label' | tr }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="option in userManagementFilterOptions" ng-class="{ 'active': userManagementFilterOption.id && userManagementFilterOption.id === option.id }"><a href="" ng-click="applyUserMangamentFilter(option);"><i class="{{ option.icon }} fa-fw"></i> {{ option.label }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<input type="text" id="appstoreSearch" class="form-control" style="width: auto; flex-grow: 1;" placeholder="{{ 'appstore.searchPlaceholder' | tr }}" ng-model="searchString" ng-change="search()" autofocus>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default" ng-click="openAppProxy()"><i class="fas fa-exchange-alt"></i> {{ 'apps.addAppproxyAction' | tr }}</a></a>
|
||||
<button type="button" class="btn btn-default" ng-click="applinksAdd.show()"><i class="fas fa-link"></i> {{ 'apps.addApplinkAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="ready && validSubscription" class="ng-cloak appstore-grid">
|
||||
<div class="text-center" ng-hide="apps.length || popularApps.length">
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<h3 class="text-muted">{{ 'appstore.noAppsFound' | tr }}</h3>
|
||||
<br/>
|
||||
<a href="https://forum.cloudron.io/category/5/app-requests" target="_blank">{{ 'appstore.appMissing' | tr }}</a>
|
||||
</div>
|
||||
<div class="" ng-show="category === '' && popularApps.length">
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-12">
|
||||
<h2>{{ 'appstore.category.popular' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-3 appstore-item" ng-repeat="app in popularApps | userManagementFilter:userManagementFilterOption">
|
||||
<div class="appstore-item-content highlight" ng-click="gotoApp(app)" ng-class="{ 'appstore-item-content-testing': app.releaseState === 'unstable' }">
|
||||
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.releaseState === 'unstable'">{{ 'appstore.unstable' | tr }}</span>
|
||||
<div class="appstore-item-content-icon col-same-height">
|
||||
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
</div>
|
||||
<div class="col-same-height">
|
||||
<h4 class="appstore-item-content-title">{{ app.manifest.title }}</h4>
|
||||
<div class="appstore-item-content-tagline text-muted">{{ app.manifest.tagline }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="" ng-show="apps.length">
|
||||
<div class="row-no-margin" ng-show="!category && !searchString">
|
||||
<div class="col-sm-12">
|
||||
<h2>{{ 'appstore.category.all' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-3 appstore-item" ng-repeat="app in apps | userManagementFilter:userManagementFilterOption | orderBy:'-priority' ">
|
||||
<div class="appstore-item-content highlight" ng-click="gotoApp(app)" ng-class="{ 'appstore-item-content-testing': app.releaseState === 'unstable' }">
|
||||
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.releaseState === 'unstable'">{{ 'appstore.unstable' | tr }}</span>
|
||||
<div class="appstore-item-content-icon col-same-height">
|
||||
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
</div>
|
||||
<div class="appstore-item-content-description col-same-height">
|
||||
<h4 class="appstore-item-content-title">{{ app.manifest.title }}</h4>
|
||||
<div class="appstore-item-content-tagline text-muted">{{ app.manifest.tagline }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,836 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global async */
|
||||
/* global ERROR */
|
||||
/* global RSTATES */
|
||||
/* global moment */
|
||||
|
||||
angular.module('Application').controller('AppStoreController', ['$scope', '$translate', '$location', '$timeout', '$routeParams', 'Client', function ($scope, $translate, $location, $timeout, $routeParams, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.HOST_PORT_MIN = 1024;
|
||||
$scope.HOST_PORT_MAX = 65535;
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.apps = [];
|
||||
$scope.popularApps = [];
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.users = [];
|
||||
$scope.groups = [];
|
||||
$scope.domains = [];
|
||||
$scope.category = '';
|
||||
$scope.cachedCategory = ''; // used to cache the selected category while searching
|
||||
$scope.searchString = '';
|
||||
$scope.validSubscription = false;
|
||||
$scope.subscription = {};
|
||||
$scope.memory = null; // { memory, swap }
|
||||
|
||||
$scope.showView = function (view) {
|
||||
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
|
||||
$('.modal').on('hidden.bs.modal', function () {
|
||||
$scope.$apply(function () {
|
||||
$scope.appInstall.reset();
|
||||
$('.modal').off('hidden.bs.modal');
|
||||
$location.path(view);
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal').modal('hide');
|
||||
};
|
||||
|
||||
// If new categories added make sure the translation below exists
|
||||
$scope.categories = [
|
||||
{ id: 'analytics', icon: 'fa fa-chart-line', label: 'Analytics'},
|
||||
{ id: 'automation', icon: 'fa fa-robot', label: 'Automation'},
|
||||
{ id: 'blog', icon: 'fa fa-font', label: 'Blog'},
|
||||
{ id: 'chat', icon: 'fa fa-comments', label: 'Chat'},
|
||||
{ id: 'crm', icon: 'fab fa-connectdevelop', label: 'CRM'},
|
||||
{ id: 'document', icon: 'fa fa-file-word', label: 'Documents'},
|
||||
{ id: 'email', icon: 'fa fa-envelope', label: 'Email'},
|
||||
{ id: 'federated', icon: 'fa fa-project-diagram', label: 'Federated'},
|
||||
{ id: 'finance', icon: 'fa fa-hand-holding-usd', label: 'Finance'},
|
||||
{ id: 'forum', icon: 'fa fa-users', label: 'Forum'},
|
||||
{ id: 'fun', icon: 'fa fa-gamepad', label: 'Fun'},
|
||||
{ id: 'gallery', icon: 'fa fa-images', label: 'Gallery'},
|
||||
{ id: 'game', icon: 'fa fa-gamepad', label: 'Games'},
|
||||
{ id: 'git', icon: 'fa fa-code-branch', label: 'Code Hosting'},
|
||||
{ id: 'hosting', icon: 'fa fa-server', label: 'Web Hosting'},
|
||||
{ id: 'learning', icon: 'fas fa-graduation-cap', label: 'Learning'},
|
||||
{ id: 'media', icon: 'fas fa-photo-video', label: 'Media'},
|
||||
{ id: 'no-code', icon: 'fas fa-code', label: 'No-code'},
|
||||
{ id: 'notes', icon: 'fa fa-sticky-note', label: 'Notes'},
|
||||
{ id: 'project', icon: 'fas fa-project-diagram', label: 'Project Management'},
|
||||
{ id: 'sync', icon: 'fa fa-sync-alt', label: 'File Sync'},
|
||||
{ id: 'vpn', icon: 'fa fa-user-secret', label: 'VPN'},
|
||||
{ id: 'wiki', icon: 'fab fa-wikipedia-w', label: 'Wiki'},
|
||||
];
|
||||
|
||||
// Translation IDs are generated as "appstore.category.<categoryId>"
|
||||
$translate($scope.categories.map(function (c) { return 'appstore.category.' + c.id; })).then(function (tr) {
|
||||
Object.keys(tr).forEach(function (key) {
|
||||
if (key === tr[key]) return; // missing translation use default label
|
||||
|
||||
var category = $scope.categories.find(function (c) { return key.endsWith(c.id); });
|
||||
if (category) category.label = tr[key];
|
||||
});
|
||||
});
|
||||
|
||||
$scope.categoryButtonLabel = function (category) {
|
||||
var categoryLabel = $translate.instant('appstore.categoryLabel');
|
||||
|
||||
if (category === '') return $translate.instant('appstore.category.all');
|
||||
if (category === 'new') return $translate.instant('appstore.category.newApps');
|
||||
|
||||
var tmp = $scope.categories.find(function (c) { return c.id === category; });
|
||||
if (tmp) return tmp.label;
|
||||
|
||||
return categoryLabel;
|
||||
};
|
||||
|
||||
$scope.isProxyApp = function (app) {
|
||||
if (!app) return false;
|
||||
|
||||
return app.id === 'io.cloudron.builtin.appproxy';
|
||||
};
|
||||
|
||||
$scope.userManagementFilterOptions = [
|
||||
{ id: '', icon: '', label: $translate.instant('appstore.ssofilter.all') },
|
||||
{ id: 'sso', icon: 'fas fa-user', label: $translate.instant('apps.auth.sso') },
|
||||
{ id: 'nosso', icon: 'far fa-user', label: $translate.instant('apps.auth.nosso') },
|
||||
{ id: 'email', icon: 'fas fa-envelope', label: $translate.instant('apps.auth.email') },
|
||||
];
|
||||
$scope.userManagementFilterOption = $scope.userManagementFilterOptions[0];
|
||||
|
||||
$scope.userManagementFilterOptionIsActive = function (option) {
|
||||
return option.id === $scope.userManagementFilterOption.id;
|
||||
};
|
||||
|
||||
$scope.applyUserMangamentFilter = function (option) {
|
||||
$scope.userManagementFilterOption = option;
|
||||
};
|
||||
|
||||
$scope.appInstall = {
|
||||
busy: false,
|
||||
state: 'appInfo',
|
||||
error: {},
|
||||
app: {},
|
||||
needsOverwrite: false,
|
||||
subdomain: '',
|
||||
domain: null, // object and not the string
|
||||
secondaryDomains: {},
|
||||
portBindings: {},
|
||||
mediaLinks: [],
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: '',
|
||||
accessRestrictionOption: '',
|
||||
accessRestriction: { users: [], groups: [] },
|
||||
customAuth: false,
|
||||
optionalSso: false,
|
||||
subscriptionErrorMesssage: '',
|
||||
upstreamUri: '',
|
||||
|
||||
isAccessRestrictionValid: function () {
|
||||
var tmp = $scope.appInstall.accessRestriction;
|
||||
return !!(tmp.users.length || tmp.groups.length);
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.appInstall.app = {};
|
||||
$scope.appInstall.error = {};
|
||||
$scope.appInstall.needsOverwrite = false;
|
||||
$scope.appInstall.subdomain = '';
|
||||
$scope.appInstall.domain = null;
|
||||
$scope.appInstall.secondaryDomains = {};
|
||||
$scope.appInstall.portBindings = {};
|
||||
$scope.appInstall.state = 'appInfo';
|
||||
$scope.appInstall.mediaLinks = [];
|
||||
$scope.appInstall.certificateFile = null;
|
||||
$scope.appInstall.certificateFileName = '';
|
||||
$scope.appInstall.keyFile = null;
|
||||
$scope.appInstall.keyFileName = '';
|
||||
$scope.appInstall.accessRestrictionOption = '';
|
||||
$scope.appInstall.accessRestriction = { users: [], groups: [] };
|
||||
$scope.appInstall.optionalSso = false;
|
||||
$scope.appInstall.customAuth = false;
|
||||
$scope.appInstall.subscriptionErrorMesssage = '';
|
||||
$scope.appInstall.upstreamUri = '';
|
||||
|
||||
$('#collapseInstallForm').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('hide');
|
||||
$('#collapseSubscriptionRequired').collapse('hide');
|
||||
$('#collapseMediaLinksCarousel').collapse('show');
|
||||
|
||||
if ($scope.appInstallForm) {
|
||||
$scope.appInstallForm.$setPristine();
|
||||
$scope.appInstallForm.$setUntouched();
|
||||
}
|
||||
},
|
||||
|
||||
showForm: function (force) {
|
||||
var app = $scope.appInstall.app;
|
||||
var DEFAULT_MEMORY_LIMIT = 1024 * 1024 * 256;
|
||||
|
||||
var needed = app.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT; // RAM+Swap
|
||||
var used = Client.getInstalledApps().reduce(function (prev, cur) {
|
||||
if (cur.runState === RSTATES.STOPPED) return prev;
|
||||
|
||||
return prev + (cur.memoryLimit || cur.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT);
|
||||
}, 0);
|
||||
var totalMemory = ($scope.memory.memory + $scope.memory.swap) * 1.5;
|
||||
var available = (totalMemory || 0) - used;
|
||||
|
||||
var enoughResourcesAvailable = (available - needed) >= 0;
|
||||
|
||||
if (enoughResourcesAvailable || force) {
|
||||
$scope.appInstall.state = 'installForm';
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('hide');
|
||||
$('#collapseInstallForm').collapse('show');
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else {
|
||||
$scope.appInstall.state = 'resourceConstraint';
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('show');
|
||||
}
|
||||
},
|
||||
|
||||
show: function (app) { // this is an appstore app object!
|
||||
$scope.appInstall.reset();
|
||||
|
||||
// make a copy to work with in case the app object gets updated while polling
|
||||
angular.copy(app, $scope.appInstall.app);
|
||||
|
||||
$scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
|
||||
$scope.appInstall.domain = $scope.domains.find(function (d) { return $scope.config.adminDomain === d.domain; }); // pre-select the adminDomain
|
||||
|
||||
$scope.appInstall.secondaryDomains = {};
|
||||
var httpPorts = $scope.appInstall.app.manifest.httpPorts || {};
|
||||
for (var env2 in httpPorts) {
|
||||
$scope.appInstall.secondaryDomains[env2] = {
|
||||
subdomain: httpPorts[env2].defaultValue || '',
|
||||
domain: $scope.appInstall.domain
|
||||
};
|
||||
}
|
||||
|
||||
$scope.appInstall.portBindingsInfo = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts); // Portbinding map only for information
|
||||
$scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair
|
||||
$scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
|
||||
var manifest = app.manifest;
|
||||
$scope.appInstall.optionalSso = !!manifest.optionalSso;
|
||||
$scope.appInstall.customAuth = !(manifest.addons['ldap'] || manifest.addons['oidc'] || manifest.addons['proxyAuth']);
|
||||
|
||||
$scope.appInstall.accessRestrictionOption = $scope.groups.length ? '' : 'any'; // make the user select an ACL conciously if groups are used
|
||||
$scope.appInstall.accessRestriction = { users: [], groups: [] };
|
||||
|
||||
// set default ports
|
||||
var allPorts = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts);
|
||||
for (var env in allPorts) {
|
||||
$scope.appInstall.portBindings[env] = allPorts[env].defaultValue || 0;
|
||||
$scope.appInstall.portBindingsEnabled[env] = true;
|
||||
}
|
||||
|
||||
$('#appInstallModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.appInstall.busy = true;
|
||||
$scope.appInstall.error.other = null;
|
||||
$scope.appInstall.error.location = null;
|
||||
$scope.appInstall.error.port = null;
|
||||
|
||||
var secondaryDomains = {};
|
||||
for (var env2 in $scope.appInstall.secondaryDomains) {
|
||||
secondaryDomains[env2] = {
|
||||
subdomain: $scope.appInstall.secondaryDomains[env2].subdomain,
|
||||
domain: $scope.appInstall.secondaryDomains[env2].domain.domain
|
||||
};
|
||||
}
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.appInstall.portBindings) {
|
||||
if ($scope.appInstall.portBindingsEnabled[env]) {
|
||||
finalPortBindings[env] = $scope.appInstall.portBindings[env];
|
||||
}
|
||||
}
|
||||
|
||||
var finalAccessRestriction = null;
|
||||
if ($scope.appInstall.accessRestrictionOption === 'groups') {
|
||||
finalAccessRestriction = { users: [], groups: [] };
|
||||
finalAccessRestriction.users = $scope.appInstall.accessRestriction.users.map(function (u) { return u.id; });
|
||||
finalAccessRestriction.groups = $scope.appInstall.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var data = {
|
||||
overwriteDns: $scope.appInstall.needsOverwrite,
|
||||
subdomain: $scope.appInstall.subdomain || '',
|
||||
domain: $scope.appInstall.domain.domain,
|
||||
secondaryDomains: secondaryDomains,
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: finalAccessRestriction,
|
||||
cert: $scope.appInstall.certificateFile,
|
||||
key: $scope.appInstall.keyFile,
|
||||
sso: !$scope.appInstall.optionalSso ? undefined : ($scope.appInstall.accessRestrictionOption !== 'nosso'),
|
||||
};
|
||||
|
||||
if ($scope.appInstall.upstreamUri) {
|
||||
data.upstreamUri = $scope.appInstall.upstreamUri;
|
||||
data.upstreamUri = data.upstreamUri.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
var domains = [];
|
||||
domains.push({ subdomain: data.subdomain, domain: data.domain, type: 'primary' });
|
||||
var canInstall = true;
|
||||
|
||||
async.eachSeries(domains, function (domain, callback) {
|
||||
if (data.overwriteDns) return callback();
|
||||
|
||||
Client.checkDNSRecords(domain.domain, domain.subdomain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var message;
|
||||
if (result.error) {
|
||||
if (result.error.reason === ERROR.ACCESS_DENIED) {
|
||||
message = 'DNS credentials for ' + domain.domain + ' are invalid. Update it in Domains & Certs view';
|
||||
if (domain.type === 'primary') {
|
||||
$scope.appInstall.error.location = message;
|
||||
} else {
|
||||
$scope.appInstall.error.secondaryDomain = message;
|
||||
}
|
||||
} else {
|
||||
if (domain.type === 'primary') {
|
||||
$scope.appInstall.error.location = result.error.message;
|
||||
} else {
|
||||
$scope.appInstall.error.secondaryDomain = message;
|
||||
}
|
||||
}
|
||||
canInstall = false;
|
||||
} else if (result.needsOverwrite) {
|
||||
message = 'DNS Record already exists. Confirm that the domain is not in use for services external to Cloudron';
|
||||
if (data.type === 'primary') {
|
||||
$scope.appInstall.error.location = message;
|
||||
} else {
|
||||
$scope.appInstall.error.secondaryDomain = message;
|
||||
}
|
||||
$scope.appInstall.needsOverwrite = true;
|
||||
canInstall = false;
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
$scope.appInstall.busy = false;
|
||||
return Client.error(error);
|
||||
}
|
||||
|
||||
if (!canInstall) {
|
||||
$scope.appInstall.busy = false;
|
||||
$scope.appInstallForm.location.$setPristine();
|
||||
$('#appInstallLocationInput').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, data, function (error, newAppId) {
|
||||
if (error) {
|
||||
var errorMessage = error.message.toLowerCase();
|
||||
|
||||
if (error.statusCode === 402) {
|
||||
$scope.appInstall.state = 'subscriptionRequired';
|
||||
$scope.appInstall.subscriptionErrorMesssage = error.message;
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('hide');
|
||||
$('#collapseInstallForm').collapse('hide');
|
||||
$('#collapseSubscriptionRequired').collapse('show');
|
||||
} else if (error.statusCode === 409) {
|
||||
if (errorMessage.indexOf('port') !== -1) {
|
||||
$scope.appInstall.error.port = error.message;
|
||||
} else if (errorMessage.indexOf('location') !== -1) {
|
||||
if (errorMessage.indexOf('primary') !== -1) {
|
||||
$scope.appInstall.error.location = error.message;
|
||||
$scope.appInstallForm.location.$setPristine();
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else {
|
||||
$scope.appInstall.error.secondaryDomain = error.message;
|
||||
}
|
||||
} else {
|
||||
$scope.appInstall.error.other = error.message;
|
||||
}
|
||||
} else if (error.statusCode === 400) {
|
||||
if (errorMessage.indexOf('cert') !== -1) {
|
||||
$scope.appInstall.error.cert = error.message;
|
||||
$scope.appInstall.certificateFileName = '';
|
||||
$scope.appInstall.certificateFile = null;
|
||||
$scope.appInstall.keyFileName = '';
|
||||
$scope.appInstall.keyFile = null;
|
||||
} else {
|
||||
$scope.appInstall.error.other = error.message;
|
||||
}
|
||||
} else {
|
||||
$scope.appInstall.error.other = error.message;
|
||||
}
|
||||
|
||||
$scope.appInstall.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appInstall.busy = false;
|
||||
|
||||
// stash new app id for later
|
||||
$scope.appInstall.app.id = newAppId;
|
||||
|
||||
// we track the postinstall confirmation for the current user's browser
|
||||
// TODO later we might want to have a notification db to track the state across admins and browsers
|
||||
if ($scope.appInstall.app.manifest.postInstallMessage) {
|
||||
localStorage['confirmPostInstall_' + $scope.appInstall.app.id] = true;
|
||||
}
|
||||
|
||||
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
|
||||
$('#appInstallModal').on('hidden.bs.modal', function () {
|
||||
$scope.$apply(function () {
|
||||
$location.path('/apps').search({ });
|
||||
});
|
||||
});
|
||||
|
||||
$('#appInstallModal').modal('hide');
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.appNotFound = {
|
||||
appId: '',
|
||||
version: ''
|
||||
};
|
||||
|
||||
$scope.appstoreLogin = {
|
||||
busy: false,
|
||||
error: {},
|
||||
email: '',
|
||||
password: '',
|
||||
totpToken: '',
|
||||
register: true,
|
||||
termsAccepted: false,
|
||||
|
||||
submit: function () {
|
||||
$scope.appstoreLogin.error = {};
|
||||
$scope.appstoreLogin.busy = true;
|
||||
|
||||
Client.registerCloudron($scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, $scope.appstoreLogin.register, function (error) {
|
||||
if (error) {
|
||||
$scope.appstoreLogin.busy = false;
|
||||
|
||||
if (error.statusCode === 409) {
|
||||
$scope.appstoreLogin.error.email = 'An account with this email already exists';
|
||||
$scope.appstoreLogin.password = '';
|
||||
$scope.appstoreLoginForm.email.$setPristine();
|
||||
$scope.appstoreLoginForm.password.$setPristine();
|
||||
$('#inputAppstoreLoginEmail').focus();
|
||||
} else if (error.statusCode === 412) {
|
||||
if (error.message.indexOf('TOTP token missing') !== -1) {
|
||||
$scope.appstoreLogin.error.totpToken = 'A 2FA token is required';
|
||||
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
|
||||
} else if (error.message.indexOf('TOTP token invalid') !== -1) {
|
||||
$scope.appstoreLogin.error.totpToken = 'Wrong 2FA token';
|
||||
$scope.appstoreLogin.totpToken = '';
|
||||
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
|
||||
} else {
|
||||
$scope.appstoreLogin.error.password = 'Wrong email or password';
|
||||
$scope.appstoreLogin.password = '';
|
||||
$('#inputAppstoreLoginPassword').focus();
|
||||
$scope.appstoreLoginForm.password.$setPristine();
|
||||
}
|
||||
} else if (error.statusCode === 424) {
|
||||
if (error.message === 'wrong user') {
|
||||
$scope.appstoreLogin.error.generic = 'Wrong cloudron.io account';
|
||||
$scope.appstoreLogin.email = '';
|
||||
$scope.appstoreLogin.password = '';
|
||||
$scope.appstoreLoginForm.email.$setPristine();
|
||||
$scope.appstoreLoginForm.password.$setPristine();
|
||||
$('#inputAppstoreLoginEmail').focus();
|
||||
} else {
|
||||
console.error(error);
|
||||
$scope.appstoreLogin.error.generic = error.message;
|
||||
}
|
||||
} else {
|
||||
console.error(error);
|
||||
$scope.appstoreLogin.error.generic = error.message || 'Please retry later';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// do a full re-init of the view now that we have a subscription
|
||||
init();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// TODO does not support testing apps in search
|
||||
$scope.search = function () {
|
||||
if (!$scope.searchString) return $scope.showCategory($scope.cachedCategory);
|
||||
|
||||
$scope.category = '';
|
||||
|
||||
Client.getAppstoreAppsFast(function (error, apps) {
|
||||
if (error) return $timeout($scope.search, 1000);
|
||||
|
||||
var token = $scope.searchString.toUpperCase();
|
||||
|
||||
$scope.popularApps = [];
|
||||
$scope.apps = apps.filter(function (app) {
|
||||
// on searches we give highe priority if title or tagline matches
|
||||
app.priority = 0;
|
||||
|
||||
if (app.manifest.title.toUpperCase().indexOf(token) !== -1) {
|
||||
app.priority = 2;
|
||||
return true;
|
||||
}
|
||||
if (app.manifest.tagline.toUpperCase().indexOf(token) !== -1) {
|
||||
app.priority = 1;
|
||||
return true;
|
||||
}
|
||||
if (app.manifest.id.toUpperCase().indexOf(token) !== -1) return true;
|
||||
if (app.manifest.description.toUpperCase().indexOf(token) !== -1) return true;
|
||||
if (app.manifest.tags.join().toUpperCase().indexOf(token) !== -1) return true;
|
||||
return false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function filterForNewApps(apps) {
|
||||
var minApps = apps.length < 12 ? apps.length : 12; // prevent endless loop
|
||||
var tmp = [];
|
||||
var i = 0;
|
||||
|
||||
do {
|
||||
var offset = moment().subtract(i++, 'days');
|
||||
tmp = apps.filter(function (app) { return moment(app.publishedAt).isAfter(offset); });
|
||||
} while(tmp.length < minApps);
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
function filterForRecentlyUpdatedApps(apps) {
|
||||
var minApps = apps.length < 12 ? apps.length : 12; // prevent endless loop
|
||||
var tmp = [];
|
||||
var i = 0;
|
||||
|
||||
do {
|
||||
var offset = moment().subtract(i++, 'days');
|
||||
tmp = apps.filter(function (app) { return moment(app.creationDate).isAfter(offset); }); // creationDate here is from appstore's appversions table
|
||||
} while(tmp.length < minApps);
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
$scope.showCategory = function (category) {
|
||||
$scope.category = category;
|
||||
|
||||
$scope.cachedCategory = $scope.category;
|
||||
|
||||
Client.getAppstoreAppsFast(function (error, apps) {
|
||||
if (error) return $timeout($scope.showCategory.bind(null, category), 1000);
|
||||
|
||||
if (!$scope.category) {
|
||||
$scope.apps = apps.slice(0).filter(function (app) { return !app.featured; }).sort(function (a1, a2) { return a1.manifest.title.localeCompare(a2.manifest.title); });
|
||||
$scope.popularApps = apps.slice(0).filter(function (app) { return app.featured; }).sort(function (a1, a2) { return a2.ranking - a1.ranking; });
|
||||
} else if ($scope.category === 'new') {
|
||||
$scope.apps = filterForNewApps(apps);
|
||||
} else if ($scope.category === 'recent') {
|
||||
$scope.apps = filterForRecentlyUpdatedApps(apps);
|
||||
} else {
|
||||
$scope.apps = apps.filter(function (app) {
|
||||
return app.manifest.tags.some(function (tag) { return $scope.category.toUpperCase() === tag.toUpperCase(); }); // reverse sort;
|
||||
}).sort(function (a1, a2) { return a2.ranking - a1.ranking; });
|
||||
}
|
||||
|
||||
// ensure we scroll to top
|
||||
document.getElementById('ng-view').scrollTop = 0;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error) return console.error('Unable to get subscription.', error);
|
||||
|
||||
Client.openSubscriptionSetup(subscription);
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById('appInstallCertificateFileInput').onchange = function (event) {
|
||||
$scope.$apply(function () {
|
||||
$scope.appInstall.certificateFile = null;
|
||||
$scope.appInstall.certificateFileName = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
$scope.appInstall.certificateFile = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById('appInstallKeyFileInput').onchange = function (event) {
|
||||
$scope.$apply(function () {
|
||||
$scope.appInstall.keyFile = null;
|
||||
$scope.appInstall.keyFileName = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
$scope.appInstall.keyFile = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showAppNotFound = function (appId, version) {
|
||||
$scope.appNotFound.appId = appId;
|
||||
$scope.appNotFound.version = version || 'latest';
|
||||
|
||||
$('#appNotFoundModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.gotoApp = function (app) {
|
||||
$location.path('/appstore/' + app.manifest.id, false).search({ version : app.manifest.version });
|
||||
};
|
||||
|
||||
$scope.openAppProxy = function () {
|
||||
$location.path('/appstore/io.cloudron.builtin.appproxy', false).search({});
|
||||
};
|
||||
|
||||
$scope.applinksAdd = {
|
||||
error: {},
|
||||
busy: false,
|
||||
upstreamUri: '',
|
||||
label: '',
|
||||
tags: '',
|
||||
accessRestrictionOption: 'any',
|
||||
accessRestriction: { users: [], groups: [] },
|
||||
|
||||
isAccessRestrictionValid: function () {
|
||||
return !!($scope.applinksAdd.accessRestriction.users.length || $scope.applinksAdd.accessRestriction.groups.length);
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.applinksAdd.error = {};
|
||||
$scope.applinksAdd.busy = false;
|
||||
$scope.applinksAdd.upstreamUri = '';
|
||||
$scope.applinksAdd.label = '';
|
||||
$scope.applinksAdd.tags = '';
|
||||
|
||||
$scope.applinksAddForm.$setUntouched();
|
||||
$scope.applinksAddForm.$setPristine();
|
||||
|
||||
$('#applinksAddModal').modal('show');
|
||||
|
||||
return false; // prevent propagation and default
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
if (!$scope.applinksAdd.upstreamUri) return;
|
||||
|
||||
$scope.applinksAdd.busy = true;
|
||||
$scope.applinksAdd.error = {};
|
||||
|
||||
var accessRestriction = null;
|
||||
if ($scope.applinksAdd.accessRestrictionOption === 'groups') {
|
||||
accessRestriction = { users: [], groups: [] };
|
||||
accessRestriction.users = $scope.applinksAdd.accessRestriction.users.map(function (u) { return u.id; });
|
||||
accessRestriction.groups = $scope.applinksAdd.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var data = {
|
||||
upstreamUri: $scope.applinksAdd.upstreamUri,
|
||||
label: $scope.applinksAdd.label,
|
||||
accessRestriction: accessRestriction,
|
||||
tags: $scope.applinksAdd.tags.split(' ').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; })
|
||||
};
|
||||
|
||||
Client.addApplink(data, function (error) {
|
||||
$scope.applinksAdd.busy = false;
|
||||
|
||||
if (error && error.statusCode === 400 && error.message.includes('upstreamUri')) {
|
||||
$scope.applinksAdd.error.upstreamUri = error.message;
|
||||
$scope.applinksAddForm.$setUntouched();
|
||||
$scope.applinksAddForm.$setPristine();
|
||||
return;
|
||||
}
|
||||
if (error) return console.error('Failed to add applink', error);
|
||||
|
||||
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
|
||||
$('#applinksAddModal').on('hidden.bs.modal', function () {
|
||||
$scope.$apply(function () {
|
||||
$location.path('/apps').search({ });
|
||||
});
|
||||
});
|
||||
|
||||
$('#applinksAddModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function hashChangeListener() {
|
||||
// event listener is called from DOM not angular, need to use $apply
|
||||
$scope.$apply(function () {
|
||||
var appId = $location.path().slice('/appstore/'.length);
|
||||
var version = $location.search().version;
|
||||
|
||||
if (appId) {
|
||||
Client.getAppstoreAppByIdAndVersion(appId, version || 'latest', function (error, result) {
|
||||
if (error) {
|
||||
$scope.showAppNotFound(appId, version);
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appInstall.show(result);
|
||||
});
|
||||
} else {
|
||||
$scope.appInstall.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fetchUsers() {
|
||||
Client.getAllUsers(function (error, users) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchUsers, 5000);
|
||||
}
|
||||
|
||||
$scope.users = users;
|
||||
});
|
||||
}
|
||||
|
||||
function fetchGroups() {
|
||||
Client.getGroups(function (error, groups) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchGroups, 5000);
|
||||
}
|
||||
|
||||
$scope.groups = groups;
|
||||
});
|
||||
}
|
||||
|
||||
function fetchMemory() {
|
||||
Client.memory(function (error, memory) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchMemory, 5000);
|
||||
}
|
||||
|
||||
$scope.memory = memory;
|
||||
});
|
||||
}
|
||||
|
||||
function getSubscription(callback) {
|
||||
var validSubscription = false;
|
||||
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error) {
|
||||
if (error.statusCode === 412) { // not registered yet
|
||||
validSubscription = false;
|
||||
} else if (error.statusCode === 402) { // invalid token, license error
|
||||
validSubscription = false;
|
||||
} else { // 424/external error?
|
||||
return callback(error);
|
||||
}
|
||||
} else {
|
||||
validSubscription = true;
|
||||
$scope.subscription = subscription;
|
||||
}
|
||||
|
||||
// clear busy state when a login/signup was performed
|
||||
$scope.appstoreLogin.busy = false;
|
||||
|
||||
// also update the root controller status
|
||||
if ($scope.$parent) $scope.$parent.updateSubscriptionStatus();
|
||||
|
||||
callback(null, validSubscription);
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
Client.getAppstoreAppsFast(function (error) {
|
||||
if (error && error.statusCode === 402) {
|
||||
$scope.validSubscription = false;
|
||||
$scope.ready = true;
|
||||
return;
|
||||
} else if (error) {
|
||||
console.error('Failed to get apps. Will retry.', error);
|
||||
$timeout(init, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.showCategory('');
|
||||
|
||||
getSubscription(function (error, validSubscription) {
|
||||
if (error) console.error('Failed to get subscription.', error);
|
||||
|
||||
$scope.validSubscription = validSubscription;
|
||||
$scope.ready = true;
|
||||
|
||||
|
||||
// refresh everything in background
|
||||
Client.getAppstoreApps(function (error) { if (error) console.error('Failed to fetch apps.', error); });
|
||||
Client.refreshConfig(); // refresh domain, user, group limit etc
|
||||
fetchUsers();
|
||||
fetchGroups();
|
||||
fetchMemory();
|
||||
|
||||
// domains is required since we populate the dropdown with domains[0]
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return console.error('Error getting domains.', error);
|
||||
|
||||
$scope.domains = result;
|
||||
|
||||
// show install app dialog immediately if an app id was passed in the query
|
||||
// hashChangeListener calls $apply, so make sure we don't double digest here
|
||||
setTimeout(hashChangeListener, 1);
|
||||
|
||||
setTimeout(function () { $('#appstoreSearch').focus(); }, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Client.onReady(init);
|
||||
|
||||
// note: do not use hide.bs.model since it is called immediately from switchToAppsView which is already in angular scope
|
||||
$('#appInstallModal').on('hidden.bs.modal', function () {
|
||||
// clear the appid and version in the search bar when dialog is cancelled
|
||||
$scope.$apply(function () {
|
||||
$location.path('/appstore', false).search({ }); // 'false' means do not reload
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', hashChangeListener);
|
||||
|
||||
$scope.$on('$destroy', function handler() {
|
||||
window.removeEventListener('hashchange', hashChangeListener);
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['appInstallModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
// autofocus if appstore login is shown
|
||||
$scope.$watch('validSubscription', function (newValue/*, oldValue */) {
|
||||
if (!newValue) setTimeout(function () { $('[name=appstoreLoginForm]').find('[autofocus]:first').focus(); }, 1000);
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,647 +0,0 @@
|
||||
<!-- Modal details -->
|
||||
<div class="modal fade" id="backupDetailsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" style="width: 750px">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.backupDetails.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupDetails.id' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.id }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupEdit.label' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.label }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupEdit.remotePath' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.remotePath }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupDetails.date' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.creationTime | prettyLongDate }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupDetails.version' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">v{{ backupDetails.backup.packageVersion }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupDetails.format' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.format }}</div>
|
||||
</div>
|
||||
<br/>
|
||||
<p class="text-muted">{{ 'backups.backupDetails.list' | tr:{ appCount: backupDetails.backup.contents.length } }}:</p>
|
||||
<span ng-repeat="app in backupDetails.backup.contents | orderBy:['label','fqdn']">
|
||||
<a ng-href="/#/app/{{app.id}}/backups">{{ app.label || app.fqdn }}</a><span ng-hide="$last">,</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal edit individual backup (label and retention sec) -->
|
||||
<div class="modal fade" id="editBackupModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.backupEdit.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="editBackupForm" role="form" novalidate ng-submit="editBackup.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="editBackup.error">{{ editBackup.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputBackupLabel">{{ 'backups.backupEdit.label' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="editBackup.label" id="inputBackupLabel" name="label" ng-disabled="editBackup.busy" placeholder="" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="editBackup.persist">{{ 'backups.backupEdit.preserved.description' | tr }}</input>
|
||||
|
||||
<sup><a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{ 'backups.backupEdit.preserved.tooltip' | tr: { appsLength: editBackup.backup.contents.length} }}"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="editBackup.submit()" ng-disabled="editBackupForm.$invalid || editBackup.busy"><i class="fa fa-circle-notch fa-spin" ng-show="editBackup.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal backup failed -->
|
||||
<div class="modal fade" id="createBackupFailedModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.backupFailed.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ createBackup.errorMessage }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cleanup backups info -->
|
||||
<div class="modal fade" id="cleanupBackupsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.cleanupBackups.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">{{ 'backups.cleanupBackups.description' | tr }}</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="cleanupBackups.start()">{{ 'backups.cleanupBackups.cleanupNow' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal backup schedule config -->
|
||||
<div class="modal fade" id="backupPolicyModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.configureBackupSchedule.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="backupPolicyForm" role="form" novalidate ng-submit="backupPolicy.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="backupPolicy.error">{{ backupPolicy.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="backupSchedule">{{ 'backups.configureBackupSchedule.schedule' | tr }}</label>
|
||||
<p ng-bind-html="'backups.configureBackupSchedule.scheduleDescription' | tr"></p>
|
||||
|
||||
<div class="row" style="margin-left: 20px;">
|
||||
<div class="col-md-5" ng-class="{ 'has-error': !backupPolicy.days.length }">
|
||||
{{ 'backups.configureBackupSchedule.days' | tr }}: <multiselect id="backupSchedule" class="input-sm stretch" ng-model="backupPolicy.days" options="a.name for a in cronDays" data-multiple="true" ng-required></multiselect>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5" ng-class="{ 'has-error': !backupPolicy.hours.length }">
|
||||
{{ 'backups.configureBackupSchedule.hours' | tr }}: <multiselect class="input-sm stretch" ng-model="backupPolicy.hours" options="a.name for a in cronHours" data-multiple="true"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="backupRetention">{{ 'backups.configureBackupSchedule.retentionPolicy' | tr }}</label>
|
||||
<select class="form-control" id="backupRetention" ng-model="backupPolicy.retention" ng-options="a.value as a.name for a in backupRetentions"></select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="backupPolicy.submit()" ng-disabled="!backupPolicy.valid() || backupPolicy.busy"><i class="fa fa-circle-notch fa-spin" ng-show="backupPolicy.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal backup config -->
|
||||
<div class="modal fade" id="configureBackupModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.configureBackupStorage.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="configureBackupForm" role="form" novalidate ng-submit="configureBackup.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="configureBackup.error">{{ configureBackup.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProviderProvider">{{ 'backups.configureBackupStorage.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#storage-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small text-info" ng-show="backupConfig.provider !== configureBackup.provider">Backups in the old storage location have to be removed manually.</p>
|
||||
<select class="form-control" id="storageProviderProvider" ng-model="configureBackup.provider" ng-options="a.value as a.name for a in storageProviders" ng-change=configureBackup.clearProviderFields()></select>
|
||||
</div>
|
||||
|
||||
<!-- Noop -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'noop'">
|
||||
<p class="has-error">{{ 'backups.configureBackupStorage.noopNote' | tr }}</p>
|
||||
</div>
|
||||
|
||||
<!-- mountpoint -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.mountPoint || (configureBackupForm.mountPoint.$dirty && !configureBackup.mountPoint) }" ng-show="configureBackup.provider === 'mountpoint'">
|
||||
<label class="control-label" for="inputConfigureMountPoint">{{ 'backups.configureBackupStorage.mountPoint' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountPoint" id="inputConfigureMountPoint" name="mountPoint" ng-disabled="configureBackup.busy" placeholder="/mnt/backups" ng-required="configureBackup.provider === 'mountpoint'">
|
||||
<p ng-show="configureBackup.provider === 'mointpoint'" ng-bind-html="'backups.configureBackupStorage.mountPointDescription' | tr:{ providerDocsLink: 'https://docs.cloudron.io/backups/#'+configureBackup.provider }"></p>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupHost">{{ 'backups.configureBackupStorage.server' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.host" id="configureBackupHost" name="host" ng-disabled="configureBackup.busy" placeholder="Server IP or hostname" ng-required="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.mountOptions.seal">{{ 'backups.configureBackupStorage.cifsSealSupport' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupRemoteDir">{{ 'backups.configureBackupStorage.remoteDirectory' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.remoteDir" id="configureBackupRemoteDir" name="remoteDir" ng-disabled="configureBackup.busy" placeholder="/share" ng-required="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupUsername">{{ 'backups.configureBackupStorage.username' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.username" id="configureBackupUsername" name="cifsUsername" ng-disabled="configureBackup.busy">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupPassword">{{ 'backups.configureBackupStorage.password' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="password" class="form-control" ng-model="configureBackup.mountOptions.password" id="configureBackupPassword" name="cifsPassword" ng-disabled="configureBackup.busy" password-reveal>
|
||||
</div>
|
||||
|
||||
<!-- EXT4/XFS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.diskPath || !configureBackup.mountOptions.diskPath }" ng-show="configureBackup.provider === 'xfs' || configureBackup.provider === 'ext4'">
|
||||
<label class="control-label" for="inputConfigureDiskPath">{{ 'backups.configureBackupStorage.diskPath' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="configureBackup.busy" placeholder="/dev/disk/by-uuid/uuid" ng-required="configureBackup.provider === 'xfs' || configureBackup.provider === 'ext4'">
|
||||
</div>
|
||||
|
||||
<!-- Disk -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.diskPath || !configureBackup.mountOptions.diskPath }" ng-show="configureBackup.provider === 'disk'">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.diskPath' | tr }}</label>
|
||||
<select class="form-control" ng-model="configureBackup.disk" ng-options="item as item.label for item in configureBackup.blockDevices track by item.path" ng-required="configureBackup.provider === 'disk'"></select>
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPort">{{ 'backups.configureBackupStorage.port' | tr }}</label>
|
||||
<input type="number" class="form-control" ng-model="configureBackup.mountOptions.port" id="configureBackupPort" name="port" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupUser">{{ 'backups.configureBackupStorage.user' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.user" id="configureBackupUser" name="user" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPrivateKey">{{ 'backups.configureBackupStorage.privateKey' | tr }}</label>
|
||||
<textarea class="form-control" ng-model="configureBackup.mountOptions.privateKey" id="configureBackupPrivateKey" name="privateKey" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'sshfs'"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.backupFolder }" ng-show="configureBackup.provider === 'filesystem'">
|
||||
<label class="control-label" for="inputConfigureBackupFolder">{{ 'backups.configureBackupStorage.localDirectory' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.backupFolder" id="inputConfigureBackupFolder" name="backupFolder" ng-disabled="configureBackup.busy" placeholder="Directory for backups" ng-required="configureBackup.provider === 'filesystem'">
|
||||
</div>
|
||||
|
||||
<!-- Filesystem/SSHFS/CIFS/NFS/EXT4/mountpoint -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'filesystem' || mountlike(configureBackup.provider)">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.useHardlinks">{{ 'backups.configureBackupStorage.hardlinksLabel' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- mountpoint -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'mountpoint'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.chown">{{ 'backups.configureBackupStorage.chown' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS/UpCloud/B2/R2 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.endpoint }" ng-show="configureBackup.provider === 'minio' || configureBackup.provider === 'upcloud-objectstorage' || configureBackup.provider === 'backblaze-b2' || configureBackup.provider === 'cloudflare-r2' || configureBackup.provider === 's3-v4-compat' || configureBackup.provider === 'idrive-e2'">
|
||||
<label class="control-label" for="inputConfigureBackupEndpoint">{{ 'backups.configureBackupStorage.s3Endpoint' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.endpoint" id="inputConfigureBackupEndpoint" name="endpoint" ng-disabled="configureBackup.busy" placeholder="URL" ng-required="configureBackup.provider === 'minio' || configureBackup.provider === 'upcloud-objectstorage' || configureBackup.provider === 'backblaze-b2' || configureBackup.provider === 'cloudflare-r2' || configureBackup.provider === 's3-v4-compat' || configureBackup.provider === 'idrive-e2'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'minio' || configureBackup.provider === 's3-v4-compat'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.acceptSelfSignedCerts">{{ 'backups.configureBackupStorage.acceptSelfSignedCerts' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.bucket }" ng-show="s3like(configureBackup.provider) || configureBackup.provider === 'gcs'">
|
||||
<label class="control-label" for="inputConfigureBackupBucket">{{ 'backups.configureBackupStorage.bucketName' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.bucket" id="inputConfigureBackupBucket" name="bucket" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS/SSHFS/CIFS/NFS/B2 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.prefix }" ng-show="configureBackup.provider !== 'filesystem' && configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPrefix">{{ 'backups.configureBackupStorage.prefix' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.prefix" id="inputConfigureBackupPrefix" name="prefix" ng-disabled="configureBackup.busy" placeholder="Prefix for backup file names">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 's3'">
|
||||
<label class="control-label" for="inputConfigureBackupS3Region">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupS3Region" ng-model="configureBackup.region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 's3'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 's3-v4-compat'">
|
||||
<label class="control-label" for="inputConfigureBackupS3V4CompatRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<input class="form-control" type="text" name="region" id="inputConfigureBackupS3V4CompatRegion" ng-model="configureBackup.region" ng-disabled="configureBackup.busy" placeholder="Leave empty to use us-east-1 as default"></input>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'digitalocean-spaces'">
|
||||
<label class="control-label" for="inputConfigureBackupDORegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupDORegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'digitalocean-spaces'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'exoscale-sos'">
|
||||
<label class="control-label" for="inputConfigureBackupExoscaleRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupExoscaleRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'exoscale-sos'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'wasabi'">
|
||||
<label class="control-label" for="inputConfigureBackupWasabiRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupWasabiRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in wasabiRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'wasabi'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'scaleway-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupScalewayRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupScalewayRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in scalewayRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'scaleway-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'linode-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupLinodeRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupLinodeRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in linodeRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'linode-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'ovh-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupOvhRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupOvhRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in ovhRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'ovh-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'ionos-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupIonosRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupIonosRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in ionosRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'ionos-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'vultr-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupVultrRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupVultrRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in vultrRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'vultr-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'contabo-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupContaboRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupContaboRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in contaboRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'contabo-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.accessKeyId }" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="inputConfigureBackupAccessKeyId">{{ 'backups.configureBackupStorage.s3AccessKeyId' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.secretAccessKey }" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="inputConfigureBackupSecretAccessKey">{{ 'backups.configureBackupStorage.s3SecretAccessKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.secretAccessKey" id="inputConfigureBackupSecretAccessKey" name="secretAccessKey" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.gcsKeyInput }" ng-show="configureBackup.provider === 'gcs'">
|
||||
<label class="control-label" for="gcsKeyInput">{{ 'backups.configureBackupStorage.gcsServiceKey' | tr }}</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="configureBackup.gcsKey.keyFileName" id="gcsKeyInput" name="cert" onclick="getElementById('gcsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'gcs'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="storageFormat">{{ 'backups.configureBackupStorage.format' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#backup-formats" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small text-info" ng-show="backupConfig.format !== configureBackup.format">{{ 'backups.configureBackupStorage.formatChangeNote' | tr }}</p>
|
||||
<p class="small text-info" ng-show="configureBackup.format === 'rsync' && (s3like(configureBackup.provider) || configureBackup.provider === 'gcs')">{{ 'backups.configureBackupStorage.s3LikeNote' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#amazon-s3" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<select class="form-control" id="storageFormat" ng-model="configureBackup.format" ng-options="a.value as a.name for a in formats"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.password }" ng-show="configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPassword">{{ 'backups.configureBackupStorage.encryptionPassword' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#encryption" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.encryptionDescription' | tr }}</p>
|
||||
<input type="text" class="form-control" name="encryptionPassword" ng-model="configureBackup.password" id="inputConfigureBackupPassword" ng-disabled="configureBackup.busy" placeholder="{{ 'backups.configureBackupStorage.encryptionPasswordPlaceholder' | tr }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.password && configureBackup.password !== SECRET_PLACEHOLDER" ng-class="{ 'has-error': (configureBackupForm.encryptionPassword.$dirty && configureBackup.password !== configureBackup.passwordRepeat) }">
|
||||
<label class="control-label" for="inputConfigureBackupPasswordRepeat">{{ 'backups.configureBackupStorage.encryptionPasswordRepeat' | tr }}</label>
|
||||
<input id="inputConfigureBackupPasswordRepeat" type="text" class="form-control" name="passwordRepeat" ng-model="configureBackup.passwordRepeat" ng-disabled="configureBackup.busy">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="configureBackup.password !== '' && configureBackup.format === 'rsync'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.encryptedFilenames">{{ 'backups.configureBackupStorage.encryptFilenames' | tr }}</input>
|
||||
<sup><a ng-href="https://docs.cloudron.io/backups/#filenames" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<a href="" ng-click="configureBackup.advancedVisible = true" ng-hide="configureBackup.advancedVisible">{{ 'backups.configureBackupStorage.advancedSettings' | tr }}</a>
|
||||
<div uib-collapse="!configureBackup.advancedVisible">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.memoryLimit' | tr }}: <b>{{ configureBackup.memoryLimit | prettyBinarySize:'800 MB' }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.memoryLimitDescription' | tr }}</p>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="sliderConfigureBackupMemoryLimit" ng-model="configureBackup.memoryLimit" step="134217728" tooltip="hide" ticks="configureBackup.memoryTicks" ticks-snap-bounds="67108864"></slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.uploadPartSize' | tr }}: <b>{{ configureBackup.uploadPartSize | prettyBinarySize:'Default (50 MiB)' }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.uploadPartSizeDescription' | tr }}</p>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="sliderConfigureBackupUploadPartSize" ng-model="configureBackup.uploadPartSize" step="1048576" tooltip="hide" ticks="configureBackup.uploadPartSizeTicks" ticks-snap-bounds="2097152"></slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.format === 'rsync' && configureBackup.provider !== 'noop'">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.uploadConcurrency' | tr }}: <b>{{ configureBackup.syncConcurrency }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.uploadConcurrencyDescription' | tr }}</p>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="sliderConfigureBackupSyncConcurrency" ng-model="configureBackup.syncConcurrency" tooltip="hide" min="10" max="200" step="10"></slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.format === 'rsync' && (s3like(configureBackup.provider) || configureBackup.provider === 'gcs')">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.downloadConcurrency' | tr }}: <b>{{ configureBackup.downloadConcurrency }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.downloadConcurrencyDescription' | tr }}</p>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="sliderConfigureBackupCopyConcurrency" ng-model="configureBackup.downloadConcurrency" tooltip="hide" min="10" max="200" step="10"></slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.format === 'rsync' && (s3like(configureBackup.provider) || configureBackup.provider === 'gcs')">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.copyConcurrency' | tr }}: <b>{{ configureBackup.copyConcurrency }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.copyConcurrencyDescription' | tr }}
|
||||
<span ng-show="configureBackup.provider === 'digitalocean-spaces'">{{ 'backups.configureBackupStorage.copyConcurrencyDigitalOceanNote' | tr }}</span>
|
||||
</p>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="sliderConfigureBackupCopyConcurrency" ng-model="configureBackup.copyConcurrency" tooltip="hide" min="10" max="500" step="10"></slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- advanced -->
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="configureBackupForm.$invalid || (configureBackup.password !== SECRET_PLACEHOLDER && configureBackup.password !== configureBackup.passwordRepeat)"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="configureBackup.submit()" ng-disabled="configureBackupForm.$invalid || configureBackup.busy || (configureBackup.password !== SECRET_PLACEHOLDER && configureBackup.password !== configureBackup.passwordRepeat)"><i class="fa fa-circle-notch fa-spin" ng-show="configureBackup.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>{{ 'backups.title' | tr }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'backups.location.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<p>{{ 'backups.location.description' | tr }}
|
||||
<span ng-show="manualBackupApps.length">
|
||||
{{ 'backups.location.disabledList' | tr }}
|
||||
<span ng-repeat="app in manualBackupApps">
|
||||
<a ng-href="/#/app/{{app.id}}/backups">{{app.label || app.fqdn}}</a><span ng-hide="$last">,</span>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p ng-show="backupConfig.provider === 'noop'" class="text-danger" ng-bind-html="'backups.check.noop' | tr | markdown2html"></p>
|
||||
<p ng-show="backupConfig.provider === 'filesystem'" class="text-danger" ng-bind-html="'backups.check.sameDisk' | tr | markdown2html"></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.location.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ prettyProviderName(backupConfig.provider) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.location.location' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right no-wrap">
|
||||
<span ng-show="backupConfig.provider === 'filesystem'">{{ backupConfig.backupFolder }}</span>
|
||||
<span ng-show="mountlike(backupConfig.provider)">
|
||||
<i class="fa fa-circle" ng-style="{ color: mountStatus.state === 'active' ? '#27CE65' : '#d9534f' }" ng-show="mountStatus" uib-tooltip="{{ mountStatus.message }}"></i>
|
||||
<span ng-show="backupConfig.provider === 'disk' || backupConfig.provider === 'filesystem' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs' || backupConfig.provider === 'mountpoint'">{{ backupConfig.mountOptions.diskPath || backupConfig.mountPoint }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.provider === 'sshfs'">{{ backupConfig.mountOptions.host }}:{{ backupConfig.mountOptions.remoteDir }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
</span>
|
||||
|
||||
<span ng-show="backupConfig.provider !== 's3' && backupConfig.provider !== 'minio' && (s3like(backupConfig.provider) || backupConfig.provider === 'gcs')">{{ backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 's3'">{{ backupConfig.region + ' ' + backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 'minio'">{{ backupConfig.endpoint + ' ' + backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="backupConfig.endpoint && backupConfig.provider !== 'minio'">
|
||||
<div class="col-xs-3">
|
||||
<span class="text-muted">{{ 'backups.location.endpoint' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-9 text-right">
|
||||
<span>{{ backupConfig.endpoint || backupConfig.region }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.location.format' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ backupConfig.format }} <i class="fas fa-lock" ng-show="backupConfig.password" ></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-show="user.isAtLeastOwner" ng-click="configureBackup.show()">{{ 'backups.location.configure' | tr }}</button>
|
||||
<button class="btn btn-outline btn-default pull-right" ng-show="user.isAtLeastOwner && mountlike(backupConfig.provider)" ng-disabled="remount.busy" ng-click="remount.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="remount.busy"></i> {{ 'backups.location.remount' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'backups.schedule.title' | tr }}
|
||||
<!-- <a class="btn btn-sm btn-default pull-right" ng-href="/frontend/logs.html?taskId={{cleanupBackups.taskId}}" target="_blank" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}"><i class="fas fa-align-left"></i></a> -->
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="cleanupTasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in cleanupTasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<p>{{ 'backups.schedule.description' | tr }}</p>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.schedule.schedule' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ prettyBackupSchedule(backupPolicy.currentPolicy.schedule) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.schedule.retentionPolicy' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ prettyBackupRetention(backupPolicy.currentPolicy.retention) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-default" ng-click="cleanupBackups.ask()" ng-disabled="cleanupBackups.busy" style="margin-right: 5px"><i class="fa fa-circle-notch fa-spin" ng-show="cleanupBackups.busy"></i> {{ 'backups.listing.cleanupBackups' | tr }}</button>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-show="user.isAtLeastOwner" ng-click="backupPolicy.show()">{{ 'backups.schedule.configure' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'backups.listing.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="backupTasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in backupTasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="!backups.length">{{ 'backups.listing.noBackups' | tr }}</p>
|
||||
|
||||
<table class="table table-hover" style="margin: 0;" ng-hide="!backups.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 20px"></th>
|
||||
<th>{{ 'backups.listing.version' | tr }}</th>
|
||||
<th>{{ 'main.table.date' | tr }}</th>
|
||||
<th>{{ 'backups.listing.contents' | tr }}</th>
|
||||
<th class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="backup in backups">
|
||||
<td><i class="fas fa-archive" ng-show="backup.preserveSecs === -1" uib-tooltip="{{ 'backups.listing.tooltipPreservedBackup' | tr }}"></i></td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand">v{{ backup.packageVersion }}</td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand"><span uib-tooltip="{{ backup.creationTime | prettyLongDate }}">{{ backup.creationTime | prettyDate }} <b ng-show="backup.label">({{ backup.label }})</b></span></td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand">
|
||||
<span ng-show="!backup.contents.length">{{ 'backups.listing.noApps' | tr }}</span>
|
||||
<span ng-show="backup.contents.length">{{ 'backups.listing.appCount' | tr:{ appCount: backup.contents.length } }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="editBackup.show(backup)" uib-tooltip="{{ 'backups.listing.tooltipEditBackup' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="downloadConfig(backup)" uib-tooltip="{{ 'backups.listing.tooltipDownloadBackupConfig' | tr }}"><i class="fas fa-file-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row" ng-show="createBackup.busy">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ createBackup.percent }}%"></div>
|
||||
</div>
|
||||
<p>{{ createBackup.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="!createBackup.busy && !createBackup.active && createBackup.errorMessage">
|
||||
<div class="col-md-12">
|
||||
<p class="has-error">{{ createBackup.errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary" ng-click="createBackup.startBackup()" ng-show="!createBackup.busy">{{ 'backups.listing.backupNow' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="createBackup.stopTask()" ng-show="createBackup.busy">{{ 'backups.listing.stopTask' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,895 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, angular, TASK_TYPES, SECRET_PLACEHOLDER, STORAGE_PROVIDERS, BACKUP_FORMATS, APP_TYPES */
|
||||
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR , REGIONS_CONTABO */
|
||||
|
||||
angular.module('Application').controller('BackupsController', ['$scope', '$location', '$rootScope', '$timeout', 'Client', function ($scope, $location, $rootScope, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.SECRET_PLACEHOLDER = SECRET_PLACEHOLDER;
|
||||
$scope.MIN_MEMORY_LIMIT = 800 * 1024 * 1024;
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.memory = null; // { memory, swap }
|
||||
$scope.mountStatus = null; // { state, message }
|
||||
$scope.manualBackupApps = [];
|
||||
|
||||
$scope.backupConfig = {};
|
||||
$scope.backups = [];
|
||||
$scope.backupTasks = [];
|
||||
$scope.cleanupTasks = [];
|
||||
|
||||
$scope.s3Regions = REGIONS_S3;
|
||||
$scope.wasabiRegions = REGIONS_WASABI;
|
||||
$scope.doSpacesRegions = REGIONS_DIGITALOCEAN;
|
||||
$scope.exoscaleSosRegions = REGIONS_EXOSCALE;
|
||||
$scope.scalewayRegions = REGIONS_SCALEWAY;
|
||||
$scope.linodeRegions = REGIONS_LINODE;
|
||||
$scope.ovhRegions = REGIONS_OVH;
|
||||
$scope.ionosRegions = REGIONS_IONOS;
|
||||
$scope.upcloudRegions = REGIONS_UPCLOUD;
|
||||
$scope.vultrRegions = REGIONS_VULTR;
|
||||
$scope.contaboRegions = REGIONS_CONTABO;
|
||||
|
||||
$scope.storageProviders = STORAGE_PROVIDERS.concat([
|
||||
{ name: 'No-op (Only for testing)', value: 'noop' }
|
||||
]);
|
||||
|
||||
$scope.backupRetentions = [
|
||||
{ name: '2 days', value: { keepWithinSecs: 2 * 24 * 60 * 60 }},
|
||||
{ name: '1 week', value: { keepWithinSecs: 7 * 24 * 60 * 60 }}, // default
|
||||
{ name: '1 month', value: { keepWithinSecs: 30 * 24 * 60 * 60 }},
|
||||
{ name: '3 months', value: { keepWithinSecs: 3 * 30 * 24 * 60 * 60 }},
|
||||
{ name: '2 daily, 4 weekly', value: { keepDaily: 2, keepWeekly: 4 }},
|
||||
{ name: '3 daily, 4 weekly, 6 monthly', value: { keepDaily: 3, keepWeekly: 4, keepMonthly: 6 }},
|
||||
{ name: '7 daily, 4 weekly, 12 monthly', value: { keepDaily: 7, keepWeekly: 4, keepMonthly: 12 }},
|
||||
{ name: 'Forever', value: { keepWithinSecs: -1 }}
|
||||
];
|
||||
|
||||
// values correspond to cron days
|
||||
$scope.cronDays = [
|
||||
{ name: 'Sunday', value: 0 },
|
||||
{ name: 'Monday', value: 1 },
|
||||
{ name: 'Tuesday', value: 2 },
|
||||
{ name: 'Wednesday', value: 3 },
|
||||
{ name: 'Thursday', value: 4 },
|
||||
{ name: 'Friday', value: 5 },
|
||||
{ name: 'Saturday', value: 6 },
|
||||
];
|
||||
|
||||
// generates 24h time sets (instead of american 12h) to avoid having to translate everything to locales eg. 12:00
|
||||
$scope.cronHours = Array.from({ length: 24 }).map(function (v, i) { return { name: (i < 10 ? '0' : '') + i + ':00', value: i }; });
|
||||
|
||||
$scope.formats = BACKUP_FORMATS;
|
||||
|
||||
$scope.prettyProviderName = function (provider) {
|
||||
switch (provider) {
|
||||
case 'caas': return 'Managed Cloudron';
|
||||
default: return provider;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.prettyBackupSchedule = function (pattern) {
|
||||
if (!pattern) return '';
|
||||
var tmp = pattern.split(' ');
|
||||
var hours = tmp[2].split(','), days = tmp[5].split(',');
|
||||
var prettyDay;
|
||||
if (days.length === 7 || days[0] === '*') {
|
||||
prettyDay = 'Everyday';
|
||||
} else {
|
||||
prettyDay = days.map(function (day) { return $scope.cronDays[parseInt(day, 10)].name.substr(0, 3); }).join(',');
|
||||
}
|
||||
|
||||
var prettyHour = hours.map(function (hour) { return $scope.cronHours[parseInt(hour, 10)].name; }).join(',');
|
||||
|
||||
return prettyDay + ' at ' + prettyHour;
|
||||
};
|
||||
|
||||
$scope.prettyBackupRetention = function (retention) {
|
||||
var tmp = $scope.backupRetentions.find(function (p) { return angular.equals(p.value, retention); });
|
||||
return tmp ? tmp.name : '';
|
||||
};
|
||||
|
||||
$scope.remount = {
|
||||
busy: false,
|
||||
error: null,
|
||||
|
||||
submit: function () {
|
||||
if (!$scope.mountlike($scope.backupConfig.provider)) return;
|
||||
|
||||
$scope.remount.busy = true;
|
||||
$scope.remount.error = null;
|
||||
|
||||
Client.remountBackupStorage(function (error) {
|
||||
if (error) {
|
||||
console.error('Failed to remount backup storage.', error);
|
||||
$scope.remount.error = error.message;
|
||||
}
|
||||
|
||||
// give the backend some time
|
||||
$timeout(function () {
|
||||
$scope.remount.busy = false;
|
||||
getBackupConfig();
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.createBackup = {
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
taskId: '',
|
||||
|
||||
init: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_BACKUP, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
|
||||
$scope.createBackup.taskId = task.id;
|
||||
$scope.createBackup.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.createBackup.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.createBackup.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.createBackup.busy = false;
|
||||
$scope.createBackup.message = '';
|
||||
$scope.createBackup.percent = 100; // indicates that 'result' is valid
|
||||
$scope.createBackup.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
getBackupTasks();
|
||||
|
||||
return fetchBackups();
|
||||
}
|
||||
|
||||
$scope.createBackup.busy = true;
|
||||
$scope.createBackup.percent = data.percent;
|
||||
$scope.createBackup.message = data.message;
|
||||
window.setTimeout($scope.createBackup.updateStatus, 3000);
|
||||
});
|
||||
},
|
||||
|
||||
startBackup: function () {
|
||||
$scope.createBackup.busy = true;
|
||||
$scope.createBackup.percent = 0;
|
||||
$scope.createBackup.message = '';
|
||||
$scope.createBackup.errorMessage = '';
|
||||
|
||||
Client.startBackup(function (error, taskId) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409 && error.message.indexOf('full_backup') !== -1) {
|
||||
$scope.createBackup.errorMessage = 'Backup already in progress. Please retry later.';
|
||||
} else if (error.statusCode === 409) {
|
||||
$scope.createBackup.errorMessage = 'App task is currently in progress. Please retry later.';
|
||||
} else {
|
||||
console.error(error);
|
||||
$scope.createBackup.errorMessage = error.message;
|
||||
}
|
||||
|
||||
$scope.createBackup.busy = false;
|
||||
$('#createBackupFailedModal').modal('show');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
getBackupTasks();
|
||||
|
||||
$scope.createBackup.taskId = taskId;
|
||||
$scope.createBackup.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
stopTask: function () {
|
||||
Client.stopTask($scope.createBackup.taskId, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409) {
|
||||
$scope.createBackup.errorMessage = 'No backup is currently in progress';
|
||||
} else {
|
||||
console.error(error);
|
||||
$scope.createBackup.errorMessage = error.message;
|
||||
}
|
||||
|
||||
$scope.createBackup.busy = false;
|
||||
getBackupTasks();
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.cleanupBackups = {
|
||||
busy: false,
|
||||
taskId: 0,
|
||||
|
||||
init: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_CLEAN_BACKUPS, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
|
||||
$scope.cleanupBackups.taskId = task.id;
|
||||
$scope.cleanupBackups.updateStatus();
|
||||
|
||||
getCleanupTasks();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.cleanupBackups.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.cleanupBackups.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.cleanupBackups.busy = false;
|
||||
|
||||
getCleanupTasks();
|
||||
fetchBackups();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.cleanupBackups.busy = true;
|
||||
$scope.cleanupBackups.message = data.message;
|
||||
window.setTimeout($scope.cleanupBackups.updateStatus, 3000);
|
||||
});
|
||||
},
|
||||
|
||||
ask: function () {
|
||||
$('#cleanupBackupsModal').modal('show');
|
||||
},
|
||||
|
||||
start: function () {
|
||||
$scope.cleanupBackups.busy = true;
|
||||
|
||||
$('#cleanupBackupsModal').modal('hide');
|
||||
|
||||
Client.cleanupBackups(function (error, taskId) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.cleanupBackups.taskId = taskId;
|
||||
$scope.cleanupBackups.updateStatus();
|
||||
|
||||
getCleanupTasks();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.listBackups = {
|
||||
};
|
||||
|
||||
$scope.s3like = function (provider) {
|
||||
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat'
|
||||
|| provider === 'exoscale-sos' || provider === 'digitalocean-spaces'
|
||||
|| provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2' || provider === 'cloudflare-r2'
|
||||
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage'
|
||||
|| provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2'
|
||||
|| provider === 'contabo-objectstorage';
|
||||
};
|
||||
|
||||
$scope.mountlike = function (provider) {
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs' || provider === 'disk';
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server#18197341
|
||||
function download(filename, text) {
|
||||
var element = document.createElement('a');
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||
element.setAttribute('download', filename);
|
||||
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
|
||||
element.click();
|
||||
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
$scope.downloadConfig = function (backup) {
|
||||
// secrets and tokens already come with placeholder characters we remove them
|
||||
var tmp = {
|
||||
remotePath: backup.remotePath,
|
||||
encrypted: !!$scope.backupConfig.password // we add this just to help the import UI
|
||||
};
|
||||
|
||||
Object.keys($scope.backupConfig).forEach(function (k) {
|
||||
if ($scope.backupConfig[k] !== SECRET_PLACEHOLDER) tmp[k] = $scope.backupConfig[k];
|
||||
});
|
||||
|
||||
var filename = 'cloudron-backup-config-' + (new Date()).toISOString().replace(/:|T/g,'-').replace(/\..*/,'') + ' (' + $scope.config.adminFqdn + ')' + '.json';
|
||||
download(filename, JSON.stringify(tmp, null, 4));
|
||||
};
|
||||
|
||||
$scope.editBackup = {
|
||||
busy: false,
|
||||
error: null,
|
||||
backup: null,
|
||||
|
||||
label: '',
|
||||
persist: false,
|
||||
|
||||
show: function (backup) {
|
||||
$scope.editBackup.backup = backup;
|
||||
$scope.editBackup.label = backup.label;
|
||||
$scope.editBackup.persist = backup.preserveSecs === -1;
|
||||
$scope.editBackup.error = null;
|
||||
$scope.editBackup.busy = false;
|
||||
|
||||
$('#editBackupModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.editBackup.error = null;
|
||||
$scope.editBackup.busy = true;
|
||||
|
||||
Client.editBackup($scope.editBackup.backup.id, $scope.editBackup.label, $scope.editBackup.persist ? -1 : 0, function (error) {
|
||||
$scope.editBackup.busy = false;
|
||||
if (error) return $scope.editBackup.error = error.message;
|
||||
|
||||
fetchBackups();
|
||||
|
||||
$('#editBackupModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.backupDetails = {
|
||||
backup: null,
|
||||
|
||||
show: function (backup) {
|
||||
$scope.backupDetails.backup = backup;
|
||||
$('#backupDetailsModal').modal('show');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.backupPolicy = {
|
||||
busy: false,
|
||||
error: {},
|
||||
|
||||
currentPolicy: null,
|
||||
|
||||
retention: null,
|
||||
days: [],
|
||||
hours: [],
|
||||
|
||||
init: function () {
|
||||
Client.getBackupPolicy(function (error, policy) {
|
||||
if (error) Client.error(error);
|
||||
$scope.backupPolicy.currentPolicy = policy;
|
||||
});
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.backupPolicy.error = {};
|
||||
$scope.backupPolicy.busy = false;
|
||||
|
||||
var selectedRetention = $scope.backupRetentions.find(function (x) { return angular.equals(x.value, $scope.backupPolicy.currentPolicy.retention); });
|
||||
if (!selectedRetention) selectedRetention = $scope.backupRetentions[0];
|
||||
|
||||
$scope.backupPolicy.retention = selectedRetention.value;
|
||||
|
||||
var tmp = $scope.backupPolicy.currentPolicy.schedule.split(' ');
|
||||
var hours = tmp[2].split(','), days = tmp[5].split(',');
|
||||
if (days[0] === '*') {
|
||||
$scope.backupPolicy.days = angular.copy($scope.cronDays, []);
|
||||
} else {
|
||||
$scope.backupPolicy.days = days.map(function (day) { return $scope.cronDays[parseInt(day, 10)]; });
|
||||
}
|
||||
$scope.backupPolicy.hours = hours.map(function (hour) { return $scope.cronHours[parseInt(hour, 10)]; });
|
||||
|
||||
$('#backupPolicyModal').modal('show');
|
||||
},
|
||||
|
||||
valid: function () {
|
||||
return $scope.backupPolicy.days.length && $scope.backupPolicy.hours.length;
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
if (!$scope.backupPolicy.days.length) return;
|
||||
if (!$scope.backupPolicy.hours.length) return;
|
||||
|
||||
$scope.backupPolicy.error = {};
|
||||
$scope.backupPolicy.busy = true;
|
||||
|
||||
var daysPattern;
|
||||
if ($scope.backupPolicy.days.length === 7) daysPattern = '*';
|
||||
else daysPattern = $scope.backupPolicy.days.map(function (d) { return d.value; });
|
||||
|
||||
var hoursPattern;
|
||||
if ($scope.backupPolicy.hours.length === 24) hoursPattern = '*';
|
||||
else hoursPattern = $scope.backupPolicy.hours.map(function (d) { return d.value; });
|
||||
|
||||
var policy = {
|
||||
retention: $scope.backupPolicy.retention,
|
||||
schedule: '00 00 ' + hoursPattern + ' * * ' + daysPattern
|
||||
};
|
||||
|
||||
Client.setBackupPolicy(policy, function (error) {
|
||||
$scope.backupPolicy.busy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 424) {
|
||||
$scope.backupPolicy.error.generic = error.message;
|
||||
} else if (error.statusCode === 400) {
|
||||
$scope.backupPolicy.error.generic = error.message;
|
||||
} else {
|
||||
console.error('Unable to change schedule or retention.', error);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$('#backupPolicyModal').modal('hide');
|
||||
|
||||
$scope.backupPolicy.init();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('configureBackup.disk', function (newValue) {
|
||||
if (!newValue) return;
|
||||
$scope.configureBackup.mountOptions.diskPath = '/dev/disk/by-uuid/' + newValue.uuid;
|
||||
});
|
||||
|
||||
$scope.configureBackup = {
|
||||
busy: false,
|
||||
error: {},
|
||||
|
||||
provider: '',
|
||||
bucket: '',
|
||||
prefix: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
gcsKey: { keyFileName: '', content: '' },
|
||||
region: '',
|
||||
endpoint: '',
|
||||
backupFolder: '',
|
||||
mountPoint: '',
|
||||
acceptSelfSignedCerts: false,
|
||||
useHardlinks: true,
|
||||
chown: true,
|
||||
format: 'tgz',
|
||||
password: '',
|
||||
passwordRepeat: '',
|
||||
encryptedFilenames: true,
|
||||
advancedVisible: false,
|
||||
|
||||
memoryTicks: [],
|
||||
memoryLimit: $scope.MIN_MEMORY_LIMIT,
|
||||
uploadPartSizeTicks: [],
|
||||
uploadPartSize: 50 * 1024 * 1024,
|
||||
copyConcurrency: '',
|
||||
downloadConcurrency: '',
|
||||
syncConcurrency: '', // sort of similar to upload
|
||||
|
||||
blockDevices: [],
|
||||
disk: null,
|
||||
mountOptions: {
|
||||
host: '',
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
diskPath: '',
|
||||
seal: false,
|
||||
user: '',
|
||||
port: 22,
|
||||
privateKey: ''
|
||||
},
|
||||
|
||||
clearProviderFields: function () {
|
||||
$scope.configureBackup.bucket = '';
|
||||
$scope.configureBackup.prefix = '';
|
||||
$scope.configureBackup.accessKeyId = '';
|
||||
$scope.configureBackup.secretAccessKey = '';
|
||||
$scope.configureBackup.gcsKey.keyFileName = '';
|
||||
$scope.configureBackup.gcsKey.content = '';
|
||||
$scope.configureBackup.endpoint = '';
|
||||
$scope.configureBackup.region = '';
|
||||
$scope.configureBackup.backupFolder = '';
|
||||
$scope.configureBackup.mountPoint = '';
|
||||
$scope.configureBackup.acceptSelfSignedCerts = false;
|
||||
$scope.configureBackup.useHardlinks = true;
|
||||
$scope.configureBackup.chown = true;
|
||||
$scope.configureBackup.memoryLimit = $scope.MIN_MEMORY_LIMIT;
|
||||
|
||||
// scaleway only supports 1000 parts per object (https://www.scaleway.com/en/docs/s3-multipart-upload/)
|
||||
$scope.configureBackup.uploadPartSize = $scope.configureBackup.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024;
|
||||
$scope.configureBackup.downloadConcurrency = $scope.configureBackup.provider === 's3' ? 30 : 10;
|
||||
$scope.configureBackup.syncConcurrency = $scope.configureBackup.provider === 's3' ? 20 : 10;
|
||||
$scope.configureBackup.copyConcurrency = $scope.configureBackup.provider === 's3' ? 500 : 10;
|
||||
|
||||
$scope.configureBackup.disk = null;
|
||||
$scope.configureBackup.mountOptions = { host: '', remoteDir: '', username: '', password: '', diskPath: '', seal: false, user: '', port: 22, privateKey: '' };
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.configureBackup.error = {};
|
||||
$scope.configureBackup.busy = false;
|
||||
|
||||
$scope.configureBackup.advancedVisible = false;
|
||||
|
||||
$scope.configureBackup.provider = $scope.backupConfig.provider;
|
||||
$scope.configureBackup.bucket = $scope.backupConfig.bucket;
|
||||
$scope.configureBackup.prefix = $scope.backupConfig.prefix;
|
||||
$scope.configureBackup.region = $scope.backupConfig.region;
|
||||
$scope.configureBackup.accessKeyId = $scope.backupConfig.accessKeyId;
|
||||
$scope.configureBackup.secretAccessKey = $scope.backupConfig.secretAccessKey;
|
||||
if ($scope.backupConfig.provider === 'gcs') {
|
||||
$scope.configureBackup.gcsKey.keyFileName = $scope.backupConfig.credentials.client_email;
|
||||
$scope.configureBackup.gcsKey.content = JSON.stringify({
|
||||
project_id: $scope.backupConfig.projectId,
|
||||
client_email: $scope.backupConfig.credentials.client_email,
|
||||
private_key: $scope.backupConfig.credentials.private_key,
|
||||
});
|
||||
}
|
||||
$scope.configureBackup.endpoint = $scope.backupConfig.endpoint;
|
||||
$scope.configureBackup.password = $scope.backupConfig.password || '';
|
||||
$scope.configureBackup.passwordRepeat = '';
|
||||
$scope.configureBackup.encryptedFilenames = 'encryptedFilenames' in $scope.backupConfig ? $scope.backupConfig.encryptedFilenames : true;
|
||||
$scope.configureBackup.backupFolder = $scope.backupConfig.backupFolder;
|
||||
$scope.configureBackup.mountPoint = $scope.backupConfig.mountPoint;
|
||||
$scope.configureBackup.format = $scope.backupConfig.format;
|
||||
$scope.configureBackup.acceptSelfSignedCerts = !!$scope.backupConfig.acceptSelfSignedCerts;
|
||||
$scope.configureBackup.useHardlinks = !$scope.backupConfig.noHardlinks;
|
||||
$scope.configureBackup.chown = $scope.backupConfig.chown;
|
||||
|
||||
var limits = $scope.backupConfig.limits || {};
|
||||
$scope.configureBackup.memoryLimit = limits.memoryLimit;
|
||||
|
||||
$scope.configureBackup.uploadPartSize = limits.uploadPartSize || ($scope.configureBackup.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024);
|
||||
$scope.configureBackup.downloadConcurrency = limits.downloadConcurrency || ($scope.backupConfig.provider === 's3' ? 30 : 10);
|
||||
$scope.configureBackup.syncConcurrency = limits.syncConcurrency || ($scope.backupConfig.provider === 's3' ? 20 : 10);
|
||||
$scope.configureBackup.copyConcurrency = limits.copyConcurrency || ($scope.backupConfig.provider === 's3' ? 500 : 10);
|
||||
|
||||
var totalMemory = Math.max(($scope.memory.memory + $scope.memory.swap) * 1.5, 2 * 1024 * 1024);
|
||||
$scope.configureBackup.memoryTicks = [ $scope.MIN_MEMORY_LIMIT ];
|
||||
for (var i = 1024; i <= totalMemory/1024/1024; i *= 2) {
|
||||
$scope.configureBackup.memoryTicks.push(i * 1024 * 1024);
|
||||
}
|
||||
|
||||
$scope.configureBackup.uploadPartSizeTicks = [ 5 * 1024 * 1024 ];
|
||||
for (var j = 32; j <= 1 * 1024; j *= 2) { // 5 GB is max for s3. but let's keep things practical for now. we upload 3 parts in parallel
|
||||
$scope.configureBackup.uploadPartSizeTicks.push(j * 1024 * 1024);
|
||||
}
|
||||
|
||||
var mountOptions = $scope.backupConfig.mountOptions || {};
|
||||
$scope.configureBackup.mountOptions = {
|
||||
host: mountOptions.host || '',
|
||||
remoteDir: mountOptions.remoteDir || '',
|
||||
username: mountOptions.username || '',
|
||||
password: mountOptions.password || '',
|
||||
diskPath: mountOptions.diskPath || '',
|
||||
seal: mountOptions.seal,
|
||||
user: mountOptions.user || '',
|
||||
port: mountOptions.port || 22,
|
||||
privateKey: mountOptions.privateKey || ''
|
||||
};
|
||||
|
||||
Client.getBlockDevices(function (error, result) {
|
||||
if (error) return console.error('Failed to list blockdevices:', error);
|
||||
|
||||
// only offer non /, /boot or /home disks
|
||||
result = result.filter(function (d) { return d.mountpoint !== '/' && d.mountpoint !== '/home' && d.mountpoint !== '/boot'; });
|
||||
// only offer xfs and ext4 disks
|
||||
result = result.filter(function (d) { return d.type === 'xfs' || d.type === 'ext4'; });
|
||||
|
||||
// amend label for UI
|
||||
result.forEach(function (d) {
|
||||
d.label = d.path;
|
||||
|
||||
// pre-select current if set
|
||||
if (d.path === $scope.configureBackup.mountOptions.diskPath || ('/dev/disk/by-uuid/' + d.uuid) === $scope.configureBackup.mountOptions.diskPath) {
|
||||
$scope.configureBackup.disk = d;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.configureBackup.blockDevices = result;
|
||||
|
||||
$('#configureBackupModal').modal('show');
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.configureBackup.error = {};
|
||||
$scope.configureBackup.busy = true;
|
||||
|
||||
var backupConfig = {
|
||||
provider: $scope.configureBackup.provider,
|
||||
format: $scope.configureBackup.format,
|
||||
// required for api call to provide all fields
|
||||
schedulePattern: $scope.backupConfig.schedulePattern,
|
||||
retentionPolicy: $scope.backupConfig.retentionPolicy,
|
||||
limits: {
|
||||
memoryLimit: $scope.configureBackup.memoryLimit,
|
||||
},
|
||||
};
|
||||
if ($scope.configureBackup.password) {
|
||||
backupConfig.password = $scope.configureBackup.password;
|
||||
backupConfig.encryptedFilenames = $scope.configureBackup.encryptedFilenames; // ignored with tgz format
|
||||
}
|
||||
|
||||
// only set provider specific fields, this will clear them in the db
|
||||
if ($scope.s3like(backupConfig.provider)) {
|
||||
backupConfig.bucket = $scope.configureBackup.bucket;
|
||||
backupConfig.prefix = $scope.configureBackup.prefix;
|
||||
backupConfig.accessKeyId = $scope.configureBackup.accessKeyId;
|
||||
backupConfig.secretAccessKey = $scope.configureBackup.secretAccessKey;
|
||||
|
||||
if ($scope.configureBackup.endpoint) backupConfig.endpoint = $scope.configureBackup.endpoint;
|
||||
|
||||
if (backupConfig.provider === 's3') {
|
||||
if ($scope.configureBackup.region) backupConfig.region = $scope.configureBackup.region;
|
||||
delete backupConfig.endpoint;
|
||||
} else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') {
|
||||
backupConfig.region = $scope.configureBackup.region || 'us-east-1';
|
||||
backupConfig.acceptSelfSignedCerts = $scope.configureBackup.acceptSelfSignedCerts;
|
||||
backupConfig.s3ForcePathStyle = true; // might want to expose this in the UI
|
||||
} else if (backupConfig.provider === 'exoscale-sos') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'wasabi') {
|
||||
backupConfig.region = $scope.wasabiRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'scaleway-objectstorage') {
|
||||
backupConfig.region = $scope.scalewayRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'linode-objectstorage') {
|
||||
backupConfig.region = $scope.linodeRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'ovh-objectstorage') {
|
||||
backupConfig.region = $scope.ovhRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'ionos-objectstorage') {
|
||||
backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'vultr-objectstorage') {
|
||||
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'contabo-objectstorage') {
|
||||
backupConfig.region = $scope.contaboRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
backupConfig.s3ForcePathStyle = true; // https://docs.contabo.com/docs/products/Object-Storage/technical-description (no virtual buckets)
|
||||
} else if (backupConfig.provider === 'upcloud-objectstorage') { // the UI sets region and endpoint
|
||||
var m = /^.*\.(.*)\.upcloudobjects.com$/.exec(backupConfig.endpoint);
|
||||
backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'digitalocean-spaces') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
}
|
||||
} else if (backupConfig.provider === 'gcs') {
|
||||
backupConfig.bucket = $scope.configureBackup.bucket;
|
||||
backupConfig.prefix = $scope.configureBackup.prefix;
|
||||
try {
|
||||
var serviceAccountKey = JSON.parse($scope.configureBackup.gcsKey.content);
|
||||
backupConfig.projectId = serviceAccountKey.project_id;
|
||||
backupConfig.credentials = {
|
||||
client_email: serviceAccountKey.client_email,
|
||||
private_key: serviceAccountKey.private_key
|
||||
};
|
||||
|
||||
if (!backupConfig.projectId || !backupConfig.credentials || !backupConfig.credentials.client_email || !backupConfig.credentials.private_key) {
|
||||
throw 'fields_missing';
|
||||
}
|
||||
} catch (e) {
|
||||
$scope.configureBackup.error.generic = 'Cannot parse Google Service Account Key: ' + e.message;
|
||||
$scope.configureBackup.error.gcsKeyInput = true;
|
||||
$scope.configureBackup.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if ($scope.mountlike(backupConfig.provider)) {
|
||||
backupConfig.prefix = $scope.configureBackup.prefix;
|
||||
backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks;
|
||||
backupConfig.mountOptions = {};
|
||||
|
||||
if (backupConfig.provider === 'cifs' || backupConfig.provider === 'sshfs' || backupConfig.provider === 'nfs') {
|
||||
backupConfig.mountOptions.host = $scope.configureBackup.mountOptions.host;
|
||||
backupConfig.mountOptions.remoteDir = $scope.configureBackup.mountOptions.remoteDir;
|
||||
|
||||
if (backupConfig.provider === 'cifs') {
|
||||
backupConfig.mountOptions.username = $scope.configureBackup.mountOptions.username;
|
||||
backupConfig.mountOptions.password = $scope.configureBackup.mountOptions.password;
|
||||
backupConfig.mountOptions.seal = $scope.configureBackup.mountOptions.seal;
|
||||
} else if (backupConfig.provider === 'sshfs') {
|
||||
backupConfig.mountOptions.user = $scope.configureBackup.mountOptions.user;
|
||||
backupConfig.mountOptions.port = $scope.configureBackup.mountOptions.port;
|
||||
backupConfig.mountOptions.privateKey = $scope.configureBackup.mountOptions.privateKey;
|
||||
}
|
||||
} else if (backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs' || backupConfig.provider === 'disk') {
|
||||
backupConfig.mountOptions.diskPath = $scope.configureBackup.mountOptions.diskPath;
|
||||
} else if (backupConfig.provider === 'mountpoint') {
|
||||
backupConfig.mountPoint = $scope.configureBackup.mountPoint;
|
||||
backupConfig.chown = $scope.configureBackup.chown;
|
||||
backupConfig.preserveAttributes = true;
|
||||
}
|
||||
} else if (backupConfig.provider === 'filesystem') {
|
||||
backupConfig.backupFolder = $scope.configureBackup.backupFolder;
|
||||
backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks;
|
||||
}
|
||||
|
||||
backupConfig.limits.uploadPartSize = $scope.configureBackup.uploadPartSize;
|
||||
|
||||
if (backupConfig.format === 'rsync') {
|
||||
backupConfig.limits.downloadConcurrency = $scope.configureBackup.downloadConcurrency;
|
||||
backupConfig.limits.syncConcurrency = $scope.configureBackup.syncConcurrency;
|
||||
backupConfig.limits.copyConcurrency = $scope.configureBackup.copyConcurrency;
|
||||
}
|
||||
|
||||
Client.setBackupConfig(backupConfig, function (error) {
|
||||
$scope.configureBackup.busy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 424) {
|
||||
$scope.configureBackup.error.generic = error.message;
|
||||
|
||||
if (error.message.indexOf('AWS Access Key Id') !== -1) {
|
||||
$scope.configureBackup.error.accessKeyId = true;
|
||||
$scope.configureBackup.accessKeyId = '';
|
||||
$scope.configureBackupForm.accessKeyId.$setPristine();
|
||||
$('#inputConfigureBackupAccessKeyId').focus();
|
||||
} else if (error.message.indexOf('not match the signature') !== -1 ) {
|
||||
$scope.configureBackup.error.secretAccessKey = true;
|
||||
$scope.configureBackup.secretAccessKey = '';
|
||||
$scope.configureBackupForm.secretAccessKey.$setPristine();
|
||||
$('#inputConfigureBackupSecretAccessKey').focus();
|
||||
} else if (error.message.toLowerCase() === 'access denied') {
|
||||
$scope.configureBackup.error.bucket = true;
|
||||
$scope.configureBackup.bucket = '';
|
||||
$scope.configureBackupForm.bucket.$setPristine();
|
||||
$('#inputConfigureBackupBucket').focus();
|
||||
} else if (error.message.indexOf('ECONNREFUSED') !== -1) {
|
||||
$scope.configureBackup.error.generic = 'Unknown region';
|
||||
$scope.configureBackup.error.region = true;
|
||||
$scope.configureBackupForm.region.$setPristine();
|
||||
$('#inputConfigureBackupDORegion').focus();
|
||||
} else if (error.message.toLowerCase() === 'wrong region') {
|
||||
$scope.configureBackup.error.generic = 'Wrong S3 Region';
|
||||
$scope.configureBackup.error.region = true;
|
||||
$scope.configureBackupForm.region.$setPristine();
|
||||
$('#inputConfigureBackupS3Region').focus();
|
||||
} else {
|
||||
$('#inputConfigureBackupBucket').focus();
|
||||
}
|
||||
} else if (error.statusCode === 400) {
|
||||
$scope.configureBackup.error.generic = error.message;
|
||||
|
||||
if (error.message.indexOf('password') !== -1) {
|
||||
$scope.configureBackup.error.password = true;
|
||||
$scope.configureBackupForm.password.$setPristine();
|
||||
} else if ($scope.configureBackup.provider === 'filesystem') {
|
||||
$scope.configureBackup.error.backupFolder = true;
|
||||
}
|
||||
} else {
|
||||
console.error('Unable to change provider.', error);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// $scope.configureBackup.reset();
|
||||
$('#configureBackupModal').modal('hide');
|
||||
|
||||
getBackupConfig();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function fetchBackups() {
|
||||
Client.getBackups(function (error, backups) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.backups = backups;
|
||||
|
||||
// add contents property
|
||||
var appsById = {}, appsByFqdn = {};
|
||||
Client.getInstalledApps().forEach(function (app) {
|
||||
appsById[app.id] = app;
|
||||
appsByFqdn[app.fqdn] = app;
|
||||
});
|
||||
|
||||
$scope.backups.forEach(function (backup) {
|
||||
backup.contents = [];
|
||||
backup.dependsOn.forEach(function (appBackupId) {
|
||||
let match = appBackupId.match(/app_(.*?)_.*/); // *? means non-greedy
|
||||
if (!match) return;
|
||||
if (match[1].indexOf('.') !== -1) { // newer backups have fqdn in them
|
||||
if (appsByFqdn[match[1]]) backup.contents.push(appsByFqdn[match[1]]);
|
||||
} else {
|
||||
if (appsById[match[1]]) backup.contents.push(appsById[match[1]]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getBackupConfig() {
|
||||
Client.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.backupConfig = backupConfig;
|
||||
$scope.mountStatus = null;
|
||||
|
||||
if (!$scope.mountlike($scope.backupConfig.provider)) return;
|
||||
|
||||
Client.getBackupMountStatus(function (error, mountStatus) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.mountStatus = mountStatus;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getBackupTasks() {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_BACKUP, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!tasks.length) return;
|
||||
|
||||
$scope.backupTasks = tasks.slice(0, 10);
|
||||
});
|
||||
}
|
||||
|
||||
function getCleanupTasks() {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_CLEAN_BACKUPS, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!tasks.length) return;
|
||||
|
||||
$scope.cleanupTasks = tasks.slice(0, 10);
|
||||
});
|
||||
}
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.memory(function (error, memory) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.memory = memory;
|
||||
|
||||
fetchBackups();
|
||||
getBackupConfig();
|
||||
|
||||
$scope.manualBackupApps = Client.getInstalledApps().filter(function (app) { return app.type !== APP_TYPES.LINK && !app.enableBackup; });
|
||||
|
||||
// show backup status
|
||||
$scope.createBackup.init();
|
||||
$scope.cleanupBackups.init();
|
||||
$scope.backupPolicy.init();
|
||||
|
||||
getBackupTasks();
|
||||
getCleanupTasks();
|
||||
});
|
||||
});
|
||||
|
||||
function readFileLocally(obj, file, fileName) {
|
||||
return function (event) {
|
||||
$scope.$apply(function () {
|
||||
obj[file] = null;
|
||||
obj[fileName] = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
obj[file] = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('gcsKeyFileInput').onchange = readFileLocally($scope.configureBackup.gcsKey, 'content', 'keyFileName');
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['configureBackupModal', 'editBackupModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,90 +0,0 @@
|
||||
<!-- Modal change avatar -->
|
||||
<div class="modal fade" id="avatarChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'branding.changeLogo.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body branding-avatar-selector">
|
||||
<img id="previewAvatar" width="128" height="128" ng-src="{{ avatarChange.avatarUrl() }}"/>
|
||||
<input type="file" id="avatarFileInput" style="display: none" accept="image/*"/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="grid">
|
||||
<div class="item" ng-repeat="avatar in avatarChange.availableAvatars" style="background-image: url('{{avatar.data || avatar.url}}');" ng-click="avatarChange.setPreviewAvatar(avatar)"></div>
|
||||
<div class="item add" ng-click="avatarChange.showCustomAvatarSelector()"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="avatarChange.setAvatar()"> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>{{ 'branding.title' | tr }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form role="form" name="aboutForm" ng-submit="about.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': about.error.cloudronName }">
|
||||
<label class="control-label">{{ 'branding.cloudronName' | tr }}</label>
|
||||
<div class="control-label" ng-show="about.error.cloudronName">{{about.error.cloudronName}}</div>
|
||||
<input type="text" class="form-control" id="inputCloudronName" name="name" ng-model="about.cloudronName" ng-minlength="1" maxlength="64" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label class="control-label">{{ 'branding.logo' | tr }}</label>
|
||||
</div>
|
||||
<div class="branding-avatar" ng-click="avatarChange.showChangeAvatar()">
|
||||
<img ng-src="{{ about.avatarUrl() }}"/>
|
||||
<div class="overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="(!about.avatar && !aboutForm.$dirty) || aboutForm.$invalid || about.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="about.submit()" ng-disabled="(!about.avatar && !aboutForm.$dirty) || aboutForm.$invalid || about.busy"><i class="fa fa-circle-notch fa-spin" ng-show="about.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'branding.footer.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form role="form" name="footerForm" autocomplete="off">
|
||||
<p>{{ 'branding.footer.description' | tr }} <sup><a ng-href="https://docs.cloudron.io/branding/#footer" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<textarea name="footer" class="form-control" ng-model="footer.content" ng-disabled="footer.busy"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="footer.submit()" ng-disabled="!footerForm.$dirty || footerForm.$invalid || footer.busy"><i class="fa fa-circle-notch fa-spin" ng-show="footer.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,229 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
|
||||
angular.module('Application').controller('BrandingController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (Client.getUserInfo().role !== 'owner') $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.$parent.subscription);
|
||||
};
|
||||
|
||||
$scope.avatarChange = {
|
||||
avatar: null, // { file, data, url }
|
||||
|
||||
availableAvatars: [{
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo.png',
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-green.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-orange.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-darkblue.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-red.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-yellow.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-black.png'
|
||||
}],
|
||||
|
||||
avatarUrl: function () {
|
||||
if ($scope.avatarChange.avatar) {
|
||||
return $scope.avatarChange.avatar.data || $scope.avatarChange.avatar.url;
|
||||
} else {
|
||||
return Client.avatar;
|
||||
}
|
||||
},
|
||||
|
||||
getBlobFromImg: function (img, callback) {
|
||||
var size = 512;
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
|
||||
var imageDimensionRatio = img.width / img.height;
|
||||
var canvasDimensionRatio = canvas.width / canvas.height;
|
||||
var renderableHeight, renderableWidth, xStart, yStart;
|
||||
|
||||
if (imageDimensionRatio > canvasDimensionRatio) {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = img.width * (renderableHeight / img.height);
|
||||
xStart = (canvas.width - renderableWidth) / 2;
|
||||
yStart = 0;
|
||||
} else if (imageDimensionRatio < canvasDimensionRatio) {
|
||||
renderableWidth = canvas.width;
|
||||
renderableHeight = img.height * (renderableWidth / img.width);
|
||||
xStart = 0;
|
||||
yStart = (canvas.height - renderableHeight) / 2;
|
||||
} else {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = canvas.width;
|
||||
xStart = 0;
|
||||
yStart = 0;
|
||||
}
|
||||
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, xStart, yStart, renderableWidth, renderableHeight);
|
||||
|
||||
canvas.toBlob(callback);
|
||||
},
|
||||
|
||||
setPreviewAvatar: function (avatar) {
|
||||
$scope.avatarChange.avatar = avatar;
|
||||
},
|
||||
|
||||
showChangeAvatar: function () {
|
||||
$scope.avatarChange.avatar = $scope.about.avatar;
|
||||
$('#avatarChangeModal').modal('show');
|
||||
},
|
||||
|
||||
showCustomAvatarSelector: function () {
|
||||
$('#avatarFileInput').click();
|
||||
},
|
||||
|
||||
setAvatar: function () {
|
||||
if (angular.equals($scope.about.avatar, $scope.avatarChange.avatar)) return $('#avatarChangeModal').modal('hide'); // nothing changed
|
||||
|
||||
$scope.about.avatar = $scope.avatarChange.avatar;
|
||||
|
||||
// get the blob now, we cannot get it if dialog is hidden
|
||||
var img = document.getElementById('previewAvatar');
|
||||
$scope.avatarChange.getBlobFromImg(img, function (blob) {
|
||||
$scope.about.avatarBlob = blob;
|
||||
|
||||
$('#avatarChangeModal').modal('hide');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
$('#avatarFileInput').get(0).onchange = function (event) {
|
||||
var fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
$scope.$apply(function () {
|
||||
var tmp = {
|
||||
file: event.target.files[0],
|
||||
data: fr.result,
|
||||
url: null
|
||||
};
|
||||
|
||||
$scope.avatarChange.availableAvatars.push(tmp);
|
||||
$scope.avatarChange.setPreviewAvatar(tmp);
|
||||
});
|
||||
};
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
};
|
||||
|
||||
$scope.about = {
|
||||
busy: false,
|
||||
error: {},
|
||||
cloudronName: '',
|
||||
avatar: null,
|
||||
avatarBlob: null,
|
||||
|
||||
avatarUrl: function () {
|
||||
if ($scope.about.avatar) {
|
||||
return $scope.about.avatar.data || $scope.about.avatar.url;
|
||||
} else {
|
||||
return Client.avatar;
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
$scope.about.cloudronName = $scope.config.cloudronName;
|
||||
$scope.about.avatar = null;
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.about.error.name = null;
|
||||
$scope.about.busy = true;
|
||||
|
||||
var NOOP = function (next) { return next(); };
|
||||
var changeCloudronName = $scope.about.cloudronName !== $scope.config.cloudronName ? Client.changeCloudronName.bind(null, $scope.about.cloudronName) : NOOP;
|
||||
|
||||
changeCloudronName(function (error) {
|
||||
if (error) {
|
||||
$scope.about.busy = false;
|
||||
if (error.statusCode === 400) {
|
||||
$scope.about.error.cloudronName = error.message || 'Invalid name';
|
||||
$('#inputCloudronName').focus();
|
||||
} else {
|
||||
console.error('Unable to change name.', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var changeAvatar = $scope.about.avatar ? Client.changeCloudronAvatar.bind(null, $scope.about.avatarBlob) : NOOP;
|
||||
|
||||
changeAvatar(function (error) {
|
||||
if (error) {
|
||||
$scope.about.busy = false;
|
||||
console.error('Unable to change avatar.', error);
|
||||
return;
|
||||
}
|
||||
|
||||
Client.refreshConfig(function () {
|
||||
if ($scope.about.avatar) Client.resetAvatar();
|
||||
|
||||
$scope.aboutForm.$setPristine();
|
||||
$scope.about.avatar = null;
|
||||
$scope.about.busy = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.footer = {
|
||||
content: '',
|
||||
busy: false,
|
||||
|
||||
refresh: function () {
|
||||
Client.getFooter(function (error, result) {
|
||||
if (error) return console.error('Failed to get footer.', error);
|
||||
|
||||
$scope.footer.content = result;
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.footer.busy = true;
|
||||
|
||||
Client.setFooter($scope.footer.content.trim(), function (error) {
|
||||
if (error) return console.error('Failed to set footer.', error);
|
||||
|
||||
Client.refreshConfig(function () {
|
||||
$scope.footer.busy = false;
|
||||
$scope.footerForm.$setPristine();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.about.refresh();
|
||||
$scope.footer.refresh();
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,493 +0,0 @@
|
||||
<!-- Modal subscription -->
|
||||
<div class="modal fade" id="subscriptionRequiredModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'domains.subscriptionRequired.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'domains.subscriptionRequired.description' | tr"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="openSubscriptionSetup()">{{ 'domains.subscriptionRequired.setupAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal domain add/configure -->
|
||||
<div class="modal fade" id="domainConfigureModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" ng-show="domainConfigure.adding">{{ 'domains.domainDialog.addTitle' | tr }}</h4>
|
||||
<h4 class="modal-title" ng-hide="domainConfigure.adding">{{ 'domains.domainDialog.editTitle' | tr:{ domain: domainConfigure.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-show="domainConfigure.adding" ng-bind-html="'domains.domainDialog.addDescription' | tr"></p>
|
||||
|
||||
<form name="domainConfigureForm" role="form" novalidate ng-submit="domainConfigure.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="domainConfigure.error">{{ domainConfigure.error }}</p>
|
||||
|
||||
<div class="form-group" ng-show="domainConfigure.adding">
|
||||
<label class="control-label">{{ 'domains.domainDialog.domain' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.newDomain" name="newDomain" ng-disabled="domainConfigure.busy" placeholder="example.com" ng-required="domainConfigure.adding" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/domains/#dns-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="domainConfigure.provider" ng-options="a.value as a.name for a in dnsProvider" ng-change="domainConfigure.setDefaultTlsProvider()"></select>
|
||||
</div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.route53AccessKeyId' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.accessKeyId" name="accessKeyId" ng-disabled="domainConfigure.busy" ng-minlength="16" ng-maxlength="32" ng-required="domainConfigure.provider === 'route53'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.route53SecretAccessKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.secretAccessKey" name="secretAccessKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'route53'">
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud DNS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'gcdns'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.gcdnsServiceAccountKey' | tr }}</label>
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcdnsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="domainConfigure.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'gcdns'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcdnsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'digitalocean'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.digitalOceanToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.digitalOceanToken" name="digitalOceanToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'digitalocean'">
|
||||
</div>
|
||||
|
||||
<!-- Gandi -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'gandi'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.gandiApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.gandiApiKey" name="gandiApiKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'gandi'">
|
||||
</div>
|
||||
|
||||
<!-- GoDaddy -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'godaddy'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.goDaddyApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.godaddyApiKey" name="apiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'godaddy'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'godaddy'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.goDaddyApiSecret' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.godaddyApiSecret" name="apiSecret" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'godaddy'">
|
||||
</div>
|
||||
|
||||
<!-- Netcup -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'netcup'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.netcupCustomerNumber' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.netcupCustomerNumber" name="netcupCustomerNumber" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'netcup'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'netcup'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.netcupApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.netcupApiKey" name="netcupApiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'netcup'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'netcup'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.netcupApiPassword' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.netcupApiPassword" name="netcupApiPassword" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'netcup'">
|
||||
</div>
|
||||
|
||||
<!-- Porkbun -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'porkbun'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.porkbunApikey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.porkbunApikey" name="porkbunApikey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'porkbun'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'porkbun'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.porkbunSecretapikey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.porkbunSecretapikey" name="porkbunSecretapikey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'porkbun'">
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.cloudflareTokenType' | tr }}</label>
|
||||
<select class="form-control" ng-model="domainConfigure.cloudflareTokenType">
|
||||
<option value="GlobalApiKey">{{ 'domains.domainDialog.cloudflareTokenTypeGlobalApiKey' | tr }}</option>
|
||||
<option value="ApiToken">{{ 'domains.domainDialog.cloudflareTokenTypeApiToken' | tr }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label" ng-show="domainConfigure.cloudflareTokenType === 'GlobalApiKey'">{{ 'domains.domainDialog.cloudflareTokenTypeGlobalApiKey' | tr }}</label>
|
||||
<label class="control-label" ng-show="domainConfigure.cloudflareTokenType === 'ApiToken'">{{ 'domains.domainDialog.cloudflareTokenTypeApiToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.cloudflareToken" name="cloudflareToken" ng-required="domainConfigure.provider === 'cloudflare'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare' && domainConfigure.cloudflareTokenType === 'GlobalApiKey'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.cloudflareEmail' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="domainConfigure.cloudflareEmail" name="cloudflareEmail" ng-required="domainConfigure.provider === 'cloudflare' && domainConfigure.cloudflareTokenType === 'GlobalApiKey'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="domainConfigure.cloudflareDefaultProxyStatus"> {{ 'domains.domainDialog.cloudflareDefaultProxyStatus' | tr }}
|
||||
<sup><a ng-href="https://docs.cloudron.io/domains/#cloudflare-dns" class="help" target="_blank" tabIndex="-1"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Linode -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'linode'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.linodeToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.linodeToken" name="linodeToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'linode'">
|
||||
</div>
|
||||
|
||||
<!-- Bunny -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'bunny'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.bunnyAccessKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.bunnyAccessKey" name="bunnyAccessKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'bunny'">
|
||||
</div>
|
||||
|
||||
<!-- Hetzner -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'hetzner'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.hetznerToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.hetznerToken" name="hetznerToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'hetzner'">
|
||||
</div>
|
||||
|
||||
<!-- Vultr -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'vultr'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.vultrToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.vultrToken" name="vultrToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'vultr'">
|
||||
</div>
|
||||
|
||||
<!-- Name.com -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecom'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.nameComUsername' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.nameComUsername" name="nameComUsername" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'namecom'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecom'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.nameComApiToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.nameComToken" name="nameComToken" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'namecom'">
|
||||
</div>
|
||||
|
||||
<!-- Namecheap -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecheap'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.namecheapUsername' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.namecheapUsername" name="namecheapUsername" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'namecheap'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecheap'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.namecheapApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.namecheapApiKey" name="namecheapApiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'namecheap'">
|
||||
</div>
|
||||
|
||||
<p class="small text-info text-bold" ng-show="domainConfigure.provider === 'namecheap'" ng-bind-html="'domains.domainDialog.namecheapInfo' | tr"></p>
|
||||
<p class="small text-info text-bold" ng-show="domainConfigure.provider === 'wildcard'" ng-bind-html="'domains.domainDialog.wildcardInfo' | tr:{ domain: domainConfigure.adding ? domainConfigure.newDomain : domainConfigure.domain.domain }"></p>
|
||||
<p class="small text-info text-bold" ng-show="domainConfigure.provider === 'manual'" ng-bind-html="'domains.domainDialog.manualInfo' | tr"></p>
|
||||
<p class="small text-info text-bold" ng-show="needsPort80(domainConfigure.provider, domainConfigure.tlsConfig.provider)" ng-bind-html="'domains.domainDialog.letsEncryptInfo' | tr"></p>
|
||||
|
||||
<a href="" ng-click="domainConfigure.advancedVisible = true" ng-hide="domainConfigure.advancedVisible">{{ 'domains.domainDialog.advancedAction' | tr }}</a>
|
||||
<div uib-collapse="!domainConfigure.advancedVisible">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.zoneName' | tr }} <sup><a ng-href="https://docs.cloudron.io/domains/#zone-name" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.zoneName" name="zoneName" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.certProvider' | tr }} <sup><a ng-href="https://docs.cloudron.io/certificates/#certificate-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="domainConfigure.tlsConfig.provider" ng-options="a.value as a.name for a in tlsProvider"></select>
|
||||
</div>
|
||||
|
||||
<!-- Fallback certificate -->
|
||||
<div ng-show="domainConfigure.tlsConfig.provider !== 'fallback'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.fallbackCert' | tr }}</label>
|
||||
<p ng-bind-html="'domains.domainDialog.fallbackCertInfo' | tr"></p>
|
||||
</div>
|
||||
|
||||
<div ng-show="domainConfigure.tlsConfig.provider === 'fallback'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.fallbackCertCustomCert' | tr }}</label>
|
||||
<p ng-bind-html="'domains.domainDialog.fallbackCertCustomCertInfo' | tr:{ customCertLink: 'https://docs.cloudron.io/certificates/#custom-certificates' }"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.cert.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="fallbackCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="{{ 'domains.domainDialog.fallbackCertCertificatePlaceholder' | tr }}" ng-model="domainConfigure.fallbackCert.certificateFileName" name="cert" onclick="getElementById('fallbackCertFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon"><i class="fa fa-upload" onclick="getElementById('fallbackCertFileInput').click();"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.key.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="fallbackKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="{{ 'domains.domainDialog.fallbackCertKeyPlaceholder' | tr }}" ng-model="domainConfigure.fallbackCert.keyFileName" id="fallbackKeyInput" name="key" onclick="getElementById('fallbackKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon"><i class="fa fa-upload" onclick="getElementById('fallbackKeyFileInput').click();"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- advanced -->
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="domainConfigure.submit()" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainConfigure.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal domain wellknown -->
|
||||
<div class="modal fade" id="domainWellKnownModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'domains.domainWellKnown.title' | tr:{ domain: domainWellKnown.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'domains.domainDialog.wellKnownDescription' | tr:{ domain: domainWellKnown.domain.domain, docsLink: 'https://docs.cloudron.io/domains/#well-known-locations' }"></p>
|
||||
|
||||
<form name="domainWellKnownForm" role="form" novalidate ng-submit="domainWellKnown.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="domainWellKnown.error">{{ domainWellKnown.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.matrixHostname' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainWellKnown.matrixHostname" name="matrixHostname" ng-disabled="domainWellKnown.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.mastodonHostname' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainWellKnown.mastodonHostname" name="mastodonHostname" ng-disabled="domainWellKnown.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.jitsiHostname' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainWellKnown.jitsiHostname" name="jitsiHostname" ng-disabled="domainWellKnown.busy">
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="domainWellKnownForm.$invalid || domainWellKnown.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="domainWellKnown.submit()" ng-disabled="domainWellKnownForm.$invalid || domainWellKnown.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainWellKnown.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal domain remove -->
|
||||
<div class="modal fade" id="domainRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'domains.removeDialog.title' | tr:{ domain: domainRemove.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'domains.removeDialog.description' | tr:{ domain: domainRemove.domain.domain }"></p>
|
||||
<br/>
|
||||
<span class="has-error" ng-show="domainRemove.error">{{ domainRemove.error }}</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="domainRemove.submit()" ng-disabled="domainRemove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainRemove.busy"></i> {{ 'domains.removeDialog.removeAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>
|
||||
{{ 'domains.title' | tr }}
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-show="domains.length <= pageSize" ng-click="domainAdd.show()"><i class="fa fa-plus"></i> {{ 'domains.addDomain' | tr }}</button>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="users-toolbar" ng-show="domains.length > pageSize">
|
||||
<input type="text" id="domainSearchInput" class="form-control" style="max-width: 350px;" ng-model="domainSearchString" placeholder="{{ 'main.searchPlaceholder' | tr }}"/>
|
||||
<div style="flex-grow: 1;"></div>
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-click="domainAdd.show()"><i class="fa fa-plus"></i> {{ 'domains.addDomain' | tr }}</button>
|
||||
</div>
|
||||
<div class="card card-large">
|
||||
<div class="row ng-hide" ng-show="!ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'domains.domain' | tr }}</th>
|
||||
<th class="text-left hidden-xs hidden-sm">{{ 'domains.provider' | tr }}</th>
|
||||
<th style="width: 100px" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="domain in domains | filter:domainSearchString | limitTo:pageSize:((currentPage-1)*pageSize)">
|
||||
<td class="elide-table-cell hand" ng-click="domainConfigure.show(domain)">
|
||||
{{ domain.domain }}
|
||||
</td>
|
||||
<td class="text-left elide-table-cell hidden-xs hidden-sm hand" ng-click="domainConfigure.show(domain)">
|
||||
{{ prettyProviderName(domain) }}
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="domainWellKnown.show(domain)" uib-tooltip="{{ 'domains.tooltipWellKnown' | tr }}"><i class="fa fa-atlas"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" uib-tooltip="{{ 'domains.tooltipEdit' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="domainRemove.show(domain)" uib-tooltip="{{ 'domains.tooltipRemove' | tr }}" ng-disabled="domain.domain === config.adminDomain"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pull-left">
|
||||
{{ 'domains.count' | tr:{ count: (domains | filter:domainSearchString).length } }}
|
||||
</div>
|
||||
<div class="pull-right" ng-show="domains.length > pageSize">
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="showPrevPage()" ng-class="{ 'btn-primary': currentPage > 1 }" ng-disabled="currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<span style="margin: 0 5px; line-height: 1.5; font-size: 12px;">{{ currentPage }}</span>
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="showNextPage()" ng-class="{ 'btn-primary': (domains | filter:domainSearchString).length > (currentPage * pageSize) }" ng-disabled="(domains | filter:domainSearchString).length <= (currentPage * pageSize)">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'domains.renewCerts.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="renewCerts.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in renewCerts.tasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-bind-html="'domains.renewCerts.description' | tr"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="renewCerts.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ renewCerts.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="renewCerts.busy">{{ renewCerts.message }}</p>
|
||||
<p ng-hide="renewCerts.busy">
|
||||
<div class="has-error" ng-show="!renewCerts.active">{{ renewCerts.errorMessage }}</div>
|
||||
</p>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="renewCerts.renew()" ng-disabled="renewCerts.busy">{{ 'domains.renewCerts.renewAllAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'domains.syncDns.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="syncDns.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in syncDns.tasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-bind-html="'domains.syncDns.description' | tr"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="syncDns.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ syncDns.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="syncDns.busy">{{ syncDns.message }}</p>
|
||||
<p ng-hide="syncDns.busy">
|
||||
<div class="has-error" ng-show="!syncDns.active">{{ syncDns.errorMessage }}</div>
|
||||
</p>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="syncDns.sync()" ng-disabled="syncDns.busy">{{ 'domains.syncDns.syncAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'domains.changeDashboardDomain.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="changeDashboard.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in changeDashboard.tasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<p ng-bind-html="'domains.changeDashboardDomain.description' | tr"></p>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon" style="background-color: white;">my.</span>
|
||||
<select class="form-control pull-right" style="display: inline-block;" ng-model="changeDashboard.selectedDomain" ng-options="a.domain for a in domains"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="changeDashboard.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ changeDashboard.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<p ng-show="changeDashboard.busy">{{ changeDashboard.message }}</p>
|
||||
<p ng-hide="changeDashboard.busy">
|
||||
<div class="has-error" ng-show="!changeDashboard.active">{{ changeDashboard.errorMessage }}</div>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-right">
|
||||
<button class="btn btn-outline btn-primary" ng-click="changeDashboard.change()" ng-hide="changeDashboard.busy" ng-disabled="changeDashboard.selectedDomain.domain === changeDashboard.adminDomain.domain">{{ 'domains.changeDashboardDomain.changeAction' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="changeDashboard.stop()" ng-show="changeDashboard.busy" style="margin-right: 10px">{{ 'domains.changeDashboardDomain.cancelAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,748 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global async */
|
||||
/* global angular */
|
||||
/* global $, TASK_TYPES */
|
||||
|
||||
angular.module('Application').controller('DomainsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.domains = [];
|
||||
$scope.ready = false;
|
||||
$scope.domainSearchString = '';
|
||||
$scope.pageSize = localStorage.cloudronPageSize || 10;
|
||||
$scope.currentPage = 1;
|
||||
|
||||
$scope.showNextPage = function () {
|
||||
$scope.currentPage++;
|
||||
};
|
||||
|
||||
$scope.showPrevPage = function () {
|
||||
if ($scope.currentPage > 1) $scope.currentPage--;
|
||||
else $scope.currentPage = 1;
|
||||
};
|
||||
|
||||
$scope.translationLinks = {
|
||||
linodeDocsLink: 'https://docs.cloudron.io/domains/#linode-dns',
|
||||
customCertLink: 'https://docs.cloudron.io/certificates/#custom-certificates'
|
||||
};
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.$parent.subscription);
|
||||
};
|
||||
|
||||
// currently, validation of wildcard with various provider is done server side
|
||||
$scope.tlsProvider = [
|
||||
{ name: 'Let\'s Encrypt Prod', value: 'letsencrypt-prod' },
|
||||
{ name: 'Let\'s Encrypt Prod - Wildcard', value: 'letsencrypt-prod-wildcard' },
|
||||
{ name: 'Let\'s Encrypt Staging', value: 'letsencrypt-staging' },
|
||||
{ name: 'Let\'s Encrypt Staging - Wildcard', value: 'letsencrypt-staging-wildcard' },
|
||||
{ name: 'Custom Wildcard Certificate', value: 'fallback' },
|
||||
];
|
||||
|
||||
// keep in sync with setupdns.js
|
||||
$scope.dnsProvider = [
|
||||
{ name: 'AWS Route53', value: 'route53' },
|
||||
{ name: 'Bunny', value: 'bunny' },
|
||||
{ name: 'Cloudflare', value: 'cloudflare' },
|
||||
{ name: 'DigitalOcean', value: 'digitalocean' },
|
||||
{ name: 'Gandi LiveDNS', value: 'gandi' },
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
{ name: 'Hetzner', value: 'hetzner' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Netcup', value: 'netcup' },
|
||||
{ name: 'Porkbun', value: 'porkbun' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
{ name: 'Manual (not recommended)', value: 'manual' },
|
||||
{ name: 'No-op (only for development)', value: 'noop' }
|
||||
];
|
||||
|
||||
$scope.prettyProviderName = function (domain) {
|
||||
switch (domain.provider) {
|
||||
case 'bunny': return 'Bunny';
|
||||
case 'route53': return 'AWS Route53';
|
||||
case 'cloudflare': return 'Cloudflare';
|
||||
case 'digitalocean': return 'DigitalOcean';
|
||||
case 'gandi': return 'Gandi LiveDNS';
|
||||
case 'hetzner': return 'Hetzner DNS';
|
||||
case 'linode': return 'Linode';
|
||||
case 'namecom': return 'Name.com';
|
||||
case 'namecheap': return 'Namecheap';
|
||||
case 'netcup': return 'Netcup';
|
||||
case 'gcdns': return 'Google Cloud';
|
||||
case 'godaddy': return 'GoDaddy';
|
||||
case 'vultr': return 'Vultr';
|
||||
case 'manual': return 'Manual';
|
||||
case 'porkbun': return 'Porkbun';
|
||||
case 'wildcard': return 'Wildcard';
|
||||
case 'noop': return 'No-op';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.needsPort80 = function (dnsProvider, tlsProvider) {
|
||||
return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') &&
|
||||
(tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging'));
|
||||
};
|
||||
|
||||
function readFileLocally(obj, file, fileName) {
|
||||
return function (event) {
|
||||
$scope.$apply(function () {
|
||||
obj[file] = null;
|
||||
obj[fileName] = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
obj[file] = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function refreshDomains(callback) {
|
||||
var domains = [ ];
|
||||
|
||||
Client.getDomains(function (error, results) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
async.eachSeries(results, function (result, iteratorDone) {
|
||||
Client.getDomain(result.domain, function (error, domain) {
|
||||
if (error) return iteratorDone(error);
|
||||
|
||||
domains.push(domain);
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function (error) {
|
||||
angular.copy(domains, $scope.domains);
|
||||
|
||||
$scope.changeDashboard.selectedDomain = $scope.changeDashboard.adminDomain = $scope.domains.find(function (d) { return d.domain === $scope.config.adminDomain; });
|
||||
|
||||
if (error) console.error(error);
|
||||
if (callback) callback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$scope.domainAdd = {
|
||||
show: function () {
|
||||
$scope.domainConfigure.show();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.domainWellKnown = {
|
||||
busy: false,
|
||||
error: null,
|
||||
|
||||
domain: null,
|
||||
mastodonHostname: '',
|
||||
matrixHostname: '',
|
||||
jitsiHostname: '',
|
||||
|
||||
reset: function () {
|
||||
$scope.domainWellKnown.busy = false;
|
||||
$scope.domainWellKnown.error = null;
|
||||
$scope.domainWellKnown.domain = null;
|
||||
|
||||
$scope.domainWellKnown.matrixHostname = '';
|
||||
$scope.domainWellKnown.mastodonHostname = '';
|
||||
$scope.domainWellKnown.jitsiHostname = '';
|
||||
},
|
||||
|
||||
show: function (domain) {
|
||||
$scope.domainWellKnown.reset();
|
||||
|
||||
$scope.domainWellKnown.domain = domain;
|
||||
|
||||
try {
|
||||
if (domain.wellKnown && domain.wellKnown['matrix/server']) {
|
||||
$scope.domainWellKnown.matrixHostname = JSON.parse(domain.wellKnown['matrix/server'])['m.server'];
|
||||
}
|
||||
if (domain.wellKnown && domain.wellKnown['host-meta']) {
|
||||
$scope.domainWellKnown.mastodonHostname = domain.wellKnown['host-meta'].match(new RegExp('template="https://(.*?)/'))[1];
|
||||
}
|
||||
if (domain.wellKnown && domain.wellKnown['matrix/client']) {
|
||||
let parsed = JSON.parse(domain.wellKnown['matrix/client']);
|
||||
if (parsed['im.vector.riot.jitsi'] && parsed['im.vector.riot.jitsi']['preferredDomain']) {
|
||||
$scope.domainWellKnown.jitsiHostname = parsed['im.vector.riot.jitsi']['preferredDomain'];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
$('#domainWellKnownModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.domainWellKnown.busy = true;
|
||||
$scope.domainWellKnown.error = null;
|
||||
|
||||
var wellKnown = {};
|
||||
if ($scope.domainWellKnown.matrixHostname) {
|
||||
wellKnown['matrix/server'] = JSON.stringify({ 'm.server': $scope.domainWellKnown.matrixHostname });
|
||||
// https://matrix.org/docs/spec/client_server/latest#get-well-known-matrix-client
|
||||
wellKnown['matrix/client'] = JSON.stringify({
|
||||
'm.homeserver': {
|
||||
'base_url': 'https://' + $scope.domainWellKnown.matrixHostname
|
||||
},
|
||||
'im.vector.riot.jitsi': {
|
||||
'preferredDomain': $scope.domainWellKnown.jitsiHostname
|
||||
}
|
||||
});
|
||||
} else if ($scope.domainWellKnown.jitsiHostname) { // only if matrixHostname is not set
|
||||
wellKnown['matrix/client'] = JSON.stringify({
|
||||
'im.vector.riot.jitsi': {
|
||||
'preferredDomain': $scope.domainWellKnown.jitsiHostname
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($scope.domainWellKnown.mastodonHostname) {
|
||||
wellKnown['host-meta'] = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
+ '<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">\n'
|
||||
+ '<Link rel="lrdd" type="application/xrd+xml" template="https://' + $scope.domainWellKnown.mastodonHostname + '/.well-known/webfinger?resource={uri}"/>\n'
|
||||
+ '</XRD>';
|
||||
}
|
||||
|
||||
Client.updateDomainWellKnown($scope.domainWellKnown.domain.domain, wellKnown, function (error) {
|
||||
$scope.domainWellKnown.busy = false;
|
||||
if (error) {
|
||||
$scope.domainWellKnown.error = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
$('#domainWellKnownModal').modal('hide');
|
||||
$scope.domainWellKnown.reset();
|
||||
refreshDomains();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// We reused configure also for adding domains to avoid much code duplication
|
||||
$scope.domainConfigure = {
|
||||
adding: false,
|
||||
error: null,
|
||||
busy: false,
|
||||
domain: null,
|
||||
advancedVisible: false,
|
||||
|
||||
// form model
|
||||
newDomain: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
gcdnsKey: { keyFileName: '', content: '' },
|
||||
digitalOceanToken: '',
|
||||
gandiApiKey: '',
|
||||
godaddyApiKey: '',
|
||||
godaddyApiSecret: '',
|
||||
cloudflareToken: '',
|
||||
cloudflareEmail: '',
|
||||
cloudflareDefaultProxyStatus: false,
|
||||
cloudflareTokenType: 'GlobalApiKey',
|
||||
linodeToken: '',
|
||||
bunnyAccessKey: '',
|
||||
hetznerToken: '',
|
||||
vultrToken: '',
|
||||
nameComToken: '',
|
||||
nameComUsername: '',
|
||||
namecheapUsername: '',
|
||||
namecheapApiKey: '',
|
||||
netcupCustomerNumber: '',
|
||||
netcupApiKey: '',
|
||||
netcupApiPassword: '',
|
||||
porkbunSecretapikey: '',
|
||||
porkbunApikey: '',
|
||||
|
||||
provider: 'route53',
|
||||
zoneName: '',
|
||||
|
||||
tlsConfig: {
|
||||
provider: 'letsencrypt-prod-wildcard'
|
||||
},
|
||||
|
||||
fallbackCert: {
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
},
|
||||
|
||||
setDefaultTlsProvider: function () {
|
||||
var dnsProvider = $scope.domainConfigure.provider;
|
||||
// wildcard LE won't work without automated DNS
|
||||
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') {
|
||||
$scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod';
|
||||
} else {
|
||||
$scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod-wildcard';
|
||||
}
|
||||
},
|
||||
|
||||
show: function (domain) {
|
||||
$scope.domainConfigure.reset();
|
||||
|
||||
if (domain) {
|
||||
$scope.domainConfigure.domain = domain;
|
||||
$scope.domainConfigure.accessKeyId = domain.config.accessKeyId;
|
||||
$scope.domainConfigure.secretAccessKey = domain.config.secretAccessKey;
|
||||
|
||||
$scope.domainConfigure.gcdnsKey.keyFileName = '';
|
||||
$scope.domainConfigure.gcdnsKey.content = '';
|
||||
if (domain.provider === 'gcdns') {
|
||||
$scope.domainConfigure.gcdnsKey.keyFileName = domain.config.credentials && domain.config.credentials.client_email;
|
||||
|
||||
$scope.domainConfigure.gcdnsKey.content = JSON.stringify({
|
||||
project_id: domain.config.projectId,
|
||||
client_email: domain.config.credentials.client_email,
|
||||
private_key: domain.config.credentials.private_key
|
||||
});
|
||||
}
|
||||
$scope.domainConfigure.digitalOceanToken = domain.provider === 'digitalocean' ? domain.config.token : '';
|
||||
$scope.domainConfigure.linodeToken = domain.provider === 'linode' ? domain.config.token : '';
|
||||
$scope.domainConfigure.bunnyAccessKey = domain.provider === 'bunny' ? domain.config.accessKey : '';
|
||||
$scope.domainConfigure.hetznerToken = domain.provider === 'hetzner' ? domain.config.token : '';
|
||||
$scope.domainConfigure.vultrToken = domain.provider === 'vultr' ? domain.config.token : '';
|
||||
$scope.domainConfigure.gandiApiKey = domain.provider === 'gandi' ? domain.config.token : '';
|
||||
$scope.domainConfigure.cloudflareToken = domain.provider === 'cloudflare' ? domain.config.token : '';
|
||||
$scope.domainConfigure.cloudflareEmail = domain.provider === 'cloudflare' ? domain.config.email : '';
|
||||
$scope.domainConfigure.cloudflareTokenType = domain.provider === 'cloudflare' ? domain.config.tokenType : 'GlobalApiKey';
|
||||
$scope.domainConfigure.cloudflareDefaultProxyStatus = domain.provider === 'cloudflare' ? !!domain.config.defaultProxyStatus : false;
|
||||
|
||||
$scope.domainConfigure.godaddyApiKey = domain.provider === 'godaddy' ? domain.config.apiKey : '';
|
||||
$scope.domainConfigure.godaddyApiSecret = domain.provider === 'godaddy' ? domain.config.apiSecret : '';
|
||||
|
||||
$scope.domainConfigure.nameComToken = domain.provider === 'namecom' ? domain.config.token : '';
|
||||
$scope.domainConfigure.nameComUsername = domain.provider === 'namecom' ? domain.config.username : '';
|
||||
|
||||
$scope.domainConfigure.namecheapApiKey = domain.provider === 'namecheap' ? domain.config.token : '';
|
||||
$scope.domainConfigure.namecheapUsername = domain.provider === 'namecheap' ? domain.config.username : '';
|
||||
|
||||
$scope.domainConfigure.netcupCustomerNumber = domain.provider === 'netcup' ? domain.config.customerNumber : '';
|
||||
$scope.domainConfigure.netcupApiKey = domain.provider === 'netcup' ? domain.config.apiKey : '';
|
||||
$scope.domainConfigure.netcupApiPassword = domain.provider === 'netcup' ? domain.config.apiPassword : '';
|
||||
|
||||
$scope.domainConfigure.porkbunApikey = domain.provider === 'porkbun' ? domain.config.apikey : '';
|
||||
$scope.domainConfigure.porkbunSecretapikey = domain.provider === 'porkbun' ? domain.config.secretapikey : '';
|
||||
|
||||
$scope.domainConfigure.provider = domain.provider;
|
||||
|
||||
$scope.domainConfigure.tlsConfig.provider = domain.tlsConfig.provider;
|
||||
if (domain.tlsConfig.provider.indexOf('letsencrypt') === 0) {
|
||||
if (domain.tlsConfig.wildcard) $scope.domainConfigure.tlsConfig.provider += '-wildcard';
|
||||
}
|
||||
$scope.domainConfigure.zoneName = domain.zoneName;
|
||||
} else {
|
||||
$scope.domainConfigure.adding = true;
|
||||
}
|
||||
|
||||
$('#domainConfigureModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.domainConfigure.busy = true;
|
||||
$scope.domainConfigure.error = null;
|
||||
|
||||
var provider = $scope.domainConfigure.provider;
|
||||
|
||||
var data = {};
|
||||
|
||||
if (provider === 'route53') {
|
||||
data.accessKeyId = $scope.domainConfigure.accessKeyId;
|
||||
data.secretAccessKey = $scope.domainConfigure.secretAccessKey;
|
||||
} else if (provider === 'gcdns') {
|
||||
try {
|
||||
var serviceAccountKey = JSON.parse($scope.domainConfigure.gcdnsKey.content);
|
||||
data.projectId = serviceAccountKey.project_id;
|
||||
data.credentials = {
|
||||
client_email: serviceAccountKey.client_email,
|
||||
private_key: serviceAccountKey.private_key
|
||||
};
|
||||
|
||||
if (!data.projectId || !data.credentials || !data.credentials.client_email || !data.credentials.private_key) {
|
||||
throw new Error('One or more fields are missing in the JSON');
|
||||
}
|
||||
} catch (e) {
|
||||
$scope.domainConfigure.error = 'Cannot parse Google Service Account Key: ' + e.message;
|
||||
$scope.domainConfigure.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if (provider === 'digitalocean') {
|
||||
data.token = $scope.domainConfigure.digitalOceanToken;
|
||||
} else if (provider === 'linode') {
|
||||
data.token = $scope.domainConfigure.linodeToken;
|
||||
} else if (provider === 'bunny') {
|
||||
data.accessKey = $scope.domainConfigure.bunnyAccessKey;
|
||||
} else if (provider === 'hetzner') {
|
||||
data.token = $scope.domainConfigure.hetznerToken;
|
||||
} else if (provider === 'vultr') {
|
||||
data.token = $scope.domainConfigure.vultrToken;
|
||||
} else if (provider === 'gandi') {
|
||||
data.token = $scope.domainConfigure.gandiApiKey;
|
||||
} else if (provider === 'godaddy') {
|
||||
data.apiKey = $scope.domainConfigure.godaddyApiKey;
|
||||
data.apiSecret = $scope.domainConfigure.godaddyApiSecret;
|
||||
} else if (provider === 'cloudflare') {
|
||||
data.token = $scope.domainConfigure.cloudflareToken;
|
||||
data.email = $scope.domainConfigure.cloudflareEmail;
|
||||
data.tokenType = $scope.domainConfigure.cloudflareTokenType;
|
||||
data.defaultProxyStatus = $scope.domainConfigure.cloudflareDefaultProxyStatus;
|
||||
} else if (provider === 'namecom') {
|
||||
data.token = $scope.domainConfigure.nameComToken;
|
||||
data.username = $scope.domainConfigure.nameComUsername;
|
||||
} else if (provider === 'namecheap') {
|
||||
data.token = $scope.domainConfigure.namecheapApiKey;
|
||||
data.username = $scope.domainConfigure.namecheapUsername;
|
||||
} else if (provider === 'netcup') {
|
||||
data.customerNumber = $scope.domainConfigure.netcupCustomerNumber;
|
||||
data.apiKey = $scope.domainConfigure.netcupApiKey;
|
||||
data.apiPassword = $scope.domainConfigure.netcupApiPassword;
|
||||
} else if (provider === 'porkbun') {
|
||||
data.apikey = $scope.domainConfigure.porkbunApikey;
|
||||
data.secretapikey = $scope.domainConfigure.porkbunSecretapikey;
|
||||
}
|
||||
|
||||
var fallbackCertificate = null;
|
||||
if ($scope.domainConfigure.fallbackCert.certificateFile && $scope.domainConfigure.fallbackCert.keyFile) {
|
||||
fallbackCertificate = {
|
||||
cert: $scope.domainConfigure.fallbackCert.certificateFile,
|
||||
key: $scope.domainConfigure.fallbackCert.keyFile
|
||||
};
|
||||
}
|
||||
|
||||
var tlsConfig = {
|
||||
provider: $scope.domainConfigure.tlsConfig.provider,
|
||||
wildcard: false
|
||||
};
|
||||
if ($scope.domainConfigure.tlsConfig.provider.indexOf('-wildcard') !== -1) {
|
||||
tlsConfig.provider = tlsConfig.provider.replace('-wildcard', '');
|
||||
tlsConfig.wildcard = true;
|
||||
}
|
||||
|
||||
// choose the right api, since we reuse this for adding and configuring domains
|
||||
var func;
|
||||
if ($scope.domainConfigure.adding) func = Client.addDomain.bind(Client, $scope.domainConfigure.newDomain, $scope.domainConfigure.zoneName, provider, data, fallbackCertificate, tlsConfig);
|
||||
else func = Client.updateDomainConfig.bind(Client, $scope.domainConfigure.domain.domain, $scope.domainConfigure.zoneName, provider, data, fallbackCertificate, tlsConfig);
|
||||
|
||||
func(function (error) {
|
||||
$scope.domainConfigure.busy = false;
|
||||
if (error) {
|
||||
$scope.domainConfigure.error = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
$('#domainConfigureModal').modal('hide');
|
||||
$scope.domainConfigure.reset();
|
||||
|
||||
refreshDomains();
|
||||
});
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.domainConfigure.adding = false;
|
||||
$scope.domainConfigure.advancedVisible = false;
|
||||
$scope.domainConfigure.newDomain = '';
|
||||
|
||||
$scope.domainConfigure.busy = false;
|
||||
$scope.domainConfigure.error = null;
|
||||
|
||||
$scope.domainConfigure.provider = '';
|
||||
$scope.domainConfigure.accessKeyId = '';
|
||||
$scope.domainConfigure.secretAccessKey = '';
|
||||
$scope.domainConfigure.gcdnsKey.keyFileName = '';
|
||||
$scope.domainConfigure.gcdnsKey.content = '';
|
||||
$scope.domainConfigure.digitalOceanToken = '';
|
||||
$scope.domainConfigure.gandiApiKey = '';
|
||||
$scope.domainConfigure.godaddyApiKey = '';
|
||||
$scope.domainConfigure.godaddyApiSecret = '';
|
||||
$scope.domainConfigure.cloudflareToken = '';
|
||||
$scope.domainConfigure.cloudflareEmail = '';
|
||||
$scope.domainConfigure.cloudflareTokenType = 'GlobalApiKey';
|
||||
$scope.domainConfigure.cloudflareDefaultProxyStatus = false;
|
||||
$scope.domainConfigure.nameComToken = '';
|
||||
$scope.domainConfigure.nameComUsername = '';
|
||||
$scope.domainConfigure.namecheapApiKey = '';
|
||||
$scope.domainConfigure.namecheapUsername = '';
|
||||
$scope.domainConfigure.netcupCustomerNumber = '';
|
||||
$scope.domainConfigure.netcupApiKey = '';
|
||||
$scope.domainConfigure.netcupApiPassword = '';
|
||||
$scope.domainConfigure.porkbunApikey = '';
|
||||
$scope.domainConfigure.porkbunSecretapikey = '';
|
||||
$scope.domainConfigure.vultrToken = '';
|
||||
|
||||
$scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod';
|
||||
$scope.domainConfigure.zoneName = '';
|
||||
|
||||
$scope.domainConfigureForm.$setPristine();
|
||||
$scope.domainConfigureForm.$setUntouched();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.renewCerts = {
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
tasks: [],
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_CHECK_CERTS, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
$scope.renewCerts.tasks = tasks.slice(0, 10);
|
||||
if ($scope.renewCerts.tasks.length && $scope.renewCerts.tasks[0].active) $scope.renewCerts.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
var taskId = $scope.renewCerts.tasks[0].id;
|
||||
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.renewCerts.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.renewCerts.busy = false;
|
||||
$scope.renewCerts.message = '';
|
||||
$scope.renewCerts.percent = 100; // indicates that 'result' is valid
|
||||
$scope.renewCerts.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
$scope.renewCerts.refreshTasks(); // update the tasks list dropdown
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.renewCerts.busy = true;
|
||||
$scope.renewCerts.percent = data.percent;
|
||||
$scope.renewCerts.message = data.message;
|
||||
window.setTimeout($scope.renewCerts.updateStatus, 500);
|
||||
});
|
||||
},
|
||||
|
||||
renew: function () {
|
||||
$scope.renewCerts.busy = true;
|
||||
$scope.renewCerts.percent = 0;
|
||||
$scope.renewCerts.message = '';
|
||||
$scope.renewCerts.errorMessage = '';
|
||||
|
||||
// always rebuild the nginx configs when triggered via the UI. we assume user is clicking this because something is wrong
|
||||
Client.renewCerts({ rebuild: true }, function (error /*, taskId */) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.renewCerts.errorMessage = error.message;
|
||||
$scope.renewCerts.busy = false;
|
||||
} else {
|
||||
$scope.renewCerts.refreshTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.syncDns = {
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
tasks: [],
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_SYNC_DNS_RECORDS, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
$scope.syncDns.tasks = tasks.slice(0, 10);
|
||||
if ($scope.syncDns.tasks.length && $scope.syncDns.tasks[0].active) $scope.syncDns.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
var taskId = $scope.syncDns.tasks[0].id;
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.syncDns.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.syncDns.busy = false;
|
||||
$scope.syncDns.message = '';
|
||||
$scope.syncDns.percent = 100; // indicates that 'result' is valid
|
||||
$scope.syncDns.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
$scope.syncDns.refreshTasks(); // update the tasks list dropdown
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.syncDns.busy = true;
|
||||
$scope.syncDns.percent = data.percent;
|
||||
$scope.syncDns.message = data.message;
|
||||
window.setTimeout($scope.syncDns.updateStatus, 500);
|
||||
});
|
||||
},
|
||||
|
||||
sync: function () {
|
||||
$scope.syncDns.busy = true;
|
||||
$scope.syncDns.percent = 0;
|
||||
$scope.syncDns.message = '';
|
||||
$scope.syncDns.errorMessage = '';
|
||||
|
||||
Client.setDnsRecords({}, function (error /*, taskId */) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.syncDns.errorMessage = error.message;
|
||||
$scope.syncDns.busy = false;
|
||||
} else {
|
||||
$scope.syncDns.refreshTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.domainRemove = {
|
||||
busy: false,
|
||||
error: null,
|
||||
domain: null,
|
||||
|
||||
show: function (domain) {
|
||||
$scope.domainRemove.reset();
|
||||
|
||||
$scope.domainRemove.domain = domain;
|
||||
|
||||
$('#domainRemoveModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.domainRemove.busy = true;
|
||||
$scope.domainRemove.error = null;
|
||||
|
||||
Client.removeDomain($scope.domainRemove.domain.domain, function (error) {
|
||||
if (error && (error.statusCode === 403 || error.statusCode === 409)) {
|
||||
$scope.domainRemove.error = error.message;
|
||||
} else if (error) {
|
||||
Client.error(error);
|
||||
} else {
|
||||
$('#domainRemoveModal').modal('hide');
|
||||
$scope.domainRemove.reset();
|
||||
|
||||
refreshDomains();
|
||||
}
|
||||
|
||||
$scope.domainRemove.busy = false;
|
||||
});
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.domainRemove.busy = false;
|
||||
$scope.domainRemove.error = null;
|
||||
$scope.domainRemove.domain = null;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.changeDashboard = {
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
taskId: '',
|
||||
selectedDomain: null,
|
||||
adminDomain: null,
|
||||
tasks: [],
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_PREPARE_DASHBOARD_LOCATION, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
$scope.changeDashboard.tasks = tasks.slice(0, 10);
|
||||
if ($scope.changeDashboard.tasks.length && $scope.changeDashboard.tasks[0].active) $scope.changeDashboard.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
stop: function () {
|
||||
Client.stopTask($scope.changeDashboard.taskId, function (error) {
|
||||
if (error) console.error(error);
|
||||
$scope.changeDashboard.busy = false;
|
||||
$scope.changeDashboard.refreshTasks();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
if (!$scope.changeDashboard.busy) return; // task got stopped
|
||||
|
||||
Client.getTask($scope.changeDashboard.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.changeDashboard.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.changeDashboard.busy = false;
|
||||
$scope.changeDashboard.message = '';
|
||||
$scope.changeDashboard.percent = 100; // indicates that 'result' is valid
|
||||
$scope.changeDashboard.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
if (!$scope.changeDashboard.errorMessage) $scope.changeDashboard.setDashboardDomain();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.changeDashboard.busy = true;
|
||||
$scope.changeDashboard.percent = data.percent;
|
||||
$scope.changeDashboard.message = data.message;
|
||||
window.setTimeout($scope.changeDashboard.updateStatus, 500);
|
||||
});
|
||||
},
|
||||
|
||||
setDashboardDomain: function () {
|
||||
Client.setDashboardDomain($scope.changeDashboard.selectedDomain.domain, function (error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.changeDashboard.errorMessage = error.message;
|
||||
|
||||
$scope.changeDashboard.busy = false;
|
||||
} else {
|
||||
window.location.href = 'https://my.' + $scope.changeDashboard.selectedDomain.domain;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
change: function () {
|
||||
$scope.changeDashboard.busy = true;
|
||||
$scope.changeDashboard.message = 'Preparing dashboard domain';
|
||||
$scope.changeDashboard.percent = 0;
|
||||
$scope.changeDashboard.errorMessage = '';
|
||||
|
||||
Client.prepareDashboardDomain($scope.changeDashboard.selectedDomain.domain, function (error, taskId) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.changeDashboard.errorMessage = error.message;
|
||||
|
||||
$scope.changeDashboard.busy = false;
|
||||
} else {
|
||||
$scope.changeDashboard.taskId = taskId;
|
||||
$scope.changeDashboard.refreshTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
refreshDomains(function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.ready = true;
|
||||
});
|
||||
|
||||
$scope.renewCerts.refreshTasks();
|
||||
$scope.syncDns.refreshTasks();
|
||||
$scope.changeDashboard.refreshTasks();
|
||||
});
|
||||
|
||||
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.domainConfigure.gcdnsKey, 'content', 'keyFileName');
|
||||
document.getElementById('fallbackCertFileInput').onchange = readFileLocally($scope.domainConfigure.fallbackCert, 'certificateFile', 'certificateFileName');
|
||||
document.getElementById('fallbackKeyFileInput').onchange = readFileLocally($scope.domainConfigure.fallbackCert, 'keyFile', 'keyFileName');
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['domainConfigureModal', 'domainRemoveModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,788 +0,0 @@
|
||||
<!-- Modal subscription -->
|
||||
<div class="modal fade" id="subscriptionRequiredModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.subscriptionDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ 'email.subscriptionDialog.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="openSubscriptionSetup()">{{ 'email.subscriptionDialog.setupAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal enable email -->
|
||||
<div class="modal fade" id="enableEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.enableEmailDialog.title' | tr:{ domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'email.enableEmailDialog.description' | tr:{ domain: domain.domain, requiredPortsDocsLink: 'https://docs.cloudron.io/email/#required-ports' }"></p>
|
||||
<p class="text-warning" ng-show="domain.provider === 'noop' || domain.provider === 'manual'" ng-bind-html="'email.enableEmailDialog.noProviderInfo' | tr"></p>
|
||||
<p class="text-danger" ng-show="adminDomain.provider === 'cloudflare'" ng-bind-html="'email.enableEmailDialog.cloudflareInfo' | tr:{ adminDomain: config.adminDomain, mailFqdn: config.mailFqdn }"></p>
|
||||
<div ng-hide="domain.provider === 'noop' || domain.provider === 'manual'">
|
||||
<p>
|
||||
<label class="control-label">
|
||||
<input type="checkbox" ng-model="incomingEmail.setupDns"> {{ 'email.enableEmailDialog.setupDnsCheckbox' | tr }}
|
||||
</label>
|
||||
</p>
|
||||
<span ng-bind-html="'email.enableEmailDialog.setupDnsInfo' | tr:{ importEmailDocsLink: 'https://docs.cloudron.io/guides/import-email' }"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="incomingEmail.enable()">{{ 'email.enableEmailDialog.enableAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal disable email -->
|
||||
<div class="modal fade" id="disableEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.disableEmailDialog.title' | tr:{ domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html="'email.disableEmailDialog.description' | tr:{ domain: domain.domain }"></div>
|
||||
<br/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="incomingEmail.disable()">{{ 'email.disableEmailDialog.disableAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal add mailbox -->
|
||||
<div class="modal fade" id="mailboxAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.addMailboxDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="mailboxadd_form" role="form" ng-submit="mailboxes.add.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': mailboxes.add.error }">
|
||||
<label class="control-label">{{ 'email.addMailboxDialog.name' | tr }}</label>
|
||||
<div class="control-label" ng-show="mailboxes.add.error">
|
||||
<small>{{ mailboxes.add.error.message }}</small>
|
||||
</div>
|
||||
<div class="input-group form-inline" style="margin-top: 10px;">
|
||||
<input type="text" class="form-control" ng-model="mailboxes.add.name" required autofocus autocomplete="off"/>
|
||||
<div class="input-group-addon">@{{ domain.domain }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'email.addMailboxDialog.owner' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<multiselect ng-model="mailboxes.add.owner" options="o.display for o in owners" data-compare-by="name" data-header-key="header" data-divider-key="divider" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="mailboxadd_form.$invalid || mailboxes.add.busy || !mailboxes.add.owner"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="mailboxes.add.submit()" ng-disabled="mailboxadd_form.$invalid || mailboxes.add.busy || !mailboxes.add.owner"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.add.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal edit mailbox -->
|
||||
<div class="modal fade" id="mailboxEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.editMailboxDialog.title' | tr:{ name: mailboxes.edit.name, domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="mailboxedit_form" role="form" ng-submit="mailboxes.edit.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'email.editMailboxDialog.owner' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<multiselect ng-model="mailboxes.edit.owner" options="o.display for o in owners" data-compare-by="name" data-header-key="header" data-divider-key="divider" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group aliases">
|
||||
<label class="control-label">{{ 'email.editMailboxDialog.aliases' | tr }}</label>
|
||||
<div class="has-error" ng-show="mailboxes.edit.error">{{ mailboxes.edit.error.message }}</div>
|
||||
|
||||
<div class="row" ng-repeat="alias in mailboxes.edit.aliases | orderBy:'reversedSortingNotation'">
|
||||
<div class="col col-lg-11">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-sm" ng-model="alias.name" autofocus>
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
<span>@{{ alias.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="incomingDomain in incomingDomains">
|
||||
<a href="" ng-click="alias.domain = incomingDomain.domain">{{ incomingDomain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-lg-1">
|
||||
<button class="btn btn-danger btn-sm" ng-click="mailboxes.edit.delAlias($event, alias)"><i class="far fa-trash-alt"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="mailboxes.edit.aliases.length === 0">
|
||||
{{ 'email.editMailboxDialog.noAliases' | tr }} <a href="" ng-click="mailboxes.edit.addAlias($event)">{{ 'email.editMailboxDialog.addAliasAction' | tr }}</a>
|
||||
</div>
|
||||
<div ng-show="mailboxes.edit.aliases.length > 0" style="margin-top: 5px;">
|
||||
<a href="" ng-click="mailboxes.edit.addAlias($event)">{{ 'email.editMailboxDialog.addAnotherAliasAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storageQuota">
|
||||
<input id="storageQuota" type="checkbox" ng-model="mailboxes.edit.storageQuotaEnabled">
|
||||
{{ 'email.editMailboxDialog.enableStorageQuota' | tr }} <b ng-hide="!mailboxes.edit.storageQuotaEnabled">: {{ mailboxes.edit.storageQuota | prettyDecimalSize }}</b>
|
||||
</input>
|
||||
</label>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="storageQuota" ng-disabled="!mailboxes.edit.storageQuotaEnabled" ng-model="mailboxes.edit.storageQuota" step="500000000" ticks-snap-bounds="1000000000" tooltip="hide" ticks="storageQuotaTicks"></slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.edit.enablePop3"> {{ 'email.updateMailboxDialog.enablePop3' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.edit.active"> {{ 'email.updateMailboxDialog.activeCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input class="hide" type="submit" ng-disabled="mailboxedit_form.$invalid || mailboxes.edit.busy || !mailboxes.edit.owner"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="mailboxes.edit.submit()" ng-disabled="mailboxedit_form.$invalid || mailboxes.edit.busy || !mailboxes.edit.owner"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.edit.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal remove mailbox -->
|
||||
<div class="modal fade" id="mailboxRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.deleteMailboxDialog.title' | tr:{ name: mailboxes.remove.mailbox.name, domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html="'email.deleteMailboxDialog.description' | tr"></div>
|
||||
<br/>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.remove.deleteMails">{{ 'email.deleteMailboxDialog.purgeMailboxCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="mailboxes.remove.submit()" ng-disabled="mailboxes.remove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.remove.busy"></i> {{ 'email.deleteMailboxDialog.deleteAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal import mailboxes -->
|
||||
<div class="modal fade" id="mailboxImportModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.mailboxImportDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-show="!mailboxImport.done">
|
||||
<div ng-show="!mailboxImport.busy">
|
||||
<p ng-bind-html=" 'email.mailboxImportDialog.description' | tr:{ docsLink: 'https://cloudron.io/documentation/email/#import-mailboxes' } "></p>
|
||||
<input type="file" style="display: none;" id="mailboxImportFileInput" accept="application/json,text/csv"/>
|
||||
<button class="btn btn-primary" ng-click="mailboxImport.openFileInput()">{{ 'email.mailboxImportDialog.fileInput' | tr }}</button>
|
||||
<br/>
|
||||
<br/>
|
||||
<p class="text-danger" ng-show="mailboxImport.error.file">{{ mailboxImport.error.file }}</p>
|
||||
<p class="text-info" ng-show="mailboxImport.mailboxes.length">{{ 'email.mailboxImportDialog.mailboxesFound' | tr:{ count: mailboxImport.mailboxes.length } }}</p>
|
||||
</div>
|
||||
<div ng-show="mailboxImport.busy" class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ mailboxImport.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="mailboxImport.done">
|
||||
<p>{{ 'email.mailboxImportDialog.success' | tr:{ count: mailboxImport.success } }}</p>
|
||||
<div ng-show="mailboxImport.error.import.length">
|
||||
<p class="text-danger">{{ 'email.mailboxImportDialog.failed' | tr }}</p>
|
||||
<div ng-repeat="tmp in mailboxImport.error.import"><b>{{ tmp.mailbox.name }}@{{ tmp.mailbox.domain }}:</b> {{ tmp.error.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="mailboxImport.import()" ng-show="!mailboxImport.done" ng-disabled="mailboxImport.busy || !mailboxImport.mailboxes.length"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxImport.busy"></i> {{ 'email.mailboxImportDialog.importAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal add mailinglist -->
|
||||
<div class="modal fade" id="mailinglistAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.addMailinglistDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="mailinglistadd_form" role="form" ng-submit="mailinglists.add.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': mailinglists.add.error.name }">
|
||||
<label class="control-label">{{ 'email.addMailinglistDialog.name' | tr }}</label>
|
||||
<div class="control-label" ng-show="mailinglists.add.error.name"><small>{{ mailinglists.add.error.name }}</small></div>
|
||||
<div class="input-group form-inline" style="margin-top: 10px;">
|
||||
<input type="text" class="form-control" ng-model="mailinglists.add.name" required autofocus autocomplete="off"/>
|
||||
<div class="input-group-addon">@{{ domain.domain }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'email.addMailinglistDialog.members' | tr }}</label><br/>
|
||||
<div class="has-error control-label" ng-show="mailinglists.add.error.members"><small>{{ mailinglists.add.error.members }}</small></div>
|
||||
<textarea ng-model="mailinglists.add.membersTxt" class="form-control" rows="5"></textarea>
|
||||
<small>{{ 'email.addMailinglistDialog.membersInfo' | tr }}</small>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailinglists.add.membersOnly">{{ 'email.addMailinglistDialog.membersOnlyCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="mailinglistadd_form.$invalid || mailinglists.add.membersTxt.length === 0 || mailinglists.add.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="mailinglists.add.submit()" ng-disabled="mailinglistadd_form.$invalid || mailinglists.add.membersTxt.length === 0 || mailinglists.add.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailinglists.add.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal edit mailinglist -->
|
||||
<div class="modal fade" id="mailinglistEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.editMailinglistDialog.title' | tr:{ name: mailinglists.edit.name, domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="mailinglistedit_form" role="form" ng-submit="mailinglists.edit.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': mailinglists.edit.error.members }">
|
||||
<label class="control-label">{{ 'email.addMailinglistDialog.members' | tr }}</label><br/>
|
||||
<div class="has-error control-label" ng-show="mailinglists.edit.error.members"><small>{{ mailinglists.edit.error.members }}</small></div>
|
||||
<textarea ng-model="mailinglists.edit.membersTxt" class="form-control" rows="5" autofocus></textarea>
|
||||
<small>{{ 'email.addMailinglistDialog.membersInfo' | tr }}</small>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailinglists.edit.membersOnly">{{ 'email.addMailinglistDialog.membersOnlyCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailinglists.edit.active"> {{ 'email.updateMailinglistDialog.activeCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="mailinglistedit_form.$invalid || mailinglists.edit.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="mailinglists.edit.submit()" ng-disabled="mailinglistedit_form.$invalid || mailinglists.edit.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailinglists.edit.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal remove mailinglist -->
|
||||
<div class="modal fade" id="mailinglistRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.deleteMailinglistDialog.title' | tr:{ name: mailinglists.remove.list.name, domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'email.deleteMailinglistDialog.description' | tr:{ name: mailinglists.remove.list.name, domain: domain.domain }"></p>`
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="mailinglists.remove.submit()" ng-disabled="mailinglists.remove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailinglist.remove.busy"></i> {{ 'email.deleteMailinglistDialog.deleteAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal how to connect -->
|
||||
<div class="modal fade" id="howToConnectInfoModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4>{{ 'email.howToConnectInfoModal' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html=" 'email.incoming.howToConnectDescription' | tr:{ domain: domain.domain } "></p>
|
||||
|
||||
<p><b>{{ 'email.incoming.incomingUserInfo' | tr }}</b><br/><i>mailboxname</i>@{{ domain.domain }}</p>
|
||||
<p><b>{{ 'email.incoming.incomingPasswordInfo' | tr }}</b><br/>{{ 'email.incoming.incomingPasswordUsage' | tr }}</p>
|
||||
<p><b>{{ 'email.incoming.incomingServerInfo' | tr }}</b><br/>{{ 'email.incoming.server' | tr }}: <span ng-click-select>{{config.mailFqdn}}</span><br/>{{ 'email.incoming.port' | tr }}: 993 (TLS)</p>
|
||||
<p><b>{{ 'email.incoming.outgointServerInfo' | tr }}</b><br/>{{ 'email.incoming.server' | tr }}: <span ng-click-select>{{config.mailFqdn}}</span><br/>{{ 'email.incoming.port' | tr }}: 587 (STARTTLS) or 465 (TLS)</p>
|
||||
<p><b>{{ 'email.incoming.sieveServerInfo' | tr }}</b><br/>{{ 'email.incoming.server' | tr }}: <span ng-click-select>{{config.mailFqdn}}</span><br/>{{ 'email.incoming.port' | tr }}: 4190 (STARTTLS)</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="!ready" class="loading-banner">
|
||||
<h1><i class="fa fa-circle-notch fa-spin"></i></h1>
|
||||
</div>
|
||||
|
||||
<div class="content" ng-show="ready">
|
||||
<a href="/#/email" class="back-to-view-link"><i class="fas fa-arrow-left"></i> {{ 'email.backAction' | tr }}</a>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>
|
||||
{{ 'email.config.title' | tr:{ domain: domain.domain } }}
|
||||
|
||||
<div class="dropdown pull-right" style="display: inline-block">
|
||||
<button class="btn btn-sm btn-default dropdown-toggle" type="button" data-toggle="dropdown" uib-tooltip="{{ 'app.docsActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom">
|
||||
<i class="fas fa-book"></i>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="https://docs.cloudron.io/email/" target="_blank">{{ 'app.docsAction' | tr }}</a></li>
|
||||
<li ng-class="{ 'disabled': !domain.mailConfig.enabled }"><a href="" ng-click="howToConnectInfo.show()">{{ 'email.config.clientConfiguration' | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<uib-tabset active="activeTab">
|
||||
<uib-tab index="'mailboxes'" select="setView('mailboxes')" heading="{{ 'email.incoming.tabTitle' | tr }}">
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<h4>{{ 'email.incoming.title' | tr }}</h4>
|
||||
|
||||
<p ng-show="domain.mailConfig.enabled">{{ 'email.incoming.enabled' | tr }}</p>
|
||||
<p ng-hide="domain.mailConfig.enabled">{{ 'email.incoming.disabled' | tr }}</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<button class="pull-right" ng-class="domain.mailConfig.enabled ? 'btn btn-danger' : 'btn btn-primary'" ng-click="incomingEmail.toggleEmailEnabled()" ng-disabled="incomingEmail.busy" ng-show="user.isAtLeastAdmin">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="incomingEmail.busy"></i>
|
||||
{{ domain.mailConfig.enabled ? ('email.incoming.disableAction' | tr) : ('email.incoming.enableAction' | tr) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3 style="margin-bottom: 15px;">{{ 'email.incoming.mailboxes.title' | tr }}
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-click="mailboxes.add.show()" ng-disabled="!domain.mailConfig.enabled" tooltip-enable="!domain.mailConfig.enabled" uib-tooltip="{{ 'email.incoming.mailboxes.disabledTooltip' | tr }}"><i class="fa fa-inbox"></i> {{ 'email.incoming.mailboxes.addAction' | tr }}</button>
|
||||
<div class="btn-group pull-right" style="margin-left: 5px;">
|
||||
<button class="btn btn-default" ng-click="mailboxImport.show()" uib-tooltip="{{ 'email.incoming.mailboxes.importTooltip' | tr }}" tooltip-append-to-body="true"><i class="fas fa-download"></i></button>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" uib-tooltip="{{ 'email.incoming.mailboxes.exportTooltip' | tr }}" tooltip-append-to-body="true">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="" ng-click="mailboxExport('csv')">{{ 'email.incoming.mailboxes.mailboxExport.csv' | tr }}</a></li>
|
||||
<li><a href="" ng-click="mailboxExport('json')">{{ 'email.incoming.mailboxes.mailboxExport.json' | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<input class="form-control pull-right" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="mailboxes.search" ng-model-options="{ debounce: 1000 }" ng-change="mailboxes.updateFilter()" />
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'email.incoming.mailboxes.name' | tr }}</th>
|
||||
<th>{{ 'email.incoming.mailboxes.owner' | tr }}</th>
|
||||
<th>{{ 'email.incoming.mailboxes.aliases' | tr }}</th>
|
||||
<th>{{ 'email.incoming.mailboxes.usage' | tr }}</th>
|
||||
<th class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="mailbox in mailboxes.mailboxes | filter:mailboxes.search" ng-class="{'text-muted': !mailbox.active}">
|
||||
<td class="hand" ng-click="mailboxes.edit.show(mailbox)">
|
||||
{{ mailbox.name }}
|
||||
</td>
|
||||
<td class="hand" ng-click="mailboxes.edit.show(mailbox)">
|
||||
{{ mailbox.ownerDisplayName }}
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="mailboxes.edit.show(mailbox)">
|
||||
<span ng-repeat="alias in mailbox.aliases"> {{ alias.name + '@' + alias.domain }}</span>
|
||||
</td>
|
||||
<td class="hand no-wrap" ng-click="mailboxes.edit.show(mailbox)">
|
||||
<span ng-show="mailUsage !== null">
|
||||
{{ mailUsage[mailbox.fullName].quotaValue | prettyDecimalSize }} <span ng-show="mailUsage[mailbox.fullName].quotaLimit">/ {{ mailUsage[mailbox.fullName].quotaLimit | prettyDecimalSize }}</span>
|
||||
</span>
|
||||
<span ng-show="mailUsage === null">
|
||||
{{ 'main.loadingPlaceholder' | tr }} ...
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap">
|
||||
<button class="btn btn-xs btn-default" ng-click="mailboxes.edit.show(mailbox)"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="mailboxes.remove.show(mailbox)"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="mailboxes.showPrevPage()" ng-class="{ 'btn-primary': mailboxes.currentPage > 1 }" ng-disabled="mailboxes.busy || mailboxes.currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="mailboxes.showNextPage()" ng-class="{ 'btn-primary': mailboxes.perPage <= mailboxes.mailboxes.length }" ng-disabled="mailboxes.busy || mailboxes.perPage > mailboxes.mailboxes.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3 style="margin-bottom: 15px;">{{ 'email.incoming.mailinglists.title' | tr }}
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-click="mailinglists.add.show()" ng-disabled="!domain.mailConfig.enabled" tooltip-enable="!domain.mailConfig.enabled" uib-tooltip="{{ 'email.incoming.mailboxes.disabledTooltip' | tr }}"><i class="fa fa-list"></i> {{ 'email.incoming.mailboxes.addAction' | tr }}</button>
|
||||
<input class="form-control pull-right" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="mailinglists.search" ng-model-options="{ debounce: 1000 }" ng-change="mailinglists.updateFilter()" />
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{ 'email.incoming.mailinglists.description' | tr }}
|
||||
<br/>
|
||||
<br/>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 0.5%;"></th>
|
||||
<th>{{ 'email.incoming.mailinglists.name' | tr }}</th>
|
||||
<th>{{ 'email.incoming.mailinglists.members' | tr }}</th>
|
||||
<th class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="list in mailinglists.mailinglists | filter:mailinglists.search | orderBy:'name'" ng-class="{'text-muted': !list.active}">
|
||||
<td>
|
||||
<i class="fas fa-door-closed" ng-show="list.membersOnly" uib-tooltip="{{ 'email.incoming.mailinglists.membersOnlyTooltip' | tr }}"></i>
|
||||
<i class="fas fa-door-open" ng-show="!list.membersOnly" uib-tooltip="{{ 'email.incoming.mailinglists.everyoneTooltip' | tr }}"></i>
|
||||
</td>
|
||||
<td class="hand" ng-click="mailinglists.edit.show(list)">
|
||||
{{ list.name }}
|
||||
</td>
|
||||
<td class="hand" ng-click="mailinglists.edit.show(list)">
|
||||
{{ list.members.join(', ') }}
|
||||
</td>
|
||||
<td class="text-right no-wrap">
|
||||
<button class="btn btn-xs btn-default" ng-click="mailinglists.edit.show(list)"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="mailinglists.remove.show(list)"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="mailinglists.showPrevPage()" ng-class="{ 'btn-primary': mailinglists.currentPage > 1 }" ng-disabled="mailinglists.busy || mailinglists.currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="mailinglists.showNextPage()" ng-class="{ 'btn-primary': mailinglists.perPage <= mailinglists.mailinglists.length }" ng-disabled="mailinglists.busy || mailinglists.perPage > mailinglists.mailinglists.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'email.incoming.catchall.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" ng-bind-html=" 'email.incoming.catchall.description' | tr "></div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<multiselect ng-model="catchall.mailboxes" options="mailbox.display for mailbox in catchall.availableMailboxes" data-compare-by="display" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<button class="btn btn-outline btn-primary" ng-click="catchall.submit()" ng-disabled="catchall.busy || !domain.mailConfig.enabled" tooltip-enable="!domain.mailConfig.enabled" uib-tooltip="{{ 'email.incoming.mailboxes.disabledTooltip' | tr }}">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="catchall.busy"></i> {{ 'email.incoming.catchall.saveAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="'outbound'" ng-if="user.isAtLeastAdmin" select="setView('outbound')" heading="{{ 'email.outbound.tabTitle' | tr }}">
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<h4>{{ 'email.outbound.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#relay-outbound-mails" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></h4>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" ng-bind-html=" 'email.outbound.description' | tr "></div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<select class="form-control" style="width: 50%;" ng-model="mailRelay.preset" ng-options="a.name for a in mailRelayPresets track by a.provider" ng-change="mailRelay.presetChanged()"></select>
|
||||
</div>
|
||||
|
||||
<p class="small text-danger" ng-show="mailRelay.preset.provider === 'noop'">
|
||||
<span ng-if="domain.domain === config.adminDomain">{{ 'email.outbound.noopAdminDomainWarning' | tr }}</span>
|
||||
<span ng-if="domain.domain !== config.adminDomain">{{ 'email.outbound.noopNonAdminDomainWarning' | tr }}</span>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="usesExternalServer(mailRelay.preset.provider)">
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<form name="mailRelayForm" role="form" ng-submit="mailRelay.submit()" autocomplete="off" novalidate>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.host.$dirty && mailRelayForm.host.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.host' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.host.$dirty && mailRelay.error.host) || (mailRelayForm.host.$dirty && mailRelayForm.host.$invalid)">
|
||||
<small ng-show="!mailRelayForm.host.$dirty && mailRelay.error.host">{{ mailRelay.error.host }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.host" name="host" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.port.$dirty && mailRelayForm.port.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.port' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.port.$dirty && mailRelay.error.port) || (mailRelayForm.port.$dirty && mailRelayForm.port.$invalid)">
|
||||
<small ng-show="!mailRelayForm.port.$dirty && mailRelay.error.port">{{ mailRelay.error.port }}</small>
|
||||
</div>
|
||||
<input type="number" class="form-control" ng-model="mailRelay.relay.port" name="port" required>
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="mailRelay.relay.provider === 'external-smtp' || mailRelay.relay.provider === 'external-smtp-noauth'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailRelay.relay.acceptSelfSignedCerts">{{ 'email.outbound.mailRelay.selfsignedCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Postmark, Sendgrid, SparkPost -->
|
||||
<div ng-show="usesTokenAuth(mailRelay.relay.provider)" class="form-group" ng-class="{ 'has-error': (mailRelayForm.serverApiToken.$dirty && mailRelayForm.serverApiToken.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.apiTokenOrKey' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.serverApiToken.$dirty && mailRelay.error.serverApiToken) || (mailRelayForm.serverApiToken.$dirty && mailRelayForm.serverApiToken.$invalid)">
|
||||
<small ng-show="!mailRelayForm.serverApiToken.$dirty && mailRelay.error.serverApiToken">{{ mailRelay.error.serverApiToken }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.serverApiToken" name="serverApiToken" ng-required="usesTokenAuth(mailRelay.relay.provider)">
|
||||
</div>
|
||||
|
||||
<!-- Other -->
|
||||
<div ng-show="usesPasswordAuth(mailRelay.relay.provider)" class="form-group" ng-class="{ 'has-error': (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.username' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.username.$dirty && mailRelay.error.username) || (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid)">
|
||||
<small ng-show="!mailRelayForm.username.$dirty && mailRelay.error.username">{{ mailRelay.error.username }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.username" name="username" ng-required="usesPasswordAuth(mailRelay.relay.provider)">
|
||||
</div>
|
||||
|
||||
<div ng-show="usesPasswordAuth(mailRelay.relay.provider)" class="form-group" ng-class="{ 'has-error': (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.password' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.password.$dirty && mailRelay.error.password) || (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid)">
|
||||
<small ng-show="!mailRelayForm.password.$dirty && mailRelay.error.password">{{ mailRelay.error.password }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="mailRelay.relay.password" name="password" ng-required="usesPasswordAuth(mailRelay.relay.provider)" password-reveal>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="mailRelayForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<button class="btn btn-primary pull-right" ng-click="mailRelay.submit()" ng-disabled="(usesExternalServer(mailRelay.preset.provider) && (!mailRelayForm.$dirty || mailRelayForm.$invalid)) || mailRelay.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailRelay.busy"></i> {{ 'email.outbound.mailRelay.saveAction' | tr }}</button>
|
||||
|
||||
<span class="has-error text-center" ng-show="mailRelay.error">{{ mailRelay.error }}</span>
|
||||
<span class="text-success text-center text-bold" ng-show="mailRelay.success">{{ 'email.outbound.mailRelay.saveSuccess' | tr }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="mailRelay.preset.spfDoc">
|
||||
<br/>
|
||||
<div class="col-md-12">
|
||||
<span class="text-info" ng-bind-html="'email.outbound.mailRelay.spfDocInfo' | tr:{ name: mailRelay.preset.name, spfDocsLink: mailRelay.preset.spfDoc }"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="'settings'" select="setView('settings')" heading="{{ 'email.settings.tabTitle' | tr }}">
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<h4>{{ 'email.masquerading.title' | tr }}</h4>
|
||||
<p ng-bind-html=" 'email.masquerading.description' | tr "></p>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="pull-right" ng-class="domain.mailConfig.mailFromValidation ? 'btn btn-danger' : 'btn btn-primary'" ng-disabled="mailFromValidation.busy" ng-click="mailFromValidation.submit()">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="mailFromValidation.busy"></i> {{ domain.mailConfig.mailFromValidation ? ('email.masquerading.enableAction' | tr) : ('email.masquerading.disableAction' | tr) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<h4>{{ 'email.signature.title' | tr }}</h4>
|
||||
<p ng-bind-html=" 'email.signature.description' | tr "></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form role="form" name="bannerForm" ng-submit="banner.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label" style="width: 100%">{{ 'email.signature.plainTextFormat' | tr }}</label>
|
||||
<textarea ng-model="banner.text" class="form-control" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" style="width: 100%">{{ 'email.signature.htmlFormat' | tr }}</label>
|
||||
<textarea ng-model="banner.html" class="form-control" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="banner.$invalid || banner.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="banner.submit()" ng-disabled="banner.$invalid || banner.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="banner.busy"></i> {{ 'email.signature.saveAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="'status'" ng-if="user.isAtLeastAdmin" select="setView('status')" heading="{{ 'email.status.tabTitle' | tr }}">
|
||||
<!-- nothing to show if incoming mail is disabled and using a relay -->
|
||||
<div class="card card-large" style="margin-bottom: 15px;" ng-hide="!domain.mailConfig.enabled && domain.mailConfig.relay.provider !== 'cloudron-smtp'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>{{ 'email.dnsStatus.title' | tr }}
|
||||
<button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="incomingEmail.setDnsRecords()">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="incomingEmail.setupDnsBusy"></i> {{ 'email.dnsStatus.reSetupAction' | tr }}
|
||||
</button>
|
||||
</h4>
|
||||
<span ng-bind-html="'email.dnsStatus.description' | tr:{ emailDnsDocsLink:'https://docs.cloudron.io/email/#dns-records'}"></span>
|
||||
<br/>
|
||||
<br/>
|
||||
<div ng-repeat="record in expectedDnsRecordsTypes">
|
||||
<div class="row" ng-if="expectedDnsRecords[record.value].expected">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-hide="refreshBusy" ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">{{ record.name }} record</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!expectedDnsRecords[record.value].status"><i class="fa fa-sync-alt" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p ng-show="record.name === 'MX' && domain.provider === 'namecheap'">{{ 'email.dnsStatus.namecheapInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/domains/#namecheap-dns" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<p ng-show="record.name === 'PTR'">{{ 'email.dnsStatus.ptrInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#ptr-record" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<p ng-show="expectedDnsRecords[record.value].name">{{ 'email.dnsStatus.hostname' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].name }}</tt></b></p>
|
||||
<p ng-hide="expectedDnsRecords[record.value].name">{{ 'email.dnsStatus.domain' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].domain }}</tt></b></p>
|
||||
<p>{{ 'email.dnsStatus.type' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].type }}</tt></b></p>
|
||||
<p style="overflow: auto; white-space: nowrap;">{{ 'email.dnsStatus.expected' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].expected }}</tt></b></p>
|
||||
<p style="overflow: auto; white-space: nowrap;">{{ 'email.dnsStatus.current' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].value ? expectedDnsRecords[record.value].value : ('['+('email.dnsStatus.recordNotSet' | tr)+']') }}</tt></b></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;" ng-if="domain.mailConfig.relay.provider !== 'noop'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>{{ 'email.smtpStatus.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#smtp-status" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></h4>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-hide="refreshBusy" ng-class="domain.mailStatus.relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_outbound_smtp">
|
||||
{{ domain.mailConfig.relay.provider === 'cloudron-smtp' ? ('email.smtpStatus.outboudDirect' | tr) : ('email.smtpStatus.outboudRelay' | tr) }}
|
||||
</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!domain.mailStatus.relay.status"><i class="fa fa-sync-alt" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_outbound_smtp" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p><b> {{ domain.mailStatus.relay.value }} </b> </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="domain.mailConfig.relay.provider === 'cloudron-smtp'">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-hide="refreshBusy" ng-class="domain.mailStatus.rbl.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_rbl">{{ 'email.smtpStatus.blacklistCheck' | tr }}</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!domain.mailStatus.rbl.status"><i class="fa fa-sync-alt" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_rbl" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<div ng-show="domain.mailStatus.rbl.servers.length" ng-bind-html="'email.smtpStatus.blacklisted' | tr:{ ip: domain.mailStatus.rbl.ip }"></div>
|
||||
<div ng-hide="domain.mailStatus.rbl.servers.length" ng-bind-html="'email.smtpStatus.notBlacklisted' | tr:{ ip: domain.mailStatus.rbl.ip }"></div>
|
||||
<div ng-repeat="server in domain.mailStatus.rbl.servers">
|
||||
<a ng-href="{{server.site}}" target="_blank">{{ server.name }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</div>
|
||||