Compare commits
138 Commits
Author | SHA1 | Date |
---|---|---|
Inex Code | 3024016fe2 | |
Inex Code | 9cc3fa5f91 | |
NaiJi ✨ | f370a7fc91 | |
NaiJi ✨ | ed4234ee63 | |
Inex Code | 18d0c2c40f | |
NaiJi ✨ | ad53000415 | |
Inex Code | bf03f61668 | |
Inex Code | 3c3cb376e2 | |
NaiJi ✨ | 3fbdc05469 | |
NaiJi ✨ | 43411adf2c | |
NaiJi ✨ | 80e0488700 | |
NaiJi ✨ | 2ac8e4366b | |
Inex Code | 4db0413c42 | |
NaiJi ✨ | 5909b9a3e6 | |
Inex Code | e8d5ecccf6 | |
Inex Code | 7810c2a279 | |
Inex Code | 8ec3b8c3e3 | |
NaiJi ✨ | 1db8e9556e | |
NaiJi ✨ | ead19d2210 | |
Inex Code | b60fb19ecc | |
NaiJi ✨ | 72ef16c6f6 | |
NaiJi ✨ | 5dcaa060a1 | |
Inex Code | 14acfdec6b | |
NaiJi ✨ | edce25ec55 | |
Inex Code | a096e7e732 | |
NaiJi ✨ | 804e2750da | |
NaiJi ✨ | 7344858e86 | |
NaiJi ✨ | ac93a384e9 | |
Inex Code | fa6f74e884 | |
NaiJi ✨ | eddeac57d6 | |
NaiJi ✨ | eaa1ba143c | |
NaiJi ✨ | 6fd7f9400d | |
NaiJi ✨ | d8568fc82f | |
Inex Code | df40a09419 | |
Inex Code | d2553b0d08 | |
NaiJi ✨ | 2d96b4505e | |
Inex Code | dd77b99ac8 | |
NaiJi ✨ | 20f6e8156c | |
Inex Code | bf79fb1adf | |
NaiJi ✨ | 19bc780db1 | |
NaiJi ✨ | 0d0a3a4fee | |
NaiJi ✨ | 93215d90fb | |
NaiJi ✨ | 7a719f15ce | |
NaiJi ✨ | ee53590ba0 | |
NaiJi ✨ | a56af9dbec | |
NaiJi ✨ | f46ca7ad13 | |
Inex Code | b4145dc5c8 | |
NaiJi ✨ | 10488d6832 | |
Inex Code | 8b5bf24f3a | |
Inex Code | cc91b14b44 | |
NaiJi ✨ | 129c1bb4c6 | |
NaiJi ✨ | 4a42733d31 | |
NaiJi ✨ | 01b1f7462d | |
NaiJi ✨ | ce3e046f5a | |
NaiJi ✨ | 31be961dd0 | |
NaiJi ✨ | c4ae2b3b4f | |
NaiJi ✨ | 8d6cbfdfc9 | |
NaiJi ✨ | 4c7cf05578 | |
Inex Code | 9cec5e901a | |
Inex Code | c5fa712ef0 | |
NaiJi ✨ | 4c99579f13 | |
Inex Code | 265cc15ea5 | |
Inex Code | 4ddde34b47 | |
NaiJi ✨ | 834cddfe13 | |
Inex Code | 125788c3ab | |
Inex Code | 2e6bfcc5e1 | |
Inex Code | 902b9fbda5 | |
Inex Code | c36038b1ab | |
Inex Code | 45134d26da | |
Inex Code | cca55f82cc | |
Inex Code | 4d8843f008 | |
Inex Code | bef625a934 | |
Inex Code | 073b7bfcb6 | |
NaiJi ✨ | 8d884af594 | |
Inex Code | 3e2a86ede1 | |
Inex Code | 85235a2e7c | |
Inex Code | d240e493b1 | |
Inex Code | e4bdd47848 | |
Inex Code | e925a1897a | |
Inex Code | 914d56ff87 | |
Inex Code | 83a2d19e37 | |
Inex Code | 8de33ea19b | |
kherel | 7eff0968d0 | |
Inex Code | d06e8976c5 | |
Inex Code | a0edbd636d | |
Inex Code | 9afe61db42 | |
kherel | 83ff387998 | |
Inex Code | f6508dfcad | |
Inex Code | 0bc3d9f31c | |
Inex Code | 88d5dbf010 | |
kherel | 9566a6ad10 | |
Illia Chub | d79b41a3fe | |
Inex Code | 3cd187e416 | |
Inex Code | 2dfb92f650 | |
Inex Code | 72100e483a | |
Inex Code | de13b09f23 | |
Inex Code | 2e8908053c | |
Inex Code | 8622ed30f1 | |
Inex Code | 5957e5720b | |
Inex Code | e72df08453 | |
Inex Code | 532a3ab197 | |
Inex Code | 3a63e75e8e | |
Illia Chub | 495cd115fa | |
Illia Chub | 56fe8fd329 | |
Inex Code | 893c5bb5c0 | |
Inex Code | e7e9209cce | |
Inex Code | 30937740b6 | |
Inex Code | c937cfdbb4 | |
Inex Code | c66bff005c | |
Inex Code | 85a27e8ee2 | |
Inex Code | 4ca873e794 | |
Inex Code | 9b2fe905e6 | |
Illia Chub | 9594c538c7 | |
Inex Code | 1c15712596 | |
Inex Code | 1b42d3a382 | |
Illia Chub | 6653408dfb | |
Illia Chub | dfed4a113e | |
Illia Chub | af80823678 | |
Illia Chub | fe41ae2cb8 | |
Illia Chub | 71d0cafdec | |
Inex Code | f633fecd57 | |
Inex Code | 2b8c009ef1 | |
Inex Code | 35c1eea7f0 | |
Inex Code | 49efd16d37 | |
Inex Code | d21b9df734 | |
Inex Code | b40bea63d1 | |
Inex Code | 650e0e7376 | |
kherel | 1b490c7bc9 | |
Inex Code | 79f672acbc | |
Inex Code | 6011d6fdce | |
Illia Chub | b0a2c3312b | |
Inex Code | 74d80b2989 | |
kherel kechil | 64276c036e | |
kherel | ca9c3205f1 | |
Inex Code | 08ff445935 | |
Zholnay Kirill | 518ae1791b | |
Illia Chub | be55a1b13b | |
Kherel | 139843dee8 |
29
.metadata
|
@ -1,10 +1,33 @@
|
||||||
# This file tracks properties of this Flutter project.
|
# This file tracks properties of this Flutter project.
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
#
|
#
|
||||||
# This file should be version controlled and should not be manually edited.
|
# This file should be version controlled.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: 1aafb3a8b9b0c36241c5f5b34ee914770f015818
|
revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a
|
||||||
channel: stable
|
channel: beta
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a
|
||||||
|
base_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a
|
||||||
|
- platform: linux
|
||||||
|
create_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a
|
||||||
|
base_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a
|
||||||
|
- platform: windows
|
||||||
|
create_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a
|
||||||
|
base_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
|
|
|
@ -0,0 +1,674 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
@ -1,6 +1,7 @@
|
||||||
# selfprivacy
|
# SelfPrivacy App
|
||||||
|
|
||||||
selfprivacy.org
|
- [Official site](https://selfprivacy.org)
|
||||||
|
- [SelfPrivacy App (F-Droid)](https://f-droid.org/en/packages/pro.kherel.selfprivacy/)
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- lib/generated_plugin_registrant.dart
|
||||||
|
- lib/**.g.dart
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at
|
||||||
|
# https://dart-lang.github.io/linter/lints/index.html.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
always_use_package_imports: true
|
||||||
|
invariant_booleans: true
|
||||||
|
no_adjacent_strings_in_list: true
|
||||||
|
unnecessary_statements: true
|
||||||
|
always_declare_return_types: true
|
||||||
|
always_put_required_named_parameters_first: true
|
||||||
|
always_put_control_body_on_new_line: true
|
||||||
|
avoid_escaping_inner_quotes: true
|
||||||
|
avoid_setters_without_getters: true
|
||||||
|
eol_at_end_of_file: true
|
||||||
|
prefer_constructors_over_static_methods: true
|
||||||
|
prefer_expression_function_bodies: true
|
||||||
|
prefer_final_in_for_each: true
|
||||||
|
prefer_final_locals: true
|
||||||
|
prefer_final_parameters: true
|
||||||
|
prefer_foreach: true
|
||||||
|
prefer_if_elements_to_conditional_expressions: true
|
||||||
|
prefer_mixin: true
|
||||||
|
prefer_null_aware_method_calls: true
|
||||||
|
require_trailing_commas: true
|
||||||
|
sized_box_shrink_expand: true
|
||||||
|
sort_constructors_first: true
|
||||||
|
unnecessary_await_in_return: true
|
||||||
|
unnecessary_null_checks: true
|
||||||
|
unnecessary_parenthesis: true
|
||||||
|
use_enums: true
|
||||||
|
use_if_null_to_convert_nulls_to_bools: true
|
||||||
|
use_is_even_rather_than_modulo: true
|
||||||
|
use_late_for_private_fields_and_variables: true
|
||||||
|
use_named_constants: true
|
||||||
|
use_setters_to_change_properties: true
|
||||||
|
use_string_buffers: true
|
||||||
|
use_super_parameters: true
|
||||||
|
use_to_and_as_if_applicable: true
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
|
@ -26,12 +26,22 @@ apply plugin: 'kotlin-android'
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion flutter.compileSdkVersion
|
||||||
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
disable 'InvalidPackage'
|
disable 'InvalidPackage'
|
||||||
}
|
}
|
||||||
|
@ -39,8 +49,8 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "pro.kherel.selfprivacy"
|
applicationId "pro.kherel.selfprivacy"
|
||||||
minSdkVersion 18
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 31
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="pro.kherel.selfprivacy">
|
package="pro.kherel.selfprivacy">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
In most cases you can leave this as-is, but you if you want to provide
|
In most cases you can leave this as-is, but you if you want to provide
|
||||||
additional functionality it is fine to subclass or reimplement
|
additional functionality it is fine to subclass or reimplement
|
||||||
FlutterApplication and put your custom class here. -->
|
FlutterApplication and put your custom class here. -->
|
||||||
<application
|
<application
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
android:name="${applicationName}"
|
||||||
android:label="SelfPrivacy"
|
android:label="SelfPrivacy"
|
||||||
android:icon="@mipmap/launcher_icon">
|
android:icon="@mipmap/launcher_icon">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:exported="true"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
Flutter draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
|
@ -1,12 +1,12 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.3.50'
|
ext.kotlin_version = '1.6.10'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||||
|
|
After Width: | Height: | Size: 2.1 MiB |
After Width: | Height: | Size: 2.6 MiB |
After Width: | Height: | Size: 3.3 MiB |
|
@ -0,0 +1,8 @@
|
||||||
|
### How to get Backblaze API Token
|
||||||
|
1. Visit the following link and authorize: https://secure.backblaze.com/user_signin.htm
|
||||||
|
2. On the left side of the interface, select **App Keys** in the **B2 Cloud Storage** subcategory.
|
||||||
|
3. Click on the blue **Generate New Master Application Key** button.
|
||||||
|
4. In the appeared pop-up window confirm the generation.
|
||||||
|
5. Save _keyID_ and _applicationKey_ in the safe place. For example, in the password manager.
|
||||||
|
|
||||||
|
![Backblaze token setup](resource:assets/images/gifs/Backblaze.gif)
|
|
@ -0,0 +1,8 @@
|
||||||
|
### Как получить Backblaze API Token
|
||||||
|
1. Переходим по ссылке https://secure.backblaze.com/user_signin.htm и авторизуемся.
|
||||||
|
2. В левой части интерфейса выбираем **App Keys** в подкатегории **"Account"**.
|
||||||
|
3. Нажимаем на синюю кнопку **Generate New Master Application Key**.
|
||||||
|
4. Во всплывающем окне подтверждаем генерацию.
|
||||||
|
5. Сохраняем _keyID_ и _applicationKey_ в надёжном месте. Например в менеджере паролей.
|
||||||
|
|
||||||
|
![Backblaze token setup](resource:assets/images/gifs/Backblaze.gif)
|
|
@ -0,0 +1,17 @@
|
||||||
|
### How to get Cloudflare API Token
|
||||||
|
1. Visit the following link: https://dash.cloudflare.com/
|
||||||
|
2. the right corner, click on the profile icon (a man in a circle). For the mobile version of the site, in the upper left corner, click the **Menu** button (three horizontal bars), in the dropdown menu, click on **My Profile**
|
||||||
|
3. There are four configuration categories to choose from: *Communication*, *Authentication*, **API Tokens**, *Session*. Choose **API Tokens**.
|
||||||
|
4. Click on **Create Token** button.
|
||||||
|
5. Go down to the bottom and see the **Create Custom Token** field and press **Get Started** button on the right side.
|
||||||
|
6. In the **Token Name** field, give your token a name.
|
||||||
|
7. Next we have Permissions. In the leftmost field, select **Zone**. In the longest field, center, select **DNS**. In the rightmost field, select **Edit**.
|
||||||
|
8. Next, right under this line, click Add More. Similar field will appear.
|
||||||
|
9. In the leftmost field of the new line, select, similar to the last line — **Zone**. In the center — a little different. Here choose the same as in the left — **Zone**. In the rightmost field, select **Read**.
|
||||||
|
10. Next look at **Zone Resources**. Under this inscription there is a line with two fields. The left must have **Include** and the right must have **Specific Zone**. Once you select Specific Zone, another field appears on the right. Choose your domain in it.
|
||||||
|
11. Flick to the bottom and press the blue **Continue to Summary** button.
|
||||||
|
12. Check if you got everything right. A similar string must be present: *Domain — DNS:Edit, Zone:Read*.
|
||||||
|
13. Click on **Create Token**.
|
||||||
|
14. We copy the created token, and save it in a reliable place (preferably in the password manager).
|
||||||
|
|
||||||
|
![Cloudflare token setup](resource:assets/images/gifs/CloudFlare.gif)
|
|
@ -0,0 +1,15 @@
|
||||||
|
### Как получить Cloudflare API Token
|
||||||
|
1. Переходим по [ссылке](https://dash.cloudflare.com/) и авторизуемся в ранее созданном аккаунте. https://dash.cloudflare.com/
|
||||||
|
В правом углу кликаем на иконку профиля (человечек в кружочке). Для мобильной версии сайта, в верхнем левом углу, нажимаем кнопку **Меню** (три горизонтальных полоски), в выпавшем меню, ищем пункт **My Profile**.
|
||||||
|
3. Нам предлагается на выбор, четыре категории настройки: **Preferences**, **Authentication**, **API Tokens**, **Sessions**. Выбираем **API Tokens**.
|
||||||
|
4. Самым первым пунктом видим кнопку **Create Token**. С полной уверенностью в себе и желанием обрести приватность, нажимаем на неё.
|
||||||
|
5. Спускаемся в самый низ и видим поле **Create Custom Token** и кнопку **Get Started** с правой стороны. Нажимаем.
|
||||||
|
6. В поле **Token Name** даём своему токену имя. Можете покреативить и отнестись к этому как к наименованию домашнего зверька :)
|
||||||
|
7. Далее, у нас **Permissions**. В первом поле выбираем Zone. Во втором поле, по центру, выбираем **DNS**. В последнем поле выбираем **Edit**.
|
||||||
|
8. Далее смотрим на **Zone Resources**. Под этой надписью есть строка с двумя полями. В первом должно быть **Include**, а во втором — **Specific Zone**. Как только Вы выберите **Specific Zone**, справа появится ещё одно поле. В нём выбираем наш домен.
|
||||||
|
9. Листаем в самый низ и нажимаем на синюю кнопку **Continue to Summary**.
|
||||||
|
10. Проверяем, всё ли мы правильно выбрали. Должна присутствовать подобная строка: ваш.домен — **DNS:Edit, Zone:Read**.
|
||||||
|
11. Нажимаем **Create Token**.
|
||||||
|
12. Копируем созданный токен, и сохраняем его в надёжном месте (желательно — в менеджере паролей).
|
||||||
|
|
||||||
|
![Cloudflare token setup](resource:assets/images/gifs/CloudFlare.gif)
|
|
@ -0,0 +1,3 @@
|
||||||
|
In the next window, enter the token obtained from the console of the previous version of the application.
|
||||||
|
|
||||||
|
Enter it without the word *Bearer*.
|
|
@ -0,0 +1,3 @@
|
||||||
|
Введите в следующем окне токен, полученный из консоли прошлой версии приложения.
|
||||||
|
|
||||||
|
Вводить нужно без слова *Bearer*.
|
|
@ -0,0 +1,19 @@
|
||||||
|
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cat /etc/nixos/userdata/tokens.json
|
||||||
|
```
|
||||||
|
|
||||||
|
This file will have a similar construction:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tokens": [
|
||||||
|
{
|
||||||
|
"token": "token_to_copy",
|
||||||
|
"name": "device_name",
|
||||||
|
"date": "date"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the token from the file and paste it in the next window.
|
|
@ -0,0 +1,19 @@
|
||||||
|
Войдите как root пользователь на свой сервер и посмотрите содержимое файла `/etc/nixos/userdata/tokens.json`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cat /etc/nixos/userdata/tokens.json
|
||||||
|
```
|
||||||
|
|
||||||
|
В этом файле будет схожая конструкция:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tokens": [
|
||||||
|
{
|
||||||
|
"token": "токен_который_надо_скопировать",
|
||||||
|
"name": "имя_устройства",
|
||||||
|
"date": "дата"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Скопируйте токен из файла и вставьте в следующем окне.
|
|
@ -0,0 +1,26 @@
|
||||||
|
In the Hetzner server control panel, go to the **Rescue** tab. Then, click on **Enable rescue & power cycle**.
|
||||||
|
|
||||||
|
In *Choose a Recue OS* select **linux64**, and in *SSH Key* select your key if it has been added to your Hetzner account.
|
||||||
|
|
||||||
|
Click **Enable rescue & power cycle** and wait for the server to reboot. The login and password will be displayed on the screen. Login to the root user using your login and password information.
|
||||||
|
|
||||||
|
Mount your server file system and see the contents of the token file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mount /dev/sda1 /mnt
|
||||||
|
cat /mnt/etc/nixos/userdata/tokens.json
|
||||||
|
```
|
||||||
|
|
||||||
|
This file will have a similar construction:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tokens": [
|
||||||
|
{
|
||||||
|
"token": "token_to_copy",
|
||||||
|
"name": "device_name",
|
||||||
|
"date": "date"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the token from the file and paste it in the next window.
|
|
@ -0,0 +1,26 @@
|
||||||
|
В панели управления сервером в Hetzner перейдите во вкладку **Rescue**. Затем, нажмите на кнопку **Enable rescue & power cycle**.
|
||||||
|
|
||||||
|
В поле *Choose a Recue OS* выберите **linux64**, а в *SSH Key* свой ключ, если он был добавлен в ваш аккаунт Hetzner.
|
||||||
|
|
||||||
|
Нажмите **Enable rescue & power cycle** и подождите перезагрузки сервера. На экране будет отображён пароль для входа. Войдите в root пользователя используя данные логин и пароль.
|
||||||
|
|
||||||
|
Примонтируйте файловую систему вашего сервера и посмотрите содержимое файла с токенами:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mount /dev/sda1 /mnt
|
||||||
|
cat /mnt/etc/nixos/userdata/tokens.json
|
||||||
|
```
|
||||||
|
|
||||||
|
В этом файле будет схожая конструкция:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tokens": [
|
||||||
|
{
|
||||||
|
"token": "токен_который_надо_скопировать",
|
||||||
|
"name": "имя_устройства",
|
||||||
|
"date": "дата"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Скопируйте токен из файла и вставьте в следующем окне.
|
|
@ -1,3 +1,4 @@
|
||||||
|
### How to get Hetzner API Token
|
||||||
1. Visit the following [link](https://console.hetzner.cloud/) and sign
|
1. Visit the following [link](https://console.hetzner.cloud/) and sign
|
||||||
into newly created account.
|
into newly created account.
|
||||||
2. Enter into previously created project. If you haven't created one,
|
2. Enter into previously created project. If you haven't created one,
|
||||||
|
@ -17,4 +18,6 @@
|
||||||
**permissions**. Pick **Read & Write**.
|
**permissions**. Pick **Read & Write**.
|
||||||
8. Click **Generate API Token.**
|
8. Click **Generate API Token.**
|
||||||
9. After that, our key will be shown. Store it in the reliable place,
|
9. After that, our key will be shown. Store it in the reliable place,
|
||||||
or in the password manager, which is better.
|
or in the password manager, which is better.
|
||||||
|
|
||||||
|
![Hetzner token setup](resource:assets/images/gifs/Hetzner.gif)
|
||||||
|
|
|
@ -4,4 +4,6 @@
|
||||||
3. Наводим мышкой на боковую панель. Она должна раскрыться, показав нам пункты меню. Нас интересует последний — Security (с иконкой ключика).
|
3. Наводим мышкой на боковую панель. Она должна раскрыться, показав нам пункты меню. Нас интересует последний — Security (с иконкой ключика).
|
||||||
4. Далее, в верхней части интерфейса видим примерно такой список: SSH Keys, API Tokens, Certificates, Members. Нам нужен API Tokens. Переходим по нему.
|
4. Далее, в верхней части интерфейса видим примерно такой список: SSH Keys, API Tokens, Certificates, Members. Нам нужен API Tokens. Переходим по нему.
|
||||||
5. В правой части интерфейса, нас будет ожидать кнопка Generate API token. Если же Вы используете мобильную версию сайта, в нижнем правом углу Вы увидите красный плюсик. Нажимаем на эту кнопку.
|
5. В правой части интерфейса, нас будет ожидать кнопка Generate API token. Если же Вы используете мобильную версию сайта, в нижнем правом углу Вы увидите красный плюсик. Нажимаем на эту кнопку.
|
||||||
6. В поле Description, даём нашему токену название (это может быть любое название, которые Вам нравиться. Сути оно не меняет.
|
6. В поле Description, даём нашему токену название (это может быть любое название, которые Вам нравиться. Сути оно не меняет.
|
||||||
|
|
||||||
|
![Hetzner token setup](resource:assets/images/gifs/Hetzner.gif)
|
||||||
|
|
|
@ -21,13 +21,15 @@
|
||||||
"saving": "Saving..",
|
"saving": "Saving..",
|
||||||
"nickname": "Nickname",
|
"nickname": "Nickname",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"later": "I will setup it later",
|
"later": "Skip to setup later",
|
||||||
|
"connect_to_existing": "Connect to an existing server",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
"no_data": "No data",
|
"no_data": "No data",
|
||||||
"wait": "Wait",
|
"wait": "Wait",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"apply": "Apply"
|
"apply": "Apply",
|
||||||
|
"done": "Done"
|
||||||
},
|
},
|
||||||
"more": {
|
"more": {
|
||||||
"_comment": "'More' tab",
|
"_comment": "'More' tab",
|
||||||
|
@ -35,7 +37,7 @@
|
||||||
"about_project": "About us",
|
"about_project": "About us",
|
||||||
"about_app": "About application",
|
"about_app": "About application",
|
||||||
"onboarding": "Onboarding",
|
"onboarding": "Onboarding",
|
||||||
"create_ssh_key": "Create ssh key",
|
"create_ssh_key": "Create SSH key",
|
||||||
"generate_key": "Generate key",
|
"generate_key": "Generate key",
|
||||||
"generate_key_text": "You can generate ssh key",
|
"generate_key_text": "You can generate ssh key",
|
||||||
"console": "Console",
|
"console": "Console",
|
||||||
|
@ -62,6 +64,20 @@
|
||||||
"6": "This removes the Server. It will be no longer accessible"
|
"6": "This removes the Server. It will be no longer accessible"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ssh": {
|
||||||
|
"title": "SSH keys",
|
||||||
|
"create": "Create SSH key",
|
||||||
|
"delete": "Delete SSH key",
|
||||||
|
"delete_confirm_question": "Are you sure you want to delete SSH key?",
|
||||||
|
"subtitle_with_keys": "{} keys",
|
||||||
|
"subtitle_without_keys": "No keys",
|
||||||
|
"no_key_name": "Unnamed key",
|
||||||
|
"root": {
|
||||||
|
"title": "These are superuser keys",
|
||||||
|
"subtitle": "Owners of these keys get full access to the server and can do anything on it. Only add your own keys to the server."
|
||||||
|
},
|
||||||
|
"input_label": "Public ED25519 or RSA key"
|
||||||
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"_comment": "Onboarding pages",
|
"_comment": "Onboarding pages",
|
||||||
"page1_title": "Digital independence, available to all of us",
|
"page1_title": "Digital independence, available to all of us",
|
||||||
|
@ -91,6 +107,38 @@
|
||||||
"status": "Status — Good",
|
"status": "Status — Good",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "It's your personal internet address that will point to the server and other services of yours."
|
"1": "It's your personal internet address that will point to the server and other services of yours."
|
||||||
|
},
|
||||||
|
"screen_title": "Domain and DNS",
|
||||||
|
"states": {
|
||||||
|
"ok": "Records are OK",
|
||||||
|
"error": "Problems found",
|
||||||
|
"error_subtitle": "Tap here to fix them",
|
||||||
|
"refreshing": "Refreshing status...",
|
||||||
|
"uninitialized": "Data is not retrieved yet"
|
||||||
|
},
|
||||||
|
"record_description": {
|
||||||
|
"root": "Root domain",
|
||||||
|
"api": "SelfPrivacy API",
|
||||||
|
"cloud": "File cloud",
|
||||||
|
"git": "Git server",
|
||||||
|
"meet": "Video conference",
|
||||||
|
"social": "Social network",
|
||||||
|
"password": "Password manager",
|
||||||
|
"vpn": "VPN",
|
||||||
|
"mx": "MX record",
|
||||||
|
"dmarc": "DMARC record",
|
||||||
|
"spf": "SPF record",
|
||||||
|
"dkim": "DKIM key"
|
||||||
|
},
|
||||||
|
"cards": {
|
||||||
|
"services": {
|
||||||
|
"title": "Services",
|
||||||
|
"subtitle": "Type “A” records required for each service."
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"title": "Email",
|
||||||
|
"subtitle": "Records necessary for secure email exchange."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
|
@ -99,14 +147,29 @@
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "Will save your day in case of incident: hackers attack, server deletion, etc.",
|
"1": "Will save your day in case of incident: hackers attack, server deletion, etc.",
|
||||||
"2": "3Gb/10Gb, last backup was yesterday {}"
|
"2": "3Gb/10Gb, last backup was yesterday {}"
|
||||||
}
|
},
|
||||||
|
"reuploadKey": "Force reupload key",
|
||||||
|
"reuploadedKey": "Key reuploaded",
|
||||||
|
"initialize": "Initialize",
|
||||||
|
"waitingForRebuild": "You will be able to create your first backup in a few minutes.",
|
||||||
|
"restore": "Restore from backup",
|
||||||
|
"no_backups": "There are no backups yet",
|
||||||
|
"create_new": "Create a new backup",
|
||||||
|
"creating": "Creating a new backup: {}%",
|
||||||
|
"restoring": "Restoring from backup",
|
||||||
|
"error_pending": "Server returned error, check it below",
|
||||||
|
"restore_alert": "You are about to restore from backup created on {}. All current data will be lost. Are you sure?",
|
||||||
|
"refresh": "Refresh status",
|
||||||
|
"refetchBackups": "Refetch backup list",
|
||||||
|
"refetchingList": "In a few minutes list will be updated"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"not_ready_card": {
|
"not_ready_card": {
|
||||||
"_comment": "Card shown when user skips initial setup",
|
"_comment": "Card shown when user skips initial setup",
|
||||||
"1": "Please finish application setup using ",
|
"1": "Please finish application setup using ",
|
||||||
"2": "@:more.configuration_wizard",
|
"2": "@:more.configuration_wizard",
|
||||||
"3": " for further work"
|
"3": " for further work",
|
||||||
|
"in_menu": "Server is not set up yet. Please finish setup using setup wizard for further work."
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"_comment": "Вкладка сервисы",
|
"_comment": "Вкладка сервисы",
|
||||||
|
@ -114,6 +177,7 @@
|
||||||
"mail": {
|
"mail": {
|
||||||
"title": "E-Mail",
|
"title": "E-Mail",
|
||||||
"subtitle": "E-Mail for company and family.",
|
"subtitle": "E-Mail for company and family.",
|
||||||
|
"login_info": "Use username and password from users tab. IMAP port is 143 with STARTTLS, SMTP port is 587 with STARTTLS.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "To connect to the mailserver, please use {} domain alongside with username and password, that you created. Also feel free to invite",
|
"1": "To connect to the mailserver, please use {} domain alongside with username and password, that you created. Also feel free to invite",
|
||||||
"2": "new users"
|
"2": "new users"
|
||||||
|
@ -122,6 +186,7 @@
|
||||||
"messenger": {
|
"messenger": {
|
||||||
"title": "Messenger",
|
"title": "Messenger",
|
||||||
"subtitle": "Telegram or Signal not so private as Delta.Chat that uses your private server.",
|
"subtitle": "Telegram or Signal not so private as Delta.Chat that uses your private server.",
|
||||||
|
"login_info": "Use the same username and password as for e-mail.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "For connection, please use {} domain and credentials that you created."
|
"1": "For connection, please use {} domain and credentials that you created."
|
||||||
}
|
}
|
||||||
|
@ -129,6 +194,7 @@
|
||||||
"password_manager": {
|
"password_manager": {
|
||||||
"title": "Password Manager",
|
"title": "Password Manager",
|
||||||
"subtitle": "Base of your security. Bitwarden will help you to create, store and move passwords between devices, as well as input them, when requested using autocompletion.",
|
"subtitle": "Base of your security. Bitwarden will help you to create, store and move passwords between devices, as well as input them, when requested using autocompletion.",
|
||||||
|
"login_info": "You will have to create an account on the website.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "You can connect to the service and create a user via this link:"
|
"1": "You can connect to the service and create a user via this link:"
|
||||||
}
|
}
|
||||||
|
@ -136,6 +202,7 @@
|
||||||
"video": {
|
"video": {
|
||||||
"title": "Videomeet",
|
"title": "Videomeet",
|
||||||
"subtitle": "Zoom and Google Meet are good, but Jitsi Meet is a worth alternative that also gives you confidence that you're not being listened.",
|
"subtitle": "Zoom and Google Meet are good, but Jitsi Meet is a worth alternative that also gives you confidence that you're not being listened.",
|
||||||
|
"login_info": "No account needed.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "Using Jitsi as simple as just visiting this link:"
|
"1": "Using Jitsi as simple as just visiting this link:"
|
||||||
}
|
}
|
||||||
|
@ -143,6 +210,7 @@
|
||||||
"cloud": {
|
"cloud": {
|
||||||
"title": "Cloud Storage",
|
"title": "Cloud Storage",
|
||||||
"subtitle": "Do not allow cloud services to read your data by using NextCloud.",
|
"subtitle": "Do not allow cloud services to read your data by using NextCloud.",
|
||||||
|
"login_info": "Login is admin, password is the same as with your main user. Create new accounts in Nextcloud interface.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "You can connect and create a new user here:"
|
"1": "You can connect and create a new user here:"
|
||||||
}
|
}
|
||||||
|
@ -150,6 +218,7 @@
|
||||||
"social_network": {
|
"social_network": {
|
||||||
"title": "Social Network",
|
"title": "Social Network",
|
||||||
"subtitle": "It's hard to believe, but it became possible to create your own social network, with your own rules and target audience.",
|
"subtitle": "It's hard to believe, but it became possible to create your own social network, with your own rules and target audience.",
|
||||||
|
"login_info": "You will have to create an account on the website.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "You can connect and create new social user here:"
|
"1": "You can connect and create new social user here:"
|
||||||
}
|
}
|
||||||
|
@ -157,6 +226,7 @@
|
||||||
"git": {
|
"git": {
|
||||||
"title": "Git Server",
|
"title": "Git Server",
|
||||||
"subtitle": "Private alternative to the Github, that belongs to you, but not a Microsoft.",
|
"subtitle": "Private alternative to the Github, that belongs to you, but not a Microsoft.",
|
||||||
|
"login_info": "You will have to create an account on the website. First user will become an admin.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "You can connect and create a new user here:"
|
"1": "You can connect and create a new user here:"
|
||||||
}
|
}
|
||||||
|
@ -182,12 +252,12 @@
|
||||||
"delete_confirm_question": "Are you sure?",
|
"delete_confirm_question": "Are you sure?",
|
||||||
"reset_password": "Reset password",
|
"reset_password": "Reset password",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"send_regisration_data": "Share login credentials"
|
"send_registration_data": "Share login credentials"
|
||||||
},
|
},
|
||||||
"initializing": {
|
"initializing": {
|
||||||
"_comment": "initializing page",
|
"_comment": "initializing page",
|
||||||
"1": "Connect a server",
|
"1": "Connect a server",
|
||||||
"2": "Here, your data and SelfPrivacy services wiil reside",
|
"2": "A place where your data and SelfPrivacy services will reside:",
|
||||||
"how": "How to obtain API token",
|
"how": "How to obtain API token",
|
||||||
"3": "Connect CloudFlare",
|
"3": "Connect CloudFlare",
|
||||||
"4": "To manage your domain's DNS",
|
"4": "To manage your domain's DNS",
|
||||||
|
@ -205,7 +275,6 @@
|
||||||
"15": "Server created. DNS checks and server boot in progress...",
|
"15": "Server created. DNS checks and server boot in progress...",
|
||||||
"16": "Until the next check: ",
|
"16": "Until the next check: ",
|
||||||
"17": "Check",
|
"17": "Check",
|
||||||
"18": "How to obtain Hetzner API Token",
|
|
||||||
"19": "1 Go via this link ",
|
"19": "1 Go via this link ",
|
||||||
"20": "\n",
|
"20": "\n",
|
||||||
"21": "One more restart to apply your security certificates.",
|
"21": "One more restart to apply your security certificates.",
|
||||||
|
@ -214,6 +283,94 @@
|
||||||
"finish": "Everything is initialized",
|
"finish": "Everything is initialized",
|
||||||
"checks": "Checks have been completed \n{} ouf of {}"
|
"checks": "Checks have been completed \n{} ouf of {}"
|
||||||
},
|
},
|
||||||
|
"recovering": {
|
||||||
|
"recovery_main_header": "Connect to an existing server",
|
||||||
|
"domain_recovery_description": "Enter a server domain you want to get access for:",
|
||||||
|
"domain_recover_placeholder": "Your domain",
|
||||||
|
"domain_recover_error": "Server with such domain was not found",
|
||||||
|
"method_select_description": "Select a recovery method:",
|
||||||
|
"method_select_other_device": "I have access on another device",
|
||||||
|
"method_select_recovery_key": "I have a recovery key",
|
||||||
|
"method_select_nothing": "I don't have any of that",
|
||||||
|
"method_device_description": "Open the application on another device, then go to the devices page. Press \"Add device\" to receive your token.",
|
||||||
|
"method_device_button": "I have received my token",
|
||||||
|
"method_device_input_description": "Enter your authorization token",
|
||||||
|
"method_device_input_placeholder": "Token",
|
||||||
|
"method_recovery_input_description": "Enter your recovery key",
|
||||||
|
"fallback_select_description": "What exactly do you have? Pick the first available option:",
|
||||||
|
"fallback_select_token_copy": "Copy of auth token from other version of the application.",
|
||||||
|
"fallback_select_root_ssh": "Root SSH access to the server.",
|
||||||
|
"fallback_select_provider_console": "Access to the server console of my prodiver.",
|
||||||
|
"authorization_failed": "Couldn't log in with this key",
|
||||||
|
"fallback_select_provider_console_hint": "For example: Hetzner.",
|
||||||
|
"hetzner_connected": "Connect to Hetzner",
|
||||||
|
"hetzner_connected_description": "Communication established. Enter Hetzner token with access to {}:",
|
||||||
|
"hetzner_connected_placeholder": "Hetzner token",
|
||||||
|
"confirm_server": "Confirm server",
|
||||||
|
"confirm_server_description": "Found your server! Confirm it is correct.",
|
||||||
|
"confirm_server_accept": "Yes! That's it",
|
||||||
|
"confirm_server_decline": "Choose a different server",
|
||||||
|
"choose_server": "Choose your server",
|
||||||
|
"choose_server_description": "We couldn't figure out which server your are trying to connect to.",
|
||||||
|
"no_servers": "There is no available servers on your account.",
|
||||||
|
"domain_not_available_on_token": "Selected domain is not available on this token.",
|
||||||
|
"modal_confirmation_title": "Is it really your server?",
|
||||||
|
"modal_confirmation_description": "If you connect to a wrong server you may lose all your data.",
|
||||||
|
"modal_confirmation_dns_valid": "Reverse DNS is valid",
|
||||||
|
"modal_confirmation_dns_invalid": "Reverse DNS points to another domain",
|
||||||
|
"modal_confirmation_ip_valid": "IP is the same as in DNS record",
|
||||||
|
"modal_confirmation_ip_invalid": "IP is not the same as in DNS record",
|
||||||
|
"confirm_cloudflare": "Connect to CloudFlare",
|
||||||
|
"confirm_cloudflare_description": "Enter a Cloudflare token with access to {}:",
|
||||||
|
"confirm_backblaze": "Connect to Backblaze",
|
||||||
|
"confirm_backblaze_description": "Enter a Backblaze token with access to backup storage:"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"main_screen": {
|
||||||
|
"header": "Devices",
|
||||||
|
"description": "These devices have full access to the server via SelfPrivacy app.",
|
||||||
|
"this_device": "This device",
|
||||||
|
"other_devices": "Other devices",
|
||||||
|
"authorize_new_device": "Authorize new device",
|
||||||
|
"access_granted_on" : "Access granted on {}",
|
||||||
|
"tip": "Press on the device to revoke access."
|
||||||
|
},
|
||||||
|
"add_new_device_screen": {
|
||||||
|
"header": "Authorizing new device",
|
||||||
|
"description": "Enter the key on the device you want to authorize:",
|
||||||
|
"please_wait": "Please wait",
|
||||||
|
"tip": "The key is valid for 10 minutes.",
|
||||||
|
"expired": "The key has expired.",
|
||||||
|
"get_new_key": "Get new key"
|
||||||
|
},
|
||||||
|
"revoke_device_alert": {
|
||||||
|
"header": "Revoke access?",
|
||||||
|
"description": "The device {} will no longer have access to the server.",
|
||||||
|
"yes": "Revoke",
|
||||||
|
"no": "Cancel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"recovery_key": {
|
||||||
|
"key_connection_error": "Couldn't connect to the server.",
|
||||||
|
"key_synchronizing": "Synchronizing...",
|
||||||
|
"key_main_header": "Recovery key",
|
||||||
|
"key_main_description": "Is needed for SelfPrivacy authorization when all your other authorized devices aren't available.",
|
||||||
|
"key_amount_toggle": "Limit by number of uses",
|
||||||
|
"key_amount_field_title": "Max number of uses",
|
||||||
|
"key_duedate_toggle": "Limit by time",
|
||||||
|
"key_duedate_field_title": "Due date of expiration",
|
||||||
|
"key_receive_button": "Receive key",
|
||||||
|
"key_valid": "Your key is valid",
|
||||||
|
"key_invalid": "Your key is no longer valid",
|
||||||
|
"key_valid_until": "Valid until {}",
|
||||||
|
"key_valid_for": "Valid for {} uses",
|
||||||
|
"key_creation_date": "Created on {}",
|
||||||
|
"key_replace_button": "Generate new key",
|
||||||
|
"key_receiving_description": "Write down this key and put to a safe place. It is used to restore full access to your server:",
|
||||||
|
"key_receiving_info": "The key will never ever be shown again, but you will be able to replace it with another one.",
|
||||||
|
"key_receiving_done": "Done!",
|
||||||
|
"generation_error": "Couldn't generate a recovery key. {}"
|
||||||
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"_comment": "messages in modals",
|
"_comment": "messages in modals",
|
||||||
"1": "Server with such name, already exist",
|
"1": "Server with such name, already exist",
|
||||||
|
@ -223,7 +380,11 @@
|
||||||
"5": "Yes, purge all my tokens",
|
"5": "Yes, purge all my tokens",
|
||||||
"6": "Delete the server and volume?",
|
"6": "Delete the server and volume?",
|
||||||
"7": "Yes",
|
"7": "Yes",
|
||||||
"8": "Remove task"
|
"8": "Remove task",
|
||||||
|
"9": "Reboot",
|
||||||
|
"10": "You cannot use this API for domains with such TLD.",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No"
|
||||||
},
|
},
|
||||||
"timer": {
|
"timer": {
|
||||||
"sec": "{} sec"
|
"sec": "{} sec"
|
||||||
|
@ -233,18 +394,30 @@
|
||||||
"title": "Jobs list",
|
"title": "Jobs list",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"empty": "No jobs",
|
"empty": "No jobs",
|
||||||
"createUser": "Create",
|
"createUser": "Create user",
|
||||||
|
"deleteUser": "Delete user",
|
||||||
"serviceTurnOff": "Turn off",
|
"serviceTurnOff": "Turn off",
|
||||||
"serviceTurnOn": "Turn on",
|
"serviceTurnOn": "Turn on",
|
||||||
"jobAdded": "Job added",
|
"jobAdded": "Job added",
|
||||||
"runJobs": "Run jobs"
|
"runJobs": "Run jobs",
|
||||||
|
"rebootSuccess": "Server is rebooting",
|
||||||
|
"rebootFailed": "Couldn't reboot the server. Check the app logs.",
|
||||||
|
"configPullFailed": "Failed to pull configuration upgrade. Started software upgrade anyways.",
|
||||||
|
"upgradeSuccess": "Server upgrade started",
|
||||||
|
"upgradeFailed": "Failed to upgrade server",
|
||||||
|
"upgradeServer": "Upgrade server",
|
||||||
|
"rebootServer": "Reboot server",
|
||||||
|
"create_ssh_key": "Create SSH key for {}",
|
||||||
|
"delete_ssh_key": "Delete SSH key for {}"
|
||||||
},
|
},
|
||||||
"validations": {
|
"validations": {
|
||||||
"required": "Required",
|
"required": "Required.",
|
||||||
"invalid_format": "Invalid format",
|
"invalid_format": "Invalid format.",
|
||||||
"root_name": "User name cannot be 'root'",
|
"root_name": "User name cannot be 'root'.",
|
||||||
"key_format": "Invalid key format",
|
"key_format": "Invalid key format.",
|
||||||
"length": "Length is [] shoud be {}",
|
"length_not_equal": "Length is []. Should be {}.",
|
||||||
"user_alredy_exist": "Already exists"
|
"length_longer": "Length is []. Should be shorter than or equal to {}.",
|
||||||
|
"user_already_exist": "This user already exists.",
|
||||||
|
"key_already_exists": "This key already exists."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,16 +18,18 @@
|
||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
"connect": "Подключить",
|
"connect": "Подключить",
|
||||||
"domain": "Домен",
|
"domain": "Домен",
|
||||||
"saving": "Сохранение..",
|
"saving": "Сохранение…",
|
||||||
"nickname": "Никнейм",
|
"nickname": "Никнейм",
|
||||||
"loading": "Загрузка",
|
"loading": "Загрузка",
|
||||||
"later": "Настрою потом",
|
"later": "Пропустить и настроить потом",
|
||||||
|
"connect_to_existing": "Подключиться к существующему серверу",
|
||||||
"reset": "Сбросить",
|
"reset": "Сбросить",
|
||||||
"details": "Детальная информация",
|
"details": "Детальная информация",
|
||||||
"no_data": "Нет данных",
|
"no_data": "Нет данных",
|
||||||
"wait": "Загрузка",
|
"wait": "Загрузка",
|
||||||
"remove": "Удалить",
|
"remove": "Удалить",
|
||||||
"apply": "Подать"
|
"apply": "Подать",
|
||||||
|
"done": "Готово"
|
||||||
},
|
},
|
||||||
"more": {
|
"more": {
|
||||||
"_comment": "вкладка ещё",
|
"_comment": "вкладка ещё",
|
||||||
|
@ -43,11 +45,11 @@
|
||||||
"enable": "Включить",
|
"enable": "Включить",
|
||||||
"ok": "ok",
|
"ok": "ok",
|
||||||
"continue": "Продолжить",
|
"continue": "Продолжить",
|
||||||
"ssh_key_exist_text": "У Вас уже есть сгенерированный ssk ключ",
|
"ssh_key_exist_text": "У Вас уже есть сгенерированный ssh ключ",
|
||||||
"yes_delete": "Да, удалить",
|
"yes_delete": "Да, удалить",
|
||||||
"share": "Поделиться",
|
"share": "Поделиться",
|
||||||
"copy_buffer": "Копировать в буфер",
|
"copy_buffer": "Копировать в буфер",
|
||||||
"copied_ssh": "SSH копировано в буфер",
|
"copied_ssh": "SSH ключ cкопирован в буфер",
|
||||||
"delete_ssh_text": "Удалить SSH ключ?",
|
"delete_ssh_text": "Удалить SSH ключ?",
|
||||||
"about_app_page": {
|
"about_app_page": {
|
||||||
"text": "Версия приложения: v.{}"
|
"text": "Версия приложения: v.{}"
|
||||||
|
@ -59,9 +61,23 @@
|
||||||
"3": "Сброс настроек",
|
"3": "Сброс настроек",
|
||||||
"4": "Сбросить API ключи а также root пользвателя.",
|
"4": "Сбросить API ключи а также root пользвателя.",
|
||||||
"5": "Удалить сервер",
|
"5": "Удалить сервер",
|
||||||
"6": "Действие приведет к удалению сервера. После чего он не будет доступен."
|
"6": "Действие приведет к удалению сервера. После этого он будет недоступен."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ssh": {
|
||||||
|
"title": "SSH ключи",
|
||||||
|
"create": "Добавить SSH ключ",
|
||||||
|
"delete": "Удалить SSH ключ",
|
||||||
|
"delete_confirm_question": "Вы уверены что хотите удалить следующий ключ?",
|
||||||
|
"subtitle_with_keys": "Ключей: {}",
|
||||||
|
"subtitle_without_keys": "Ключей нет",
|
||||||
|
"no_key_name": "Безымянный ключ",
|
||||||
|
"root": {
|
||||||
|
"title": "Это ключи суперпользователя",
|
||||||
|
"subtitle": "Владельцы указанных здесь ключей получают полный доступ к данным и настройкам сервера. Добавляйте исключительно свои ключи."
|
||||||
|
},
|
||||||
|
"input_label": "Публичный ED25519 или RSA ключ"
|
||||||
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"_comment": "страницы онбординга",
|
"_comment": "страницы онбординга",
|
||||||
"page1_title": "Цифровая независимость доступна каждому",
|
"page1_title": "Цифровая независимость доступна каждому",
|
||||||
|
@ -91,22 +107,70 @@
|
||||||
"status": "Статус — в норме",
|
"status": "Статус — в норме",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "Это ваш личный адрес в интернете, который будет указывать на сервер и другие ваши сервисы."
|
"1": "Это ваш личный адрес в интернете, который будет указывать на сервер и другие ваши сервисы."
|
||||||
|
},
|
||||||
|
"screen_title": "Домен и DNS",
|
||||||
|
"states": {
|
||||||
|
"ok": "Записи в норме",
|
||||||
|
"error": "Обнаружены проблемы",
|
||||||
|
"error_subtitle": "Нажмите здесь, чтобы исправить",
|
||||||
|
"refreshing": "Обновление данных...",
|
||||||
|
"uninitialized": "Данные ещё не получены"
|
||||||
|
},
|
||||||
|
"record_description": {
|
||||||
|
"root": "Корневой домен",
|
||||||
|
"api": "SelfPrivacy API",
|
||||||
|
"cloud": "Файловое облако",
|
||||||
|
"git": "Git сервер",
|
||||||
|
"meet": "Видеоконференции",
|
||||||
|
"social": "Социальная сеть",
|
||||||
|
"password": "Менеджер паролей",
|
||||||
|
"vpn": "VPN",
|
||||||
|
"mx": "MX запись",
|
||||||
|
"dmarc": "DMARC запись",
|
||||||
|
"spf": "SPF запись",
|
||||||
|
"dkim": "DKIM ключ"
|
||||||
|
},
|
||||||
|
"cards": {
|
||||||
|
"services": {
|
||||||
|
"title": "Сервисы",
|
||||||
|
"subtitle": "Записи типа “A” необходимые для работы сервисов."
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"title": "Электронная почта",
|
||||||
|
"subtitle": "Записи необходимые для безопасного обмена электронной почтой."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
"card_title": "Резервное копирование",
|
"card_title": "Резервное копирование",
|
||||||
"status": "Статус — в норме",
|
"status": "Статус — в норме",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "Выручит Вас в любой ситуации: хакерская атака, удаление сервера и.т.д.",
|
"1": "Выручит Вас в любой ситуации: хакерская атака, удаление сервера и т.д.",
|
||||||
"2": "Использовано 3Gb из бестплатых 10Gb. Последнее копирование была сделана вчера в {}."
|
"2": "Использовано 3Gb из бесплатых 10Gb. Последнее копирование была сделано вчера в {}."
|
||||||
}
|
},
|
||||||
|
"reuploadKey": "Принудительно обновить ключ",
|
||||||
|
"reuploadedKey": "Ключ на сервере обновлён",
|
||||||
|
"initialize": "Настроить",
|
||||||
|
"waitingForRebuild": "Через несколько минут можно будет создать первую копию.",
|
||||||
|
"restore": "Восстановить из копии",
|
||||||
|
"no_backups": "Резервных копий пока нет",
|
||||||
|
"create_new": "Создать новую копию",
|
||||||
|
"creating": "Создание копии: {}%",
|
||||||
|
"restoring": "Восстановление из копии",
|
||||||
|
"error_pending": "Сервер вернул ошибку: проверьте её ниже.",
|
||||||
|
"restore_alert": "Вы собираетесь восстановить из копии созданной {}. Все текущие данные будут потеряны. Вы уверены?",
|
||||||
|
"refresh": "Обновить статус",
|
||||||
|
"refetchBackups": "Обновить список копий",
|
||||||
|
"refetchingList": "Через несколько минут список будет обновлён"
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"not_ready_card": {
|
"not_ready_card": {
|
||||||
"_comment": "Карточка показывающая когда человек скипнул настройку, на карте текст из 3 блоков, средний содержит ссыку на мастер подключения",
|
"_comment": "Карточка показывающая когда человек скипнул настройку, на карте текст из 3 блоков, средний содержит ссыку на мастер подключения",
|
||||||
"1": "Завершите настройку приложения используя ",
|
"1": "Завершите настройку приложения используя ",
|
||||||
"2": "@:more.configuration_wizard",
|
"2": "@:more.configuration_wizard",
|
||||||
"3": " для продолжения работы"
|
"3": " для продолжения работы",
|
||||||
|
"in_menu": "Сервер ещё не настроен, воспользуйтесь мастером подключения."
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"_comment": "Вкладка сервисы",
|
"_comment": "Вкладка сервисы",
|
||||||
|
@ -114,6 +178,7 @@
|
||||||
"mail": {
|
"mail": {
|
||||||
"title": "Почта",
|
"title": "Почта",
|
||||||
"subtitle": "Электронная почта для семьи или компании.",
|
"subtitle": "Электронная почта для семьи или компании.",
|
||||||
|
"login_info": "Используйте логин и пароль из вкладки пользователей. IMAP порт: 143, STARTTLS. SMTP порт: 587, STARTTLS.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "Для подключения почтового ящика используйте {} и профиль, который Вы создали. Так же приглашайте",
|
"1": "Для подключения почтового ящика используйте {} и профиль, который Вы создали. Так же приглашайте",
|
||||||
"2": "новых пользователей."
|
"2": "новых пользователей."
|
||||||
|
@ -121,21 +186,24 @@
|
||||||
},
|
},
|
||||||
"messenger": {
|
"messenger": {
|
||||||
"title": "Мессенджер",
|
"title": "Мессенджер",
|
||||||
"subtitle": "Telegram и Signal не так приватны, как Delta.Chat — который использует Ваш личный сервер.",
|
"subtitle": "Telegram и Signal не так приватны, как Delta.Chat — он использует Ваш личный сервер.",
|
||||||
|
"login_info": "Используйте те же логин и пароль, что и для почты.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "Для подключения используйте {} и логин пароль, который Вы создали."
|
"1": "Для подключения используйте {} и логин пароль, который Вы создали."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"password_manager": {
|
"password_manager": {
|
||||||
"title": "Менеджер паролей",
|
"title": "Менеджер паролей",
|
||||||
"subtitle": "Это фундамент Вашей безопасности. Создавать, хранить, копировать пароли между устройствами и вбивать их в формы поможет — Bitwarden.",
|
"subtitle": "Это фундамент Вашей безопасности. Создавать, хранить, копировать пароли между устройствами и вбивать их в формы поможет Bitwarden.",
|
||||||
|
"login_info": "Аккаунт нужно создать на сайте.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "Подключиться к серверу и создать пользователя можно по адресу:."
|
"1": "Подключиться к серверу и создать пользователя можно по адресу:."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"video": {
|
"video": {
|
||||||
"title": "Видеоконференция",
|
"title": "Видеоконференция",
|
||||||
"subtitle": "Jitsi meet — отличный аналог Zoom и Google meet который по мимо удобства ещё и гарантирует Вам защищённые высококачественные видеоконференции.",
|
"subtitle": "Jitsi meet — отличный аналог Zoom и Google meet который помимо удобства ещё и гарантирует Вам защищённые высококачественные видеоконференции.",
|
||||||
|
"login_info": "Аккаунт не требуется.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "Для использования просто перейдите по ссылке:."
|
"1": "Для использования просто перейдите по ссылке:."
|
||||||
}
|
}
|
||||||
|
@ -143,6 +211,7 @@
|
||||||
"cloud": {
|
"cloud": {
|
||||||
"title": "Файловое облако",
|
"title": "Файловое облако",
|
||||||
"subtitle": "Не позволяйте облачным сервисам просматривать ваши данные. Используйте NextCloud — надёжный дом для всех Ваших данных.",
|
"subtitle": "Не позволяйте облачным сервисам просматривать ваши данные. Используйте NextCloud — надёжный дом для всех Ваших данных.",
|
||||||
|
"login_info": "Логин администратора: admin, пароль такой же как у основного пользователя. Создавайте новых пользователей в интерфейсе администратора NextCloud.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "Подключиться к серверу и создать пользователя можно по адресу:."
|
"1": "Подключиться к серверу и создать пользователя можно по адресу:."
|
||||||
}
|
}
|
||||||
|
@ -150,6 +219,7 @@
|
||||||
"social_network": {
|
"social_network": {
|
||||||
"title": "Социальная сеть",
|
"title": "Социальная сеть",
|
||||||
"subtitle": "Сложно поверить, но стало возможным создать свою собственную социальную сеть, со своими правилами и аудиторией.",
|
"subtitle": "Сложно поверить, но стало возможным создать свою собственную социальную сеть, со своими правилами и аудиторией.",
|
||||||
|
"login_info": "Аккаунт нужно создать на сайте.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "Подключиться к серверу и создать пользователя можно по адресу:."
|
"1": "Подключиться к серверу и создать пользователя можно по адресу:."
|
||||||
}
|
}
|
||||||
|
@ -157,6 +227,7 @@
|
||||||
"git": {
|
"git": {
|
||||||
"title": "Git-сервер",
|
"title": "Git-сервер",
|
||||||
"subtitle": "Приватная альтернатива Github, которая принадлежит вам, а не Microsoft.",
|
"subtitle": "Приватная альтернатива Github, которая принадлежит вам, а не Microsoft.",
|
||||||
|
"login_info": "Аккаунт нужно создать на сайте. Первый зарегистрированный пользователь становится администратором.",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "Подключиться к серверу и создать пользователя можно по адресу:."
|
"1": "Подключиться к серверу и создать пользователя можно по адресу:."
|
||||||
}
|
}
|
||||||
|
@ -173,7 +244,7 @@
|
||||||
"_comment": "'Users' tab",
|
"_comment": "'Users' tab",
|
||||||
"add_new_user": "Добавьте первого пользователя.",
|
"add_new_user": "Добавьте первого пользователя.",
|
||||||
"new_user": "Новый пользователь",
|
"new_user": "Новый пользователь",
|
||||||
"not_ready": "Подключите сервер, домен и DNS в разделу Провайдеры, чтобы добавить первого пользователя",
|
"not_ready": "Подключите сервер, домен и DNS в разделе Провайдеры чтобы добавить первого пользователя",
|
||||||
"nobody_here": "Здесь будут отображаться пользователи.",
|
"nobody_here": "Здесь будут отображаться пользователи.",
|
||||||
"login": "Логин",
|
"login": "Логин",
|
||||||
"onboarding": "Приветствие",
|
"onboarding": "Приветствие",
|
||||||
|
@ -182,7 +253,7 @@
|
||||||
"delete_confirm_question": "Вы действительно хотите удалить учетную запись?",
|
"delete_confirm_question": "Вы действительно хотите удалить учетную запись?",
|
||||||
"reset_password": "Сбросить пароль",
|
"reset_password": "Сбросить пароль",
|
||||||
"account": "Учетная запись",
|
"account": "Учетная запись",
|
||||||
"send_regisration_data": "Поделиться реквизитами"
|
"send_registration_data": "Поделиться реквизитами"
|
||||||
},
|
},
|
||||||
"initializing": {
|
"initializing": {
|
||||||
"_comment": "initializing page",
|
"_comment": "initializing page",
|
||||||
|
@ -214,6 +285,90 @@
|
||||||
"finish": "Всё инициализировано.",
|
"finish": "Всё инициализировано.",
|
||||||
"checks": "Проверок выполнено: \n{} / {}"
|
"checks": "Проверок выполнено: \n{} / {}"
|
||||||
},
|
},
|
||||||
|
"recovering": {
|
||||||
|
"recovery_main_header": "Подключиться к существующему серверу",
|
||||||
|
"domain_recovery_description": "Введите домен, по которому вы хотите получить доступ к серверу:",
|
||||||
|
"domain_recover_placeholder": "Домен",
|
||||||
|
"domain_recover_error": "Не удалось найти сервер с таким доменом",
|
||||||
|
"method_select_description": "Выберите способ входа:",
|
||||||
|
"method_select_other_device": "У меня есть доступ на другом устройстве",
|
||||||
|
"method_select_recovery_key": "У меня есть ключ восстановления",
|
||||||
|
"method_select_nothing": "У меня ничего из этого нет",
|
||||||
|
"method_device_description": "Откройте приложение на другом устройстве и откройте экран управления устройствами. Нажмите \"Добавить устройство\" чтобы получить токен для авторизации.",
|
||||||
|
"method_device_button": "Я получил токен",
|
||||||
|
"method_device_input_description": "Введите ваш токен авторизации",
|
||||||
|
"method_device_input_placeholder": "Токен",
|
||||||
|
"method_recovery_input_description": "Введите ваш токен восстановления",
|
||||||
|
"fallback_select_description": "Что у вас из этого есть? Выберите первое, что подходит:",
|
||||||
|
"fallback_select_token_copy": "Копия токена авторизации из другой версии приложения.",
|
||||||
|
"fallback_select_root_ssh": "Root доступ к серверу по SSH.",
|
||||||
|
"fallback_select_provider_console": "Доступ к консоли хостинга.",
|
||||||
|
"authorization_failed": "Не удалось войти с этим ключом",
|
||||||
|
"fallback_select_provider_console_hint": "Например, Hetzner.",
|
||||||
|
"hetzner_connected": "Подключение к Hetzner",
|
||||||
|
"hetzner_connected_description": "Связь с сервером установлена. Введите токен Hetzner с доступом к {}:",
|
||||||
|
"hetzner_connected_placeholder": "Hetzner токен",
|
||||||
|
"confirm_server": "Подтвердите сервер",
|
||||||
|
"confirm_server_description": "Нашли сервер! Подтвердите, что это он:",
|
||||||
|
"confirm_server_accept": "Да, это он",
|
||||||
|
"confirm_server_decline": "Выбрать другой сервер",
|
||||||
|
"choose_server": "Выберите сервер",
|
||||||
|
"choose_server_description": "Не удалось определить, с каким сервером вы устанавливаете связь.",
|
||||||
|
"no_servers": "На вашем аккаунте нет доступных серверов.",
|
||||||
|
"domain_not_available_on_token": "Введённый токен не имеет доступа к нужному домену.",
|
||||||
|
"modal_confirmation_title": "Это действительно ваш сервер?",
|
||||||
|
"modal_confirmation_description": "Подключение к неправильному серверу может привести к деструктивным последствиям.",
|
||||||
|
"confirm_cloudflare": "Подключение к Cloudflare",
|
||||||
|
"confirm_cloudflare_description": "Введите токен Cloudflare, который имеет права на {}:",
|
||||||
|
"confirm_backblze": "Подключение к Backblaze",
|
||||||
|
"confirm_backblaze_description": "Введите токен Backblaze, который имеет права на хранилище резервных копий:"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"main_screen": {
|
||||||
|
"header": "Устройства",
|
||||||
|
"description": "Эти устройства имеют полный доступ к управлению сервером через приложение SelfPrivacy.",
|
||||||
|
"this_device": "Это устройство",
|
||||||
|
"other_devices": "Другие устройства",
|
||||||
|
"authorize_new_device": "Авторизовать новое устройство",
|
||||||
|
"access_granted_on" : "Доступ выдан {}",
|
||||||
|
"tip": "Нажмите на устройство, чтобы отозвать доступ."
|
||||||
|
},
|
||||||
|
"add_new_device_screen": {
|
||||||
|
"header": "Авторизация нового устройства",
|
||||||
|
"description": "Введите этот ключ на новом устройстве:",
|
||||||
|
"please_wait": "Пожалуйста, подождите",
|
||||||
|
"tip": "Ключ действителен 10 минут.",
|
||||||
|
"expired": "Срок действия ключа истёк.",
|
||||||
|
"get_new_key": "Получить новый ключ"
|
||||||
|
},
|
||||||
|
"revoke_device_alert": {
|
||||||
|
"header": "Отозвать доступ?",
|
||||||
|
"description": "Устройство {} больше не сможет управлять сервером.",
|
||||||
|
"yes": "Отозвать",
|
||||||
|
"no": "Отмена"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"recovery_key": {
|
||||||
|
"key_connection_error": "Не удалось соединиться с сервером",
|
||||||
|
"key_synchronizing": "Синхронизация...",
|
||||||
|
"key_main_header": "Ключ восстановления",
|
||||||
|
"key_main_description": "Требуется для авторизации SelfPrivacy, когда авторизованные устройства недоступны.",
|
||||||
|
"key_amount_toggle": "Ограничить использования",
|
||||||
|
"key_amount_field_title": "Макс. кол-во использований",
|
||||||
|
"key_duedate_toggle": "Ограничить срок использования",
|
||||||
|
"key_duedate_field_title": "Дата окончания срока",
|
||||||
|
"key_receive_button": "Получить ключ",
|
||||||
|
"key_valid": "Ваш ключ действителен",
|
||||||
|
"key_invalid": "Ваш ключ больше не действителен",
|
||||||
|
"key_valid_until": "Действителен до {}",
|
||||||
|
"key_valid_for": "Можно использовать ещё {} раз",
|
||||||
|
"key_creation_date": "Создан {}",
|
||||||
|
"key_replace_button": "Сгенерировать новый ключ",
|
||||||
|
"key_receiving_description": "Запишите этот ключ в безопасном месте. Он предоставляет полный доступ к вашему серверу:",
|
||||||
|
"key_receiving_info": "Этот ключ больше не будет показан, но вы сможете заменить его новым.",
|
||||||
|
"key_receiving_done": "Готово!",
|
||||||
|
"generation_error": "Не удалось сгенерировать ключ. {}"
|
||||||
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"_comment": "messages in modals",
|
"_comment": "messages in modals",
|
||||||
"1": "Сервер с таким именем уже существует",
|
"1": "Сервер с таким именем уже существует",
|
||||||
|
@ -223,7 +378,11 @@
|
||||||
"5": "Да, сбросить",
|
"5": "Да, сбросить",
|
||||||
"6": "Удалить сервер и диск?",
|
"6": "Удалить сервер и диск?",
|
||||||
"7": "Да, удалить",
|
"7": "Да, удалить",
|
||||||
"8": "Удалить задачу"
|
"8": "Удалить задачу",
|
||||||
|
"9": "Перезагрузить",
|
||||||
|
"10": "API не поддерживает домены с таким TLD.",
|
||||||
|
"yes": "Да",
|
||||||
|
"no": "Нет"
|
||||||
},
|
},
|
||||||
"timer": {
|
"timer": {
|
||||||
"sec": "{} сек"
|
"sec": "{} сек"
|
||||||
|
@ -233,18 +392,30 @@
|
||||||
"title": "Задачи",
|
"title": "Задачи",
|
||||||
"start": "Начать выполенение",
|
"start": "Начать выполенение",
|
||||||
"empty": "Пусто.",
|
"empty": "Пусто.",
|
||||||
"createUser": "Создать запись",
|
"createUser": "Создать пользователя",
|
||||||
|
"deleteUser": "Удалить пользователя",
|
||||||
"serviceTurnOff": "Остановить",
|
"serviceTurnOff": "Остановить",
|
||||||
"serviceTurnOn": "Запустить",
|
"serviceTurnOn": "Запустить",
|
||||||
"jobAdded": "Задача добавленна",
|
"jobAdded": "Задача добавленна",
|
||||||
"runJobs": "Запустите задачи"
|
"runJobs": "Запустите задачи",
|
||||||
|
"rebootSuccess": "Сервер перезагружается",
|
||||||
|
"rebootFailed": "Не удалось перезагрузить сервер, проверьте логи",
|
||||||
|
"configPullFailed": "Не удалось обновить конфигурацию сервера. Обновление ПО запущено.",
|
||||||
|
"upgradeSuccess": "Запущено обновление сервера",
|
||||||
|
"upgradeFailed": "Обновить сервер не вышло",
|
||||||
|
"upgradeServer": "Обновить сервер",
|
||||||
|
"rebootServer": "Перезагрузить сервер",
|
||||||
|
"create_ssh_key": "Создать SSH ключ для {}",
|
||||||
|
"delete_ssh_key": "Удалить SSH ключ для {}"
|
||||||
},
|
},
|
||||||
"validations": {
|
"validations": {
|
||||||
"required": "Обязательное поле.",
|
"required": "Обязательное поле.",
|
||||||
"invalid_format": "Неверный формат.",
|
"invalid_format": "Неверный формат.",
|
||||||
"root_name": "Имя пользователя не может быть'root'.",
|
"root_name": "Имя пользователя не может быть 'root'.",
|
||||||
"key_format": "Неверный формат.",
|
"key_format": "Неверный формат.",
|
||||||
"length": "Длина строки [] должна быть {}.",
|
"length_not_equal": "Длина строки []. Должно быть равно {}.",
|
||||||
"user_alredy_exist": "Имя уже используется."
|
"length_longer": "Длина строки []. Должно быть меньше либо равно {}.",
|
||||||
|
"user_already_exist": "Имя уже используется.",
|
||||||
|
"key_already_exists": "Этот ключ уже добавлен."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
- Added service management
|
||||||
|
- Fixed e-mail and VPN login issues for newly created users
|
||||||
|
- Fixed connection issues from Cisco AnyConnect VPN client
|
||||||
|
- Fixed Nextcloud connection issues for Desktop and mobile clients
|
||||||
|
- Updated Nextcloud to Nextcloud 22
|
||||||
|
- Added Ad-Hoc SSH support
|
|
@ -0,0 +1,2 @@
|
||||||
|
- Addressed SPCVE-0001 security issue
|
||||||
|
- Majorly improved SelfPrivacy REST API stability
|
|
@ -0,0 +1,2 @@
|
||||||
|
- Added backup creation, restoration and deletion possibility
|
||||||
|
- Implemented backend updates without server redeployment
|
|
@ -0,0 +1,4 @@
|
||||||
|
- Show progress bar on upgrading/restarting server
|
||||||
|
- Implemented user deletion
|
||||||
|
- Built-in documentation improvement
|
||||||
|
- Minor bug fixes
|
|
@ -0,0 +1,7 @@
|
||||||
|
- DKIM key is now deployed to DNS during server setup.
|
||||||
|
- Step 1 of server setup (DNS checks) is now faster.
|
||||||
|
- New DNS management screen: checks current records and lets recreate them if something is wrong.
|
||||||
|
- User creation and deletion is now more responsive.
|
||||||
|
- User list is now synchronized with the server.
|
||||||
|
- New SSH key management screen. SSH keys can now be uploaded for any user, including root.
|
||||||
|
- Root SSH key generation is removed, you can now upload your own keys.
|
|
@ -0,0 +1,2 @@
|
||||||
|
- Fix app trying to load user list before server was created
|
||||||
|
- Update fl_chart dependency
|
|
@ -0,0 +1,2 @@
|
||||||
|
- Fixed bug when you couldn't use special symbols in password during server creation.
|
||||||
|
- Raise target Android version to API 31 (Android 12).
|
|
@ -0,0 +1 @@
|
||||||
|
- Hotfix to make app installable on Android 12
|
|
@ -0,0 +1,6 @@
|
||||||
|
- Added support for multi-device server access from SelfPrivacy app.
|
||||||
|
- You can now create recovery token to regain the access to the server if you lose your device or the app's data.
|
||||||
|
- You can now connect to an existing server, instead of creating a new one.
|
||||||
|
- Initial support for Material Design 3 (Material You).
|
||||||
|
- App now uses your system colors on Android 12 (Material You), Windows 10 (accent color) and Linux (GTK colors).
|
||||||
|
- Minor bug fixes.
|
|
@ -1,5 +1,5 @@
|
||||||
SelfPrivacy - is a platform on your cloud hosting, that allows to deploy your own private services and control them using mobile application.
|
SelfPrivacy - is a platform on your cloud hosting, that allows to deploy your own private services and control them using mobile application.
|
||||||
To use this application, you'll be required to create accounts of different service providers. Please reffer to this manual: https://hugo.selfprivacy.org/posts/getting_started
|
To use this application, you'll be required to create accounts of different service providers. Please reffer to this manual: https://selfprivacy.org/en/second.html
|
||||||
Application will do the following things for you:
|
Application will do the following things for you:
|
||||||
1. Create your personal server
|
1. Create your personal server
|
||||||
2. Setup NixOS
|
2. Setup NixOS
|
||||||
|
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 43 KiB |
|
@ -61,4 +61,4 @@ SPEC CHECKSUMS:
|
||||||
|
|
||||||
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
||||||
|
|
||||||
COCOAPODS: 1.10.1
|
COCOAPODS: 1.11.2
|
||||||
|
|
|
@ -157,7 +157,7 @@
|
||||||
97C146E61CF9000F007C117D /* Project object */ = {
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastUpgradeCheck = 1020;
|
LastUpgradeCheck = 1300;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
97C146ED1CF9000F007C117D = {
|
97C146ED1CF9000F007C117D = {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1020"
|
LastUpgradeVersion = "1300"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|
|
@ -1,37 +1,67 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
|
||||||
|
|
||||||
class BlocAndProviderConfig extends StatelessWidget {
|
class BlocAndProviderConfig extends StatelessWidget {
|
||||||
const BlocAndProviderConfig({Key? key, this.child}) : super(key: key);
|
const BlocAndProviderConfig({final super.key, this.child});
|
||||||
|
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(final BuildContext context) {
|
||||||
var isDark = false;
|
const isDark = false;
|
||||||
var usersCubit = UsersCubit();
|
final serverInstallationCubit = ServerInstallationCubit()..load();
|
||||||
var appConfigCubit = AppConfigCubit()..load();
|
final usersCubit = UsersCubit(serverInstallationCubit);
|
||||||
var servicesCubit = ServicesCubit(appConfigCubit);
|
final servicesCubit = ServicesCubit(serverInstallationCubit);
|
||||||
|
final backupsCubit = BackupsCubit(serverInstallationCubit);
|
||||||
|
final dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit);
|
||||||
|
final recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit);
|
||||||
|
final apiDevicesCubit = ApiDevicesCubit(serverInstallationCubit);
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (_) => AppSettingsCubit(
|
create: (final _) => AppSettingsCubit(
|
||||||
isDarkModeOn: isDark,
|
isDarkModeOn: isDark,
|
||||||
isOnbordingShowing: true,
|
isOnboardingShowing: true,
|
||||||
)..load(),
|
)..load(),
|
||||||
),
|
),
|
||||||
BlocProvider(lazy: false, create: (_) => appConfigCubit),
|
|
||||||
BlocProvider(create: (_) => ProvidersCubit()),
|
|
||||||
BlocProvider(create: (_) => usersCubit..load(), lazy: false),
|
|
||||||
BlocProvider(create: (_) => servicesCubit..load(), lazy: false),
|
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (_) =>
|
create: (final _) => serverInstallationCubit,
|
||||||
|
lazy: false,
|
||||||
|
),
|
||||||
|
BlocProvider(create: (final _) => ProvidersCubit()),
|
||||||
|
BlocProvider(
|
||||||
|
create: (final _) => usersCubit..load(),
|
||||||
|
lazy: false,
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (final _) => servicesCubit..load(),
|
||||||
|
lazy: false,
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (final _) => backupsCubit..load(),
|
||||||
|
lazy: false,
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (final _) => dnsRecordsCubit..load(),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (final _) => recoveryKeyCubit..load(),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (final _) => apiDevicesCubit..load(),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (final _) =>
|
||||||
JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit),
|
JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:selfprivacy/ui/components/error/error.dart';
|
import 'package:selfprivacy/ui/components/error/error.dart';
|
||||||
import 'package:selfprivacy/utils/route_transitions/basic.dart';
|
import 'package:selfprivacy/utils/route_transitions/basic.dart';
|
||||||
|
|
||||||
import './get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
|
|
||||||
class SimpleBlocObserver extends BlocObserver {
|
class SimpleBlocObserver extends BlocObserver {
|
||||||
SimpleBlocObserver();
|
SimpleBlocObserver();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onError(BlocBase cubit, Object error, StackTrace stackTrace) {
|
void onError(
|
||||||
final navigator = getIt.get<NavigationService>().navigator!;
|
final BlocBase<dynamic> bloc,
|
||||||
|
final Object error,
|
||||||
|
final StackTrace stackTrace,
|
||||||
|
) {
|
||||||
|
final NavigatorState navigator = getIt.get<NavigationService>().navigator!;
|
||||||
|
|
||||||
navigator.push(
|
navigator.push(
|
||||||
materialRoute(
|
materialRoute(
|
||||||
|
@ -19,6 +24,6 @@ class SimpleBlocObserver extends BlocObserver {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
super.onError(cubit, error, stackTrace);
|
super.onError(bloc, error, stackTrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ class BrandColors {
|
||||||
static const Color gray3 = Color(0xFFFAFAFA);
|
static const Color gray3 = Color(0xFFFAFAFA);
|
||||||
static const Color gray4 = Color(0xFFDDDDDD);
|
static const Color gray4 = Color(0xFFDDDDDD);
|
||||||
static const Color gray5 = Color(0xFFEDEEF1);
|
static const Color gray5 = Color(0xFFEDEEF1);
|
||||||
static Color gray6 = Color(0xFF181818).withOpacity(0.7);
|
static Color gray6 = const Color(0xFF181818).withOpacity(0.7);
|
||||||
static const Color grey7 = Color(0xFFABABAB);
|
static const Color grey7 = Color(0xFFABABAB);
|
||||||
|
|
||||||
static const Color red1 = Color(0xFFFA0E0E);
|
static const Color red1 = Color(0xFFFA0E0E);
|
||||||
|
@ -20,8 +20,8 @@ class BrandColors {
|
||||||
|
|
||||||
static const Color green2 = Color(0xFF0F8849);
|
static const Color green2 = Color(0xFF0F8849);
|
||||||
|
|
||||||
static get navBackgroundLight => white.withOpacity(0.8);
|
static Color get navBackgroundLight => white.withOpacity(0.8);
|
||||||
static get navBackgroundDark => black.withOpacity(0.8);
|
static Color get navBackgroundDark => black.withOpacity(0.8);
|
||||||
|
|
||||||
static const List<Color> uninitializedGradientColors = [
|
static const List<Color> uninitializedGradientColors = [
|
||||||
Color(0xFF555555),
|
Color(0xFF555555),
|
||||||
|
@ -41,14 +41,14 @@ class BrandColors {
|
||||||
Color(0xFFEFD135),
|
Color(0xFFEFD135),
|
||||||
];
|
];
|
||||||
|
|
||||||
static const primary = blue;
|
static const Color primary = blue;
|
||||||
static const headlineColor = black;
|
static const Color headlineColor = black;
|
||||||
static const inactive = gray2;
|
static const Color inactive = gray2;
|
||||||
static const scaffoldBackground = gray3;
|
static const Color scaffoldBackground = gray3;
|
||||||
static const inputInactive = gray4;
|
static const Color inputInactive = gray4;
|
||||||
|
|
||||||
static const textColor1 = black;
|
static const Color textColor1 = black;
|
||||||
static const textColor2 = gray1;
|
static const Color textColor2 = gray1;
|
||||||
static const dividerColor = gray5;
|
static const Color dividerColor = gray5;
|
||||||
static const warning = red1;
|
static const Color warning = red1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:selfprivacy/config/text_themes.dart';
|
import 'package:selfprivacy/config/text_themes.dart';
|
||||||
|
|
||||||
import 'brand_colors.dart';
|
import 'package:selfprivacy/config/brand_colors.dart';
|
||||||
|
|
||||||
final ligtTheme = ThemeData(
|
final ThemeData lightTheme = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
primaryColor: BrandColors.primary,
|
primaryColor: BrandColors.primary,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
scaffoldBackgroundColor: BrandColors.scaffoldBackground,
|
scaffoldBackgroundColor: BrandColors.scaffoldBackground,
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.all(16),
|
contentPadding: EdgeInsets.all(16),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
|
@ -38,29 +39,34 @@ final ligtTheme = ThemeData(
|
||||||
color: BrandColors.red1,
|
color: BrandColors.red1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
listTileTheme: const ListTileThemeData(
|
||||||
|
minLeadingWidth: 24.0,
|
||||||
|
),
|
||||||
textTheme: TextTheme(
|
textTheme: TextTheme(
|
||||||
headline1: headline1Style,
|
headline1: headline1Style,
|
||||||
headline2: headline2Style,
|
headline2: headline2Style,
|
||||||
caption: headline4Style,
|
headline3: headline3Style,
|
||||||
|
headline4: headline4Style,
|
||||||
bodyText1: body1Style,
|
bodyText1: body1Style,
|
||||||
subtitle1: TextStyle(fontSize: 15, height: 1.6), // text input style
|
subtitle1: const TextStyle(fontSize: 15, height: 1.6), // text input style
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
var darkTheme = ligtTheme.copyWith(
|
ThemeData darkTheme = lightTheme.copyWith(
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
scaffoldBackgroundColor: Color(0xFF202120),
|
scaffoldBackgroundColor: const Color(0xFF202120),
|
||||||
iconTheme: IconThemeData(color: BrandColors.gray3),
|
iconTheme: const IconThemeData(color: BrandColors.gray3),
|
||||||
cardColor: BrandColors.gray1,
|
cardColor: BrandColors.gray1,
|
||||||
dialogBackgroundColor: Color(0xFF202120),
|
dialogBackgroundColor: const Color(0xFF202120),
|
||||||
textTheme: TextTheme(
|
textTheme: TextTheme(
|
||||||
headline1: headline1Style.copyWith(color: BrandColors.white),
|
headline1: headline1Style.copyWith(color: BrandColors.white),
|
||||||
headline2: headline2Style.copyWith(color: BrandColors.white),
|
headline2: headline2Style.copyWith(color: BrandColors.white),
|
||||||
caption: headline4Style.copyWith(color: BrandColors.white),
|
headline3: headline3Style.copyWith(color: BrandColors.white),
|
||||||
|
headline4: headline4Style.copyWith(color: BrandColors.white),
|
||||||
bodyText1: body1Style.copyWith(color: BrandColors.white),
|
bodyText1: body1Style.copyWith(color: BrandColors.white),
|
||||||
subtitle1: TextStyle(fontSize: 15, height: 1.6), // text input style
|
subtitle1: const TextStyle(fontSize: 15, height: 1.6), // text input style
|
||||||
),
|
),
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
labelStyle: TextStyle(color: BrandColors.white),
|
labelStyle: TextStyle(color: BrandColors.white),
|
||||||
hintStyle: TextStyle(color: BrandColors.white),
|
hintStyle: TextStyle(color: BrandColors.white),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
|
@ -76,6 +82,7 @@ var darkTheme = ligtTheme.copyWith(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final paddingH15V30 = EdgeInsets.symmetric(horizontal: 15, vertical: 30);
|
const EdgeInsets paddingH15V30 =
|
||||||
|
EdgeInsets.symmetric(horizontal: 15, vertical: 30);
|
||||||
|
|
||||||
final paddingH15V0 = EdgeInsets.symmetric(horizontal: 15);
|
const EdgeInsets paddingH15V0 = EdgeInsets.symmetric(horizontal: 15);
|
||||||
|
|
|
@ -2,7 +2,6 @@ import 'package:get_it/get_it.dart';
|
||||||
import 'package:selfprivacy/logic/get_it/api_config.dart';
|
import 'package:selfprivacy/logic/get_it/api_config.dart';
|
||||||
import 'package:selfprivacy/logic/get_it/console.dart';
|
import 'package:selfprivacy/logic/get_it/console.dart';
|
||||||
import 'package:selfprivacy/logic/get_it/navigation.dart';
|
import 'package:selfprivacy/logic/get_it/navigation.dart';
|
||||||
import 'package:selfprivacy/logic/get_it/ssh.dart';
|
|
||||||
import 'package:selfprivacy/logic/get_it/timer.dart';
|
import 'package:selfprivacy/logic/get_it/timer.dart';
|
||||||
|
|
||||||
export 'package:selfprivacy/logic/get_it/api_config.dart';
|
export 'package:selfprivacy/logic/get_it/api_config.dart';
|
||||||
|
@ -10,14 +9,13 @@ export 'package:selfprivacy/logic/get_it/console.dart';
|
||||||
export 'package:selfprivacy/logic/get_it/navigation.dart';
|
export 'package:selfprivacy/logic/get_it/navigation.dart';
|
||||||
export 'package:selfprivacy/logic/get_it/timer.dart';
|
export 'package:selfprivacy/logic/get_it/timer.dart';
|
||||||
|
|
||||||
final getIt = GetIt.instance;
|
final GetIt getIt = GetIt.instance;
|
||||||
|
|
||||||
Future<void> getItSetup() async {
|
Future<void> getItSetup() async {
|
||||||
getIt.registerSingleton<NavigationService>(NavigationService());
|
getIt.registerSingleton<NavigationService>(NavigationService());
|
||||||
|
|
||||||
getIt.registerSingleton<ConsoleModel>(ConsoleModel());
|
getIt.registerSingleton<ConsoleModel>(ConsoleModel());
|
||||||
getIt.registerSingleton<TimerModel>(TimerModel());
|
getIt.registerSingleton<TimerModel>(TimerModel());
|
||||||
getIt.registerSingleton<SSHModel>(SSHModel()..init());
|
|
||||||
getIt.registerSingleton<ApiConfigModel>(ApiConfigModel()..init());
|
getIt.registerSingleton<ApiConfigModel>(ApiConfigModel()..init());
|
||||||
|
|
||||||
await getIt.allReady();
|
await getIt.allReady();
|
||||||
|
|
|
@ -2,70 +2,120 @@ import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:selfprivacy/logic/models/backblaze_credential.dart';
|
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
|
||||||
import 'package:selfprivacy/logic/models/cloudflare_domain.dart';
|
import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart';
|
||||||
import 'package:selfprivacy/logic/models/server_details.dart';
|
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||||
import 'package:selfprivacy/logic/models/user.dart';
|
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||||
|
|
||||||
class HiveConfig {
|
class HiveConfig {
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
await Hive.initFlutter();
|
await Hive.initFlutter();
|
||||||
Hive.registerAdapter(UserAdapter());
|
Hive.registerAdapter(UserAdapter());
|
||||||
Hive.registerAdapter(HetznerServerDetailsAdapter());
|
Hive.registerAdapter(ServerHostingDetailsAdapter());
|
||||||
Hive.registerAdapter(CloudFlareDomainAdapter());
|
Hive.registerAdapter(ServerDomainAdapter());
|
||||||
Hive.registerAdapter(BackblazeCredentialAdapter());
|
Hive.registerAdapter(BackblazeCredentialAdapter());
|
||||||
Hive.registerAdapter(HetznerDataBaseAdapter());
|
Hive.registerAdapter(BackblazeBucketAdapter());
|
||||||
|
Hive.registerAdapter(ServerVolumeAdapter());
|
||||||
|
|
||||||
await Hive.openBox(BNames.appSettings);
|
Hive.registerAdapter(DnsProviderAdapter());
|
||||||
await Hive.openBox<User>(BNames.users);
|
Hive.registerAdapter(ServerProviderAdapter());
|
||||||
await Hive.openBox(BNames.servicesState);
|
|
||||||
|
|
||||||
var cipher = HiveAesCipher(await getEncriptedKey(BNames.key));
|
await Hive.openBox(BNames.appSettingsBox);
|
||||||
await Hive.openBox(BNames.appConfig, encryptionCipher: cipher);
|
|
||||||
|
|
||||||
var sshCipher = HiveAesCipher(await getEncriptedKey(BNames.sshEnckey));
|
final HiveAesCipher cipher = HiveAesCipher(
|
||||||
await Hive.openBox(BNames.sshConfig, encryptionCipher: sshCipher);
|
await getEncryptedKey(BNames.serverInstallationEncryptionKey),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Hive.openBox<User>(BNames.usersDeprecated);
|
||||||
|
await Hive.openBox<User>(BNames.usersBox, encryptionCipher: cipher);
|
||||||
|
|
||||||
|
final Box<User> deprecatedUsers = Hive.box<User>(BNames.usersDeprecated);
|
||||||
|
if (deprecatedUsers.isNotEmpty) {
|
||||||
|
final Box<User> users = Hive.box<User>(BNames.usersBox);
|
||||||
|
users.addAll(deprecatedUsers.values.toList());
|
||||||
|
deprecatedUsers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Uint8List> getEncriptedKey(String encKey) async {
|
static Future<Uint8List> getEncryptedKey(final String encKey) async {
|
||||||
final secureStorage = FlutterSecureStorage();
|
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
|
||||||
var hasEncryptionKey = await secureStorage.containsKey(key: encKey);
|
final bool hasEncryptionKey = await secureStorage.containsKey(key: encKey);
|
||||||
if (!hasEncryptionKey) {
|
if (!hasEncryptionKey) {
|
||||||
var key = Hive.generateSecureKey();
|
final List<int> key = Hive.generateSecureKey();
|
||||||
await secureStorage.write(key: encKey, value: base64UrlEncode(key));
|
await secureStorage.write(key: encKey, value: base64UrlEncode(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
String? string = await secureStorage.read(key: encKey);
|
final String? string = await secureStorage.read(key: encKey);
|
||||||
return base64Url.decode(string!);
|
return base64Url.decode(string!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mappings for the different boxes and their keys
|
||||||
class BNames {
|
class BNames {
|
||||||
static String appConfig = 'appConfig';
|
/// App settings box. Contains app settings like [isDarkModeOn], [isOnboardingShowing]
|
||||||
|
static String appSettingsBox = 'appSettings';
|
||||||
|
|
||||||
|
/// A boolean field of [appSettingsBox] box.
|
||||||
static String isDarkModeOn = 'isDarkModeOn';
|
static String isDarkModeOn = 'isDarkModeOn';
|
||||||
static String isOnbordingShowing = 'isOnbordingShowing';
|
|
||||||
static String users = 'users';
|
|
||||||
|
|
||||||
static String appSettings = 'appSettings';
|
/// A boolean field of [appSettingsBox] box.
|
||||||
static String servicesState = 'servicesState';
|
static String isOnboardingShowing = 'isOnboardingShowing';
|
||||||
|
|
||||||
static String key = 'key';
|
/// Encryption key to decrypt [serverInstallationBox] and [usersBox] box.
|
||||||
static String sshEnckey = 'sshEngkey';
|
static String serverInstallationEncryptionKey = 'key';
|
||||||
|
|
||||||
static String cloudFlareDomain = 'cloudFlareDomain';
|
/// Server installation box. Contains server details and provider tokens.
|
||||||
static String hetznerKey = 'hetznerKey';
|
static String serverInstallationBox = 'appConfig';
|
||||||
static String cloudFlareKey = 'cloudFlareKey';
|
|
||||||
static String rootUser = 'rootUser';
|
/// A List<String> field of [serverInstallationBox] box.
|
||||||
static String hetznerServer = 'hetznerServer';
|
static String rootKeys = 'rootKeys';
|
||||||
|
|
||||||
|
/// A boolean field of [serverInstallationBox] box.
|
||||||
static String hasFinalChecked = 'hasFinalChecked';
|
static String hasFinalChecked = 'hasFinalChecked';
|
||||||
|
|
||||||
|
/// A boolean field of [serverInstallationBox] box.
|
||||||
static String isServerStarted = 'isServerStarted';
|
static String isServerStarted = 'isServerStarted';
|
||||||
static String backblazeKey = 'backblazeKey';
|
|
||||||
|
/// A [ServerDomain] field of [serverInstallationBox] box.
|
||||||
|
static String serverDomain = 'cloudFlareDomain';
|
||||||
|
|
||||||
|
/// A String field of [serverInstallationBox] box.
|
||||||
|
static String hetznerKey = 'hetznerKey';
|
||||||
|
|
||||||
|
/// A String field of [serverInstallationBox] box.
|
||||||
|
static String cloudFlareKey = 'cloudFlareKey';
|
||||||
|
|
||||||
|
/// A [User] field of [serverInstallationBox] box.
|
||||||
|
static String rootUser = 'rootUser';
|
||||||
|
|
||||||
|
/// A [ServerHostingDetails] field of [serverInstallationBox] box.
|
||||||
|
static String serverDetails = 'hetznerServer';
|
||||||
|
|
||||||
|
/// A [BackblazeCredential] field of [serverInstallationBox] box.
|
||||||
|
static String backblazeCredential = 'backblazeKey';
|
||||||
|
|
||||||
|
/// A [BackblazeBucket] field of [serverInstallationBox] box.
|
||||||
|
static String backblazeBucket = 'backblazeBucket';
|
||||||
|
|
||||||
|
/// A boolean field of [serverInstallationBox] box.
|
||||||
static String isLoading = 'isLoading';
|
static String isLoading = 'isLoading';
|
||||||
|
|
||||||
|
/// A boolean field of [serverInstallationBox] box.
|
||||||
static String isServerResetedFirstTime = 'isServerResetedFirstTime';
|
static String isServerResetedFirstTime = 'isServerResetedFirstTime';
|
||||||
|
|
||||||
|
/// A boolean field of [serverInstallationBox] box.
|
||||||
static String isServerResetedSecondTime = 'isServerResetedSecondTime';
|
static String isServerResetedSecondTime = 'isServerResetedSecondTime';
|
||||||
static String sshConfig = 'sshConfig';
|
|
||||||
static String sshPrivateKey = "sshPrivateKey";
|
/// A boolean field of [serverInstallationBox] box.
|
||||||
static String sshPublicKey = "sshPublicKey";
|
static String isRecoveringServer = 'isRecoveringServer';
|
||||||
|
|
||||||
|
/// Deprecated users box as it is unencrypted
|
||||||
|
static String usersDeprecated = 'users';
|
||||||
|
|
||||||
|
/// Box with users
|
||||||
|
static String usersBox = 'usersEncrypted';
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,20 +3,18 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class Localization extends StatelessWidget {
|
class Localization extends StatelessWidget {
|
||||||
const Localization({
|
const Localization({
|
||||||
Key? key,
|
final super.key,
|
||||||
this.child,
|
this.child,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(final BuildContext context) => EasyLocalization(
|
||||||
return EasyLocalization(
|
supportedLocales: const [Locale('ru'), Locale('en')],
|
||||||
supportedLocales: [Locale('ru'), Locale('en')],
|
path: 'assets/translations',
|
||||||
path: 'assets/translations',
|
fallbackLocale: const Locale('en'),
|
||||||
fallbackLocale: Locale('en'),
|
saveLocale: false,
|
||||||
saveLocale: false,
|
useOnlyLangCode: true,
|
||||||
useOnlyLangCode: true,
|
child: child!,
|
||||||
child: child!,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -1,80 +1,80 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:selfprivacy/utils/named_font_weight.dart';
|
import 'package:selfprivacy/utils/named_font_weight.dart';
|
||||||
|
|
||||||
import 'brand_colors.dart';
|
import 'package:selfprivacy/config/brand_colors.dart';
|
||||||
|
|
||||||
final defaultTextStyle = TextStyle(
|
const TextStyle defaultTextStyle = TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: BrandColors.textColor1,
|
color: BrandColors.textColor1,
|
||||||
);
|
);
|
||||||
|
|
||||||
final headline1Style = defaultTextStyle.copyWith(
|
final TextStyle headline1Style = defaultTextStyle.copyWith(
|
||||||
fontSize: 40,
|
fontSize: 40,
|
||||||
fontWeight: NamedFontWeight.extraBold,
|
fontWeight: NamedFontWeight.extraBold,
|
||||||
color: BrandColors.headlineColor,
|
color: BrandColors.headlineColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
final headline2Style = defaultTextStyle.copyWith(
|
final TextStyle headline2Style = defaultTextStyle.copyWith(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: NamedFontWeight.extraBold,
|
fontWeight: NamedFontWeight.extraBold,
|
||||||
color: BrandColors.headlineColor,
|
color: BrandColors.headlineColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
final onboardingTitle = defaultTextStyle.copyWith(
|
final TextStyle onboardingTitle = defaultTextStyle.copyWith(
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
fontWeight: NamedFontWeight.extraBold,
|
fontWeight: NamedFontWeight.extraBold,
|
||||||
color: BrandColors.headlineColor,
|
color: BrandColors.headlineColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
final headline3Style = defaultTextStyle.copyWith(
|
final TextStyle headline3Style = defaultTextStyle.copyWith(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: NamedFontWeight.extraBold,
|
fontWeight: NamedFontWeight.extraBold,
|
||||||
color: BrandColors.headlineColor,
|
color: BrandColors.headlineColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
final headline4Style = defaultTextStyle.copyWith(
|
final TextStyle headline4Style = defaultTextStyle.copyWith(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: NamedFontWeight.medium,
|
fontWeight: NamedFontWeight.medium,
|
||||||
color: BrandColors.headlineColor,
|
color: BrandColors.headlineColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
final headline4UnderlinedStyle = defaultTextStyle.copyWith(
|
final TextStyle headline4UnderlinedStyle = defaultTextStyle.copyWith(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: NamedFontWeight.medium,
|
fontWeight: NamedFontWeight.medium,
|
||||||
color: BrandColors.headlineColor,
|
color: BrandColors.headlineColor,
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
);
|
);
|
||||||
|
|
||||||
final headline5Style = defaultTextStyle.copyWith(
|
final TextStyle headline5Style = defaultTextStyle.copyWith(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: NamedFontWeight.medium,
|
fontWeight: NamedFontWeight.medium,
|
||||||
color: BrandColors.headlineColor.withOpacity(0.8),
|
color: BrandColors.headlineColor.withOpacity(0.8),
|
||||||
);
|
);
|
||||||
|
|
||||||
final body1Style = defaultTextStyle;
|
const TextStyle body1Style = defaultTextStyle;
|
||||||
final body2Style = defaultTextStyle.copyWith(
|
final TextStyle body2Style = defaultTextStyle.copyWith(
|
||||||
color: BrandColors.textColor2,
|
color: BrandColors.textColor2,
|
||||||
);
|
);
|
||||||
|
|
||||||
final buttonTitleText = defaultTextStyle.copyWith(
|
final TextStyle buttonTitleText = defaultTextStyle.copyWith(
|
||||||
color: BrandColors.white,
|
color: BrandColors.white,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
height: 1,
|
height: 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
final mediumStyle = defaultTextStyle.copyWith(fontSize: 13, height: 1.53);
|
final TextStyle mediumStyle =
|
||||||
|
defaultTextStyle.copyWith(fontSize: 13, height: 1.53);
|
||||||
|
|
||||||
final smallStyle = defaultTextStyle.copyWith(fontSize: 11, height: 1.45);
|
final TextStyle smallStyle =
|
||||||
|
defaultTextStyle.copyWith(fontSize: 11, height: 1.45);
|
||||||
|
|
||||||
final linkStyle = defaultTextStyle.copyWith(color: BrandColors.blue);
|
const TextStyle progressTextStyleLight = TextStyle(
|
||||||
|
|
||||||
final progressTextStyleLight = TextStyle(
|
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: BrandColors.textColor1,
|
color: BrandColors.textColor1,
|
||||||
height: 1.7,
|
height: 1.7,
|
||||||
);
|
);
|
||||||
|
|
||||||
final progressTextStyleDark = progressTextStyleLight.copyWith(
|
final TextStyle progressTextStyleDark = progressTextStyleLight.copyWith(
|
||||||
color: BrandColors.white,
|
color: BrandColors.white,
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,58 +6,62 @@ import 'package:dio/adapter.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/get_it/console.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/message.dart';
|
import 'package:selfprivacy/logic/models/message.dart';
|
||||||
|
|
||||||
abstract class ApiMap {
|
abstract class ApiMap {
|
||||||
Future<Dio> getClient() async {
|
Future<Dio> getClient() async {
|
||||||
var dio = Dio(await options);
|
final Dio dio = Dio(await options);
|
||||||
if (hasLoger) {
|
if (hasLogger) {
|
||||||
dio.interceptors.add(PrettyDioLogger());
|
dio.interceptors.add(PrettyDioLogger());
|
||||||
}
|
}
|
||||||
dio.interceptors.add(ConsoleInterceptor());
|
dio.interceptors.add(ConsoleInterceptor());
|
||||||
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
|
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
|
||||||
(HttpClient client) {
|
(final HttpClient client) {
|
||||||
client.badCertificateCallback =
|
client.badCertificateCallback =
|
||||||
(X509Certificate cert, String host, int port) => true;
|
(final X509Certificate cert, final String host, final int port) =>
|
||||||
|
true;
|
||||||
return client;
|
return client;
|
||||||
};
|
};
|
||||||
|
|
||||||
dio.interceptors.add(InterceptorsWrapper(onError: (DioError e, handler) {
|
dio.interceptors.add(
|
||||||
print(e.requestOptions.path);
|
InterceptorsWrapper(
|
||||||
print(e.requestOptions.data);
|
onError: (final DioError e, final ErrorInterceptorHandler handler) {
|
||||||
|
print(e.requestOptions.path);
|
||||||
|
print(e.requestOptions.data);
|
||||||
|
|
||||||
print(e.message);
|
print(e.message);
|
||||||
print(e.response);
|
print(e.response);
|
||||||
|
|
||||||
return handler.next(e);
|
return handler.next(e);
|
||||||
}));
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
return dio;
|
return dio;
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<BaseOptions> get options;
|
FutureOr<BaseOptions> get options;
|
||||||
|
|
||||||
abstract final String rootAddress;
|
abstract final String rootAddress;
|
||||||
abstract final bool hasLoger;
|
abstract final bool hasLogger;
|
||||||
abstract final bool isWithToken;
|
abstract final bool isWithToken;
|
||||||
|
|
||||||
ValidateStatus? validateStatus;
|
ValidateStatus? validateStatus;
|
||||||
|
|
||||||
void close(Dio client) {
|
void close(final Dio client) {
|
||||||
client.close();
|
client.close();
|
||||||
validateStatus = null;
|
validateStatus = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConsoleInterceptor extends InterceptorsWrapper {
|
class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
void addMessage(Message message) {
|
void addMessage(final Message message) {
|
||||||
getIt.get<ConsoleModel>().addMessage(message);
|
getIt.get<ConsoleModel>().addMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future onRequest(
|
Future<void> onRequest(
|
||||||
RequestOptions options,
|
final RequestOptions options,
|
||||||
RequestInterceptorHandler requestInterceptorHandler,
|
final RequestInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
addMessage(
|
addMessage(
|
||||||
Message(
|
Message(
|
||||||
|
@ -65,13 +69,13 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
'request-uri: ${options.uri}\nheaders: ${options.headers}\ndata: ${options.data}',
|
'request-uri: ${options.uri}\nheaders: ${options.headers}\ndata: ${options.data}',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return super.onRequest(options, requestInterceptorHandler);
|
return super.onRequest(options, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future onResponse(
|
Future<void> onResponse(
|
||||||
Response response,
|
final Response response,
|
||||||
ResponseInterceptorHandler requestInterceptorHandler,
|
final ResponseInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
addMessage(
|
addMessage(
|
||||||
Message(
|
Message(
|
||||||
|
@ -81,13 +85,16 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
);
|
);
|
||||||
return super.onResponse(
|
return super.onResponse(
|
||||||
response,
|
response,
|
||||||
requestInterceptorHandler,
|
handler,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future onError(DioError err, ErrorInterceptorHandler handler) async {
|
Future<void> onError(
|
||||||
var response = err.response;
|
final DioError err,
|
||||||
|
final ErrorInterceptorHandler handler,
|
||||||
|
) async {
|
||||||
|
final Response? response = err.response;
|
||||||
log(err.toString());
|
log(err.toString());
|
||||||
addMessage(
|
addMessage(
|
||||||
Message.warn(
|
Message.warn(
|
||||||
|
|
|
@ -1,16 +1,37 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/api_map.dart';
|
import 'package:selfprivacy/logic/api_maps/api_map.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart';
|
||||||
|
|
||||||
|
class BackblazeApiAuth {
|
||||||
|
BackblazeApiAuth({required this.authorizationToken, required this.apiUrl});
|
||||||
|
|
||||||
|
final String authorizationToken;
|
||||||
|
final String apiUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BackblazeApplicationKey {
|
||||||
|
BackblazeApplicationKey({
|
||||||
|
required this.applicationKeyId,
|
||||||
|
required this.applicationKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String applicationKeyId;
|
||||||
|
final String applicationKey;
|
||||||
|
}
|
||||||
|
|
||||||
class BackblazeApi extends ApiMap {
|
class BackblazeApi extends ApiMap {
|
||||||
BackblazeApi({this.hasLoger = false, this.isWithToken = true});
|
BackblazeApi({this.hasLogger = false, this.isWithToken = true});
|
||||||
|
|
||||||
|
@override
|
||||||
BaseOptions get options {
|
BaseOptions get options {
|
||||||
var options = BaseOptions(baseUrl: rootAddress);
|
final BaseOptions options = BaseOptions(baseUrl: rootAddress);
|
||||||
if (isWithToken) {
|
if (isWithToken) {
|
||||||
var backblazeCredential = getIt<ApiConfigModel>().backblazeCredential;
|
final BackblazeCredential? backblazeCredential =
|
||||||
var token = backblazeCredential!.applicationKey;
|
getIt<ApiConfigModel>().backblazeCredential;
|
||||||
|
final String token = backblazeCredential!.applicationKey;
|
||||||
options.headers = {'Authorization': 'Basic $token'};
|
options.headers = {'Authorization': 'Basic $token'};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,24 +45,119 @@ class BackblazeApi extends ApiMap {
|
||||||
@override
|
@override
|
||||||
String rootAddress = 'https://api.backblazeb2.com/b2api/v2/';
|
String rootAddress = 'https://api.backblazeb2.com/b2api/v2/';
|
||||||
|
|
||||||
Future<bool> isValid(String encodedApiKey) async {
|
String apiPrefix = '/b2api/v2';
|
||||||
var client = await getClient();
|
|
||||||
Response response = await client.get(
|
Future<BackblazeApiAuth> getAuthorizationToken() async {
|
||||||
|
final Dio client = await getClient();
|
||||||
|
final BackblazeCredential? backblazeCredential =
|
||||||
|
getIt<ApiConfigModel>().backblazeCredential;
|
||||||
|
if (backblazeCredential == null) {
|
||||||
|
throw Exception('Backblaze credential is null');
|
||||||
|
}
|
||||||
|
final String encodedApiKey = encodedBackblazeKey(
|
||||||
|
backblazeCredential.keyId,
|
||||||
|
backblazeCredential.applicationKey,
|
||||||
|
);
|
||||||
|
final Response response = await client.get(
|
||||||
'b2_authorize_account',
|
'b2_authorize_account',
|
||||||
options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}),
|
options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}),
|
||||||
);
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('code: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
return BackblazeApiAuth(
|
||||||
|
authorizationToken: response.data['authorizationToken'],
|
||||||
|
apiUrl: response.data['apiUrl'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isValid(final String encodedApiKey) async {
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
final Response response = await client.get(
|
||||||
|
'b2_authorize_account',
|
||||||
|
options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}),
|
||||||
|
);
|
||||||
|
if (response.statusCode == HttpStatus.ok) {
|
||||||
|
if (response.data['allowed']['capabilities'].contains('listBuckets')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else if (response.statusCode == HttpStatus.unauthorized) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw Exception('code: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
} on DioError {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create bucket
|
||||||
|
Future<String> createBucket(final String bucketName) async {
|
||||||
|
final BackblazeApiAuth auth = await getAuthorizationToken();
|
||||||
|
final BackblazeCredential? backblazeCredential =
|
||||||
|
getIt<ApiConfigModel>().backblazeCredential;
|
||||||
|
final Dio client = await getClient();
|
||||||
|
client.options.baseUrl = auth.apiUrl;
|
||||||
|
final Response response = await client.post(
|
||||||
|
'$apiPrefix/b2_create_bucket',
|
||||||
|
data: {
|
||||||
|
'accountId': backblazeCredential!.keyId,
|
||||||
|
'bucketName': bucketName,
|
||||||
|
'bucketType': 'allPrivate',
|
||||||
|
'lifecycleRules': [
|
||||||
|
{
|
||||||
|
'daysFromHidingToDeleting': 30,
|
||||||
|
'daysFromUploadingToHiding': null,
|
||||||
|
'fileNamePrefix': ''
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
headers: {'Authorization': auth.authorizationToken},
|
||||||
|
),
|
||||||
|
);
|
||||||
close(client);
|
close(client);
|
||||||
if (response.statusCode == HttpStatus.ok) {
|
if (response.statusCode == HttpStatus.ok) {
|
||||||
return true;
|
return response.data['bucketId'];
|
||||||
} else if (response.statusCode == HttpStatus.unauthorized) {
|
} else {
|
||||||
return false;
|
throw Exception('code: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a limited capability key with access to the given bucket
|
||||||
|
Future<BackblazeApplicationKey> createKey(final String bucketId) async {
|
||||||
|
final BackblazeApiAuth auth = await getAuthorizationToken();
|
||||||
|
final Dio client = await getClient();
|
||||||
|
client.options.baseUrl = auth.apiUrl;
|
||||||
|
final Response response = await client.post(
|
||||||
|
'$apiPrefix/b2_create_key',
|
||||||
|
data: {
|
||||||
|
'accountId': getIt<ApiConfigModel>().backblazeCredential!.keyId,
|
||||||
|
'bucketId': bucketId,
|
||||||
|
'capabilities': ['listBuckets', 'listFiles', 'readFiles', 'writeFiles'],
|
||||||
|
'keyName': 'selfprivacy-restricted-server-key',
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
headers: {'Authorization': auth.authorizationToken},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
close(client);
|
||||||
|
if (response.statusCode == HttpStatus.ok) {
|
||||||
|
return BackblazeApplicationKey(
|
||||||
|
applicationKeyId: response.data['applicationKeyId'],
|
||||||
|
applicationKey: response.data['applicationKey'],
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw Exception('code: ${response.statusCode}');
|
throw Exception('code: ${response.statusCode}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool hasLoger;
|
bool hasLogger;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isWithToken;
|
bool isWithToken;
|
||||||
|
|
|
@ -1,21 +1,42 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/api_map.dart';
|
import 'package:selfprivacy/logic/api_maps/api_map.dart';
|
||||||
import 'package:selfprivacy/logic/models/cloudflare_domain.dart';
|
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||||
import 'package:selfprivacy/logic/models/dns_records.dart';
|
import 'package:selfprivacy/logic/models/json/dns_records.dart';
|
||||||
|
|
||||||
|
class DomainNotFoundException implements Exception {
|
||||||
|
DomainNotFoundException(this.message);
|
||||||
|
final String message;
|
||||||
|
}
|
||||||
|
|
||||||
class CloudflareApi extends ApiMap {
|
class CloudflareApi extends ApiMap {
|
||||||
CloudflareApi({this.hasLoger = false, this.isWithToken = true});
|
CloudflareApi({
|
||||||
|
this.hasLogger = false,
|
||||||
|
this.isWithToken = true,
|
||||||
|
this.customToken,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
final bool hasLogger;
|
||||||
|
@override
|
||||||
|
final bool isWithToken;
|
||||||
|
|
||||||
|
final String? customToken;
|
||||||
|
|
||||||
|
@override
|
||||||
BaseOptions get options {
|
BaseOptions get options {
|
||||||
var options = BaseOptions(baseUrl: rootAddress);
|
final BaseOptions options = BaseOptions(baseUrl: rootAddress);
|
||||||
if (isWithToken) {
|
if (isWithToken) {
|
||||||
var token = getIt<ApiConfigModel>().cloudFlareKey;
|
final String? token = getIt<ApiConfigModel>().cloudFlareKey;
|
||||||
assert(token != null);
|
assert(token != null);
|
||||||
options.headers = {'Authorization': 'Bearer $token'};
|
options.headers = {'Authorization': 'Bearer $token'};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (customToken != null) {
|
||||||
|
options.headers = {'Authorization': 'Bearer $customToken'};
|
||||||
|
}
|
||||||
|
|
||||||
if (validateStatus != null) {
|
if (validateStatus != null) {
|
||||||
options.validateStatus = validateStatus!;
|
options.validateStatus = validateStatus!;
|
||||||
}
|
}
|
||||||
|
@ -25,14 +46,15 @@ class CloudflareApi extends ApiMap {
|
||||||
@override
|
@override
|
||||||
String rootAddress = 'https://api.cloudflare.com/client/v4';
|
String rootAddress = 'https://api.cloudflare.com/client/v4';
|
||||||
|
|
||||||
Future<bool> isValid(String token) async {
|
Future<bool> isValid(final String token) async {
|
||||||
validateStatus = (status) {
|
validateStatus = (final status) =>
|
||||||
return status == HttpStatus.ok || status == HttpStatus.unauthorized;
|
status == HttpStatus.ok || status == HttpStatus.unauthorized;
|
||||||
};
|
|
||||||
|
|
||||||
var client = await getClient();
|
final Dio client = await getClient();
|
||||||
Response response = await client.get('/user/tokens/verify',
|
final Response response = await client.get(
|
||||||
options: Options(headers: {'Authorization': 'Bearer $token'}));
|
'/user/tokens/verify',
|
||||||
|
options: Options(headers: {'Authorization': 'Bearer $token'}),
|
||||||
|
);
|
||||||
|
|
||||||
close(client);
|
close(client);
|
||||||
|
|
||||||
|
@ -45,37 +67,40 @@ class CloudflareApi extends ApiMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getZoneId(String domain) async {
|
Future<String> getZoneId(final String domain) async {
|
||||||
validateStatus = (status) {
|
validateStatus = (final status) =>
|
||||||
return status == HttpStatus.ok || status == HttpStatus.forbidden;
|
status == HttpStatus.ok || status == HttpStatus.forbidden;
|
||||||
};
|
final Dio client = await getClient();
|
||||||
var client = await getClient();
|
final Response response = await client.get(
|
||||||
Response response = await client.get(
|
|
||||||
'/zones',
|
'/zones',
|
||||||
queryParameters: {'name': domain},
|
queryParameters: {'name': domain},
|
||||||
);
|
);
|
||||||
|
|
||||||
close(client);
|
close(client);
|
||||||
|
|
||||||
return response.data['result'][0]['id'];
|
if (response.data['result'].isEmpty) {
|
||||||
|
throw DomainNotFoundException('No domains found');
|
||||||
|
} else {
|
||||||
|
return response.data['result'][0]['id'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeSimilarRecords({
|
Future<void> removeSimilarRecords({
|
||||||
String? ip4,
|
required final ServerDomain cloudFlareDomain,
|
||||||
required CloudFlareDomain cloudFlareDomain,
|
final String? ip4,
|
||||||
}) async {
|
}) async {
|
||||||
var domainName = cloudFlareDomain.domainName;
|
final String domainName = cloudFlareDomain.domainName;
|
||||||
var domainZoneId = cloudFlareDomain.zoneId;
|
final String domainZoneId = cloudFlareDomain.zoneId;
|
||||||
|
|
||||||
var url = '/zones/$domainZoneId/dns_records';
|
final String url = '/zones/$domainZoneId/dns_records';
|
||||||
|
|
||||||
var client = await getClient();
|
final Dio client = await getClient();
|
||||||
Response response = await client.get(url);
|
final Response response = await client.get(url);
|
||||||
|
|
||||||
List records = response.data['result'] ?? [];
|
final List records = response.data['result'] ?? [];
|
||||||
var allDeleteFutures = <Future>[];
|
final List<Future> allDeleteFutures = <Future>[];
|
||||||
|
|
||||||
for (var record in records) {
|
for (final record in records) {
|
||||||
if (record['zone_name'] == domainName) {
|
if (record['zone_name'] == domainName) {
|
||||||
allDeleteFutures.add(
|
allDeleteFutures.add(
|
||||||
client.delete('$url/${record["id"]}'),
|
client.delete('$url/${record["id"]}'),
|
||||||
|
@ -87,59 +112,99 @@ class CloudflareApi extends ApiMap {
|
||||||
close(client);
|
close(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createMultipleDnsRecords({
|
Future<List<DnsRecord>> getDnsRecords({
|
||||||
String? ip4,
|
required final ServerDomain cloudFlareDomain,
|
||||||
required CloudFlareDomain cloudFlareDomain,
|
|
||||||
}) async {
|
}) async {
|
||||||
var domainName = cloudFlareDomain.domainName;
|
final String domainName = cloudFlareDomain.domainName;
|
||||||
var domainZoneId = cloudFlareDomain.zoneId;
|
final String domainZoneId = cloudFlareDomain.zoneId;
|
||||||
var listDnsRecords = projectDnsRecords(domainName, ip4);
|
|
||||||
|
|
||||||
var url = '$rootAddress/zones/$domainZoneId/dns_records';
|
final String url = '/zones/$domainZoneId/dns_records';
|
||||||
|
|
||||||
var allCreateFutures = <Future>[];
|
final Dio client = await getClient();
|
||||||
var client = await getClient();
|
final Response response = await client.get(url);
|
||||||
|
|
||||||
for (var record in listDnsRecords) {
|
final List records = response.data['result'] ?? [];
|
||||||
allCreateFutures.add(
|
final List<DnsRecord> allRecords = <DnsRecord>[];
|
||||||
client.post(
|
|
||||||
url,
|
for (final record in records) {
|
||||||
data: record.toJson(),
|
if (record['zone_name'] == domainName) {
|
||||||
),
|
allRecords.add(
|
||||||
);
|
DnsRecord(
|
||||||
|
name: record['name'],
|
||||||
|
type: record['type'],
|
||||||
|
content: record['content'],
|
||||||
|
ttl: record['ttl'],
|
||||||
|
proxied: record['proxied'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Future.wait(allCreateFutures);
|
|
||||||
close(client);
|
close(client);
|
||||||
|
return allRecords;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DnsRecords> projectDnsRecords(String? domainName, String? ip4) {
|
Future<void> createMultipleDnsRecords({
|
||||||
var domainA = DnsRecords(type: 'A', name: domainName, content: ip4);
|
required final ServerDomain cloudFlareDomain,
|
||||||
|
final String? ip4,
|
||||||
|
}) async {
|
||||||
|
final String domainName = cloudFlareDomain.domainName;
|
||||||
|
final String domainZoneId = cloudFlareDomain.zoneId;
|
||||||
|
final List<DnsRecord> listDnsRecords = projectDnsRecords(domainName, ip4);
|
||||||
|
final List<Future> allCreateFutures = <Future>[];
|
||||||
|
|
||||||
var mx = DnsRecords(type: 'MX', name: '@', content: domainName);
|
final Dio client = await getClient();
|
||||||
var apiA = DnsRecords(type: 'A', name: 'api', content: ip4);
|
try {
|
||||||
var cloudA = DnsRecords(type: 'A', name: 'cloud', content: ip4);
|
for (final DnsRecord record in listDnsRecords) {
|
||||||
var gitA = DnsRecords(type: 'A', name: 'git', content: ip4);
|
allCreateFutures.add(
|
||||||
var meetA = DnsRecords(type: 'A', name: 'meet', content: ip4);
|
client.post(
|
||||||
var passwordA = DnsRecords(type: 'A', name: 'password', content: ip4);
|
'/zones/$domainZoneId/dns_records',
|
||||||
var socialA = DnsRecords(type: 'A', name: 'social', content: ip4);
|
data: record.toJson(),
|
||||||
var vpn = DnsRecords(type: 'A', name: 'vpn', content: ip4);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await Future.wait(allCreateFutures);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var txt1 = DnsRecords(
|
List<DnsRecord> projectDnsRecords(
|
||||||
|
final String? domainName,
|
||||||
|
final String? ip4,
|
||||||
|
) {
|
||||||
|
final DnsRecord domainA =
|
||||||
|
DnsRecord(type: 'A', name: domainName, content: ip4);
|
||||||
|
|
||||||
|
final DnsRecord mx = DnsRecord(type: 'MX', name: '@', content: domainName);
|
||||||
|
final DnsRecord apiA = DnsRecord(type: 'A', name: 'api', content: ip4);
|
||||||
|
final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4);
|
||||||
|
final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4);
|
||||||
|
final DnsRecord meetA = DnsRecord(type: 'A', name: 'meet', content: ip4);
|
||||||
|
final DnsRecord passwordA =
|
||||||
|
DnsRecord(type: 'A', name: 'password', content: ip4);
|
||||||
|
final DnsRecord socialA =
|
||||||
|
DnsRecord(type: 'A', name: 'social', content: ip4);
|
||||||
|
final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4);
|
||||||
|
|
||||||
|
final DnsRecord txt1 = DnsRecord(
|
||||||
type: 'TXT',
|
type: 'TXT',
|
||||||
name: '_dmarc',
|
name: '_dmarc',
|
||||||
content: 'v=DMARC1; p=none',
|
content: 'v=DMARC1; p=none',
|
||||||
ttl: 18000,
|
ttl: 18000,
|
||||||
);
|
);
|
||||||
|
|
||||||
var txt2 = DnsRecords(
|
final DnsRecord txt2 = DnsRecord(
|
||||||
type: 'TXT',
|
type: 'TXT',
|
||||||
name: domainName,
|
name: domainName,
|
||||||
content: 'v=spf1 a mx ip4:$ip4 -all',
|
content: 'v=spf1 a mx ip4:$ip4 -all',
|
||||||
ttl: 18000,
|
ttl: 18000,
|
||||||
);
|
);
|
||||||
|
|
||||||
return <DnsRecords>[
|
return <DnsRecord>[
|
||||||
domainA,
|
domainA,
|
||||||
apiA,
|
apiA,
|
||||||
cloudA,
|
cloudA,
|
||||||
|
@ -154,24 +219,41 @@ class CloudflareApi extends ApiMap {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> domainList() async {
|
Future<void> setDkim(
|
||||||
var url = '$rootAddress/zones?per_page=50';
|
final String dkimRecordString,
|
||||||
var client = await getClient();
|
final ServerDomain cloudFlareDomain,
|
||||||
|
) async {
|
||||||
|
final String domainZoneId = cloudFlareDomain.zoneId;
|
||||||
|
final String url = '$rootAddress/zones/$domainZoneId/dns_records';
|
||||||
|
|
||||||
var response = await client.get(
|
final DnsRecord dkimRecord = DnsRecord(
|
||||||
|
type: 'TXT',
|
||||||
|
name: 'selector._domainkey',
|
||||||
|
content: dkimRecordString,
|
||||||
|
ttl: 18000,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
await client.post(
|
||||||
|
url,
|
||||||
|
data: dkimRecord.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> domainList() async {
|
||||||
|
final String url = '$rootAddress/zones';
|
||||||
|
final Dio client = await getClient();
|
||||||
|
|
||||||
|
final Response response = await client.get(
|
||||||
url,
|
url,
|
||||||
queryParameters: {'per_page': 50},
|
queryParameters: {'per_page': 50},
|
||||||
);
|
);
|
||||||
|
|
||||||
close(client);
|
close(client);
|
||||||
return response.data['result']
|
return response.data['result']
|
||||||
.map<String>((el) => el['name'] as String)
|
.map<String>((final el) => el['name'] as String)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
final bool hasLoger;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final bool isWithToken;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,21 +4,23 @@ import 'dart:io';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/api_map.dart';
|
import 'package:selfprivacy/logic/api_maps/api_map.dart';
|
||||||
import 'package:selfprivacy/logic/models/hetzner_server_info.dart';
|
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart';
|
||||||
import 'package:selfprivacy/logic/models/server_details.dart';
|
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||||
import 'package:selfprivacy/logic/models/user.dart';
|
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||||
import 'package:selfprivacy/utils/password_generator.dart';
|
import 'package:selfprivacy/utils/password_generator.dart';
|
||||||
|
|
||||||
class HetznerApi extends ApiMap {
|
class HetznerApi extends ApiMap {
|
||||||
bool hasLoger;
|
HetznerApi({this.hasLogger = false, this.isWithToken = true});
|
||||||
|
@override
|
||||||
|
bool hasLogger;
|
||||||
|
@override
|
||||||
bool isWithToken;
|
bool isWithToken;
|
||||||
|
|
||||||
HetznerApi({this.hasLoger = false, this.isWithToken = true});
|
@override
|
||||||
|
|
||||||
BaseOptions get options {
|
BaseOptions get options {
|
||||||
var options = BaseOptions(baseUrl: rootAddress);
|
final BaseOptions options = BaseOptions(baseUrl: rootAddress);
|
||||||
if (isWithToken) {
|
if (isWithToken) {
|
||||||
var token = getIt<ApiConfigModel>().hetznerKey;
|
final String? token = getIt<ApiConfigModel>().hetznerKey;
|
||||||
assert(token != null);
|
assert(token != null);
|
||||||
options.headers = {'Authorization': 'Bearer $token'};
|
options.headers = {'Authorization': 'Bearer $token'};
|
||||||
}
|
}
|
||||||
|
@ -33,12 +35,11 @@ class HetznerApi extends ApiMap {
|
||||||
@override
|
@override
|
||||||
String rootAddress = 'https://api.hetzner.cloud/v1';
|
String rootAddress = 'https://api.hetzner.cloud/v1';
|
||||||
|
|
||||||
Future<bool> isValid(String token) async {
|
Future<bool> isValid(final String token) async {
|
||||||
validateStatus = (status) {
|
validateStatus = (final int? status) =>
|
||||||
return status == HttpStatus.ok || status == HttpStatus.unauthorized;
|
status == HttpStatus.ok || status == HttpStatus.unauthorized;
|
||||||
};
|
final Dio client = await getClient();
|
||||||
var client = await getClient();
|
final Response response = await client.get(
|
||||||
Response response = await client.get(
|
|
||||||
'/servers',
|
'/servers',
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: {'Authorization': 'Bearer $token'},
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
@ -55,100 +56,131 @@ class HetznerApi extends ApiMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isFreeToCreate() async {
|
Future<ServerVolume> createVolume() async {
|
||||||
var client = await getClient();
|
final Dio client = await getClient();
|
||||||
|
final Response dbCreateResponse = await client.post(
|
||||||
Response serversReponse = await client.get('/servers');
|
|
||||||
List servers = serversReponse.data['servers'];
|
|
||||||
var server = servers.firstWhere(
|
|
||||||
(el) => el['name'] == 'selfprivacy-server',
|
|
||||||
orElse: null,
|
|
||||||
);
|
|
||||||
client.close();
|
|
||||||
return server == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<HetznerDataBase> createVolume() async {
|
|
||||||
var client = await getClient();
|
|
||||||
Response dbCreateResponse = await client.post(
|
|
||||||
'/volumes',
|
'/volumes',
|
||||||
data: {
|
data: {
|
||||||
"size": 10,
|
'size': 10,
|
||||||
"name": StringGenerators.dbStorageName(),
|
'name': StringGenerators.dbStorageName(),
|
||||||
"labels": {"labelkey": "value"},
|
'labels': {'labelkey': 'value'},
|
||||||
"location": "fsn1",
|
'location': 'fsn1',
|
||||||
"automount": false,
|
'automount': false,
|
||||||
"format": "ext4"
|
'format': 'ext4'
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
var dbId = dbCreateResponse.data['volume']['id'];
|
final dbId = dbCreateResponse.data['volume']['id'];
|
||||||
return HetznerDataBase(
|
return ServerVolume(
|
||||||
id: dbId,
|
id: dbId,
|
||||||
name: dbCreateResponse.data['volume']['name'],
|
name: dbCreateResponse.data['volume']['name'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<HetznerServerDetails> createServer({
|
Future<ServerHostingDetails?> createServer({
|
||||||
required String cloudFlareKey,
|
required final String cloudFlareKey,
|
||||||
required User rootUser,
|
required final User rootUser,
|
||||||
required String domainName,
|
required final String domainName,
|
||||||
required HetznerDataBase dataBase,
|
required final ServerVolume dataBase,
|
||||||
}) async {
|
}) async {
|
||||||
var client = await getClient();
|
final Dio client = await getClient();
|
||||||
|
|
||||||
// Response dbCreateResponse = await client.post(
|
final String dbPassword = StringGenerators.dbPassword();
|
||||||
// '/volumes',
|
final int dbId = dataBase.id;
|
||||||
// data: {
|
|
||||||
// "size": 10,
|
|
||||||
// "name": StringGenerators.dbStorageName(),
|
|
||||||
// "labels": {"labelkey": "value"},
|
|
||||||
// "location": "fsn1",
|
|
||||||
// "automount": false,
|
|
||||||
// "format": "ext4"
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
|
|
||||||
var dbPassword = StringGenerators.dbPassword();
|
final String apiToken = StringGenerators.apiToken();
|
||||||
// var dbId = dbCreateResponse.data['volume']['id'];
|
|
||||||
var dbId = dataBase.id;
|
final String hostname = getHostnameFromDomain(domainName);
|
||||||
|
|
||||||
|
final String base64Password =
|
||||||
|
base64.encode(utf8.encode(rootUser.password ?? 'PASS'));
|
||||||
|
|
||||||
|
print('hostname: $hostname');
|
||||||
|
|
||||||
/// add ssh key when you need it: e.g. "ssh_keys":["kherel"]
|
/// add ssh key when you need it: e.g. "ssh_keys":["kherel"]
|
||||||
/// check the branch name, it could be "development" or "master".
|
/// check the branch name, it could be "development" or "master".
|
||||||
|
///
|
||||||
|
final String userdataString =
|
||||||
|
"#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' CF_TOKEN=$cloudFlareKey DB_PASSWORD=$dbPassword API_TOKEN=$apiToken HOSTNAME=$hostname bash 2>&1 | tee /tmp/infect.log";
|
||||||
|
print(userdataString);
|
||||||
|
|
||||||
var data = jsonDecode(
|
final Map<String, Object> data = {
|
||||||
'''{"name":"$domainName","server_type":"cx11","start_after_create":false,"image":"ubuntu-20.04", "volumes":[$dbId], "networks":[], "user_data":"#cloud-config\\nruncmd:\\n- curl https://git.selfprivacy.org/ilchub/selfprivacy-nixos-infect/raw/branch/development/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-21.05 DOMAIN=$domainName LUSER=${rootUser.login} PASSWORD=${rootUser.password} CF_TOKEN=$cloudFlareKey DB_PASSWORD=$dbPassword bash 2>&1 | tee /tmp/infect.log","labels":{},"automount":true, "location": "fsn1"}''');
|
'name': hostname,
|
||||||
|
'server_type': 'cx11',
|
||||||
|
'start_after_create': false,
|
||||||
|
'image': 'ubuntu-20.04',
|
||||||
|
'volumes': [dbId],
|
||||||
|
'networks': [],
|
||||||
|
'user_data': userdataString,
|
||||||
|
'labels': {},
|
||||||
|
'automount': true,
|
||||||
|
'location': 'fsn1'
|
||||||
|
};
|
||||||
|
print('Decoded data: $data');
|
||||||
|
|
||||||
Response serverCreateResponse = await client.post(
|
ServerHostingDetails? serverDetails;
|
||||||
'/servers',
|
|
||||||
data: data,
|
|
||||||
);
|
|
||||||
|
|
||||||
client.close();
|
try {
|
||||||
return HetznerServerDetails(
|
final Response serverCreateResponse = await client.post(
|
||||||
id: serverCreateResponse.data['server']['id'],
|
'/servers',
|
||||||
ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'],
|
data: data,
|
||||||
createTime: DateTime.now(),
|
);
|
||||||
dataBase: dataBase,
|
print(serverCreateResponse.data);
|
||||||
);
|
serverDetails = ServerHostingDetails(
|
||||||
|
id: serverCreateResponse.data['server']['id'],
|
||||||
|
ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'],
|
||||||
|
createTime: DateTime.now(),
|
||||||
|
volume: dataBase,
|
||||||
|
apiToken: apiToken,
|
||||||
|
provider: ServerProvider.hetzner,
|
||||||
|
);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e);
|
||||||
|
rethrow;
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getHostnameFromDomain(final String domain) {
|
||||||
|
// Replace all non-alphanumeric characters with an underscore
|
||||||
|
String hostname =
|
||||||
|
domain.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
|
||||||
|
if (hostname.endsWith('-')) {
|
||||||
|
hostname = hostname.substring(0, hostname.length - 1);
|
||||||
|
}
|
||||||
|
if (hostname.startsWith('-')) {
|
||||||
|
hostname = hostname.substring(1);
|
||||||
|
}
|
||||||
|
if (hostname.isEmpty) {
|
||||||
|
hostname = 'selfprivacy-server';
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteSelfprivacyServerAndAllVolumes({
|
Future<void> deleteSelfprivacyServerAndAllVolumes({
|
||||||
required String domainName,
|
required final String domainName,
|
||||||
}) async {
|
}) async {
|
||||||
var client = await getClient();
|
final Dio client = await getClient();
|
||||||
|
|
||||||
Response serversReponse = await client.get('/servers');
|
final String hostname = getHostnameFromDomain(domainName);
|
||||||
List servers = serversReponse.data['servers'];
|
|
||||||
Map server = servers.firstWhere((el) => el['name'] == domainName);
|
|
||||||
List volumes = server['volumes'];
|
|
||||||
var laterFutures = <Future>[];
|
|
||||||
|
|
||||||
for (var volumeId in volumes) {
|
final Response serversReponse = await client.get('/servers');
|
||||||
|
final List servers = serversReponse.data['servers'];
|
||||||
|
final Map server = servers.firstWhere((final el) => el['name'] == hostname);
|
||||||
|
final List volumes = server['volumes'];
|
||||||
|
final List<Future> laterFutures = <Future>[];
|
||||||
|
|
||||||
|
for (final volumeId in volumes) {
|
||||||
await client.post('/volumes/$volumeId/actions/detach');
|
await client.post('/volumes/$volumeId/actions/detach');
|
||||||
}
|
}
|
||||||
await Future.delayed(Duration(seconds: 10));
|
await Future.delayed(const Duration(seconds: 10));
|
||||||
|
|
||||||
for (var volumeId in volumes) {
|
for (final volumeId in volumes) {
|
||||||
laterFutures.add(client.delete('/volumes/$volumeId'));
|
laterFutures.add(client.delete('/volumes/$volumeId'));
|
||||||
}
|
}
|
||||||
laterFutures.add(client.delete('/servers/${server['id']}'));
|
laterFutures.add(client.delete('/servers/${server['id']}'));
|
||||||
|
@ -157,20 +189,20 @@ class HetznerApi extends ApiMap {
|
||||||
close(client);
|
close(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<HetznerServerDetails> reset() async {
|
Future<ServerHostingDetails> reset() async {
|
||||||
var server = getIt<ApiConfigModel>().hetznerServer!;
|
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
|
||||||
|
|
||||||
var client = await getClient();
|
final Dio client = await getClient();
|
||||||
await client.post('/servers/${server.id}/actions/reset');
|
await client.post('/servers/${server.id}/actions/reset');
|
||||||
close(client);
|
close(client);
|
||||||
|
|
||||||
return server.copyWith(startTime: DateTime.now());
|
return server.copyWith(startTime: DateTime.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<HetznerServerDetails> powerOn() async {
|
Future<ServerHostingDetails> powerOn() async {
|
||||||
var server = getIt<ApiConfigModel>().hetznerServer!;
|
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
|
||||||
|
|
||||||
var client = await getClient();
|
final Dio client = await getClient();
|
||||||
await client.post('/servers/${server.id}/actions/poweron');
|
await client.post('/servers/${server.id}/actions/poweron');
|
||||||
close(client);
|
close(client);
|
||||||
|
|
||||||
|
@ -178,16 +210,20 @@ class HetznerApi extends ApiMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getMetrics(
|
Future<Map<String, dynamic>> getMetrics(
|
||||||
DateTime start, DateTime end, String type) async {
|
final DateTime start,
|
||||||
var hetznerServer = getIt<ApiConfigModel>().hetznerServer;
|
final DateTime end,
|
||||||
var client = await getClient();
|
final String type,
|
||||||
|
) async {
|
||||||
|
final ServerHostingDetails? hetznerServer =
|
||||||
|
getIt<ApiConfigModel>().serverDetails;
|
||||||
|
final Dio client = await getClient();
|
||||||
|
|
||||||
Map<String, dynamic> queryParameters = {
|
final Map<String, dynamic> queryParameters = {
|
||||||
"start": start.toUtc().toIso8601String(),
|
'start': start.toUtc().toIso8601String(),
|
||||||
"end": end.toUtc().toIso8601String(),
|
'end': end.toUtc().toIso8601String(),
|
||||||
"type": type
|
'type': type
|
||||||
};
|
};
|
||||||
var res = await client.get(
|
final Response res = await client.get(
|
||||||
'/servers/${hetznerServer!.id}/metrics',
|
'/servers/${hetznerServer!.id}/metrics',
|
||||||
queryParameters: queryParameters,
|
queryParameters: queryParameters,
|
||||||
);
|
);
|
||||||
|
@ -196,27 +232,46 @@ class HetznerApi extends ApiMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<HetznerServerInfo> getInfo() async {
|
Future<HetznerServerInfo> getInfo() async {
|
||||||
var hetznerServer = getIt<ApiConfigModel>().hetznerServer;
|
final ServerHostingDetails? hetznerServer =
|
||||||
var client = await getClient();
|
getIt<ApiConfigModel>().serverDetails;
|
||||||
Response response = await client.get('/servers/${hetznerServer!.id}');
|
final Dio client = await getClient();
|
||||||
|
final Response response = await client.get('/servers/${hetznerServer!.id}');
|
||||||
close(client);
|
close(client);
|
||||||
|
|
||||||
return HetznerServerInfo.fromJson(response.data!['server']);
|
return HetznerServerInfo.fromJson(response.data!['server']);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createReverseDns({
|
Future<List<HetznerServerInfo>> getServers() async {
|
||||||
required String ip4,
|
final Dio client = await getClient();
|
||||||
required String domainName,
|
final Response response = await client.get('/servers');
|
||||||
}) async {
|
|
||||||
var hetznerServer = getIt<ApiConfigModel>().hetznerServer;
|
|
||||||
var client = await getClient();
|
|
||||||
await client.post(
|
|
||||||
'/servers/${hetznerServer!.id}/actions/change_dns_ptr',
|
|
||||||
data: {
|
|
||||||
"ip": ip4,
|
|
||||||
"dns_ptr": domainName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
close(client);
|
close(client);
|
||||||
|
|
||||||
|
return (response.data!['servers'] as List)
|
||||||
|
// ignore: unnecessary_lambdas
|
||||||
|
.map((final e) => HetznerServerInfo.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createReverseDns({
|
||||||
|
required final String ip4,
|
||||||
|
required final String domainName,
|
||||||
|
}) async {
|
||||||
|
final ServerHostingDetails? hetznerServer =
|
||||||
|
getIt<ApiConfigModel>().serverDetails;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
await client.post(
|
||||||
|
'/servers/${hetznerServer!.id}/actions/change_dns_ptr',
|
||||||
|
data: {
|
||||||
|
'ip': ip4,
|
||||||
|
'dns_ptr': domainName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,116 +1,407 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
||||||
import 'package:selfprivacy/logic/models/user.dart';
|
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/api_token.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/auto_upgrade_settings.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/backup.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/device_token.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/timezone_settings.dart';
|
||||||
|
|
||||||
import 'api_map.dart';
|
import 'package:selfprivacy/logic/api_maps/api_map.dart';
|
||||||
|
|
||||||
|
class ApiResponse<D> {
|
||||||
|
ApiResponse({
|
||||||
|
required this.statusCode,
|
||||||
|
required this.data,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
final int statusCode;
|
||||||
|
final String? errorMessage;
|
||||||
|
final D data;
|
||||||
|
|
||||||
|
bool get isSuccess => statusCode >= 200 && statusCode < 300;
|
||||||
|
}
|
||||||
|
|
||||||
class ServerApi extends ApiMap {
|
class ServerApi extends ApiMap {
|
||||||
bool hasLoger;
|
ServerApi({
|
||||||
|
this.hasLogger = false,
|
||||||
|
this.isWithToken = true,
|
||||||
|
this.overrideDomain,
|
||||||
|
this.customToken,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
bool hasLogger;
|
||||||
|
@override
|
||||||
bool isWithToken;
|
bool isWithToken;
|
||||||
|
String? overrideDomain;
|
||||||
|
String? customToken;
|
||||||
|
|
||||||
ServerApi({this.hasLoger = false, this.isWithToken = true});
|
@override
|
||||||
|
|
||||||
BaseOptions get options {
|
BaseOptions get options {
|
||||||
var options = BaseOptions();
|
BaseOptions options = BaseOptions();
|
||||||
|
|
||||||
if (isWithToken) {
|
if (isWithToken) {
|
||||||
var cloudFlareDomain = getIt<ApiConfigModel>().cloudFlareDomain;
|
final ServerDomain? cloudFlareDomain =
|
||||||
var domainName = cloudFlareDomain!.domainName;
|
getIt<ApiConfigModel>().serverDomain;
|
||||||
|
final String domainName = cloudFlareDomain!.domainName;
|
||||||
|
final String? apiToken = getIt<ApiConfigModel>().serverDetails?.apiToken;
|
||||||
|
|
||||||
options = BaseOptions(baseUrl: 'https://api.$domainName');
|
options = BaseOptions(
|
||||||
|
baseUrl: 'https://api.$domainName',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $apiToken',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrideDomain != null) {
|
||||||
|
options = BaseOptions(
|
||||||
|
baseUrl: 'https://api.$overrideDomain',
|
||||||
|
headers: customToken != null
|
||||||
|
? {'Authorization': 'Bearer $customToken'}
|
||||||
|
: null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isHttpServerWorking() async {
|
Future<String?> getApiVersion() async {
|
||||||
bool res;
|
|
||||||
Response response;
|
Response response;
|
||||||
|
|
||||||
var client = await getClient();
|
final Dio client = await getClient();
|
||||||
|
String? apiVersion;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await client.get('/api/version');
|
||||||
|
apiVersion = response.data['version'];
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
return apiVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isHttpServerWorking() async {
|
||||||
|
bool res = false;
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
try {
|
try {
|
||||||
response = await client.get('/services/status');
|
response = await client.get('/services/status');
|
||||||
res = response.statusCode == HttpStatus.ok;
|
res = response.statusCode == HttpStatus.ok;
|
||||||
} catch (e) {
|
} on DioError catch (e) {
|
||||||
res = false;
|
print(e.message);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
}
|
}
|
||||||
close(client);
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> createUser(User user) async {
|
Future<ApiResponse<User>> createUser(final User user) async {
|
||||||
bool res;
|
|
||||||
Response response;
|
Response response;
|
||||||
|
|
||||||
var client = await getClient();
|
final Dio client = await getClient();
|
||||||
try {
|
try {
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
'/users/create',
|
'/users',
|
||||||
options: Options(
|
data: {
|
||||||
headers: {
|
'username': user.login,
|
||||||
"X-User": user.login,
|
'password': user.password,
|
||||||
"X-Password": user.password,
|
},
|
||||||
"X-Domain": getIt<ApiConfigModel>().cloudFlareDomain!.domainName
|
);
|
||||||
},
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse(
|
||||||
|
errorMessage: e.error.toString(),
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: User(
|
||||||
|
login: user.login,
|
||||||
|
password: user.password,
|
||||||
|
isFoundOnServer: false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
res = response.statusCode == HttpStatus.ok;
|
} finally {
|
||||||
} catch (e) {
|
close(client);
|
||||||
print(e);
|
|
||||||
res = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close(client);
|
bool isFoundOnServer = false;
|
||||||
|
int code = 0;
|
||||||
|
|
||||||
|
final bool isUserCreated = (response.statusCode != null) &&
|
||||||
|
(response.statusCode == HttpStatus.created);
|
||||||
|
|
||||||
|
if (isUserCreated) {
|
||||||
|
isFoundOnServer = true;
|
||||||
|
code = response.statusCode!;
|
||||||
|
} else {
|
||||||
|
isFoundOnServer = false;
|
||||||
|
code = HttpStatus.notAcceptable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse(
|
||||||
|
statusCode: code,
|
||||||
|
data: User(
|
||||||
|
login: user.login,
|
||||||
|
password: user.password,
|
||||||
|
isFoundOnServer: isFoundOnServer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<List<String>>> getUsersList({
|
||||||
|
final withMainUser = false,
|
||||||
|
}) async {
|
||||||
|
final List<String> res = [];
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.get(
|
||||||
|
'/users',
|
||||||
|
queryParameters: withMainUser ? {'withMainUser': 'true'} : null,
|
||||||
|
);
|
||||||
|
for (final user in response.data) {
|
||||||
|
res.add(user.toString());
|
||||||
|
}
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: [],
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
return ApiResponse(
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
statusCode: HttpStatus.internalServerError,
|
||||||
|
data: [],
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
|
||||||
|
return ApiResponse(
|
||||||
|
statusCode: code,
|
||||||
|
data: res,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<void>> addUserSshKey(
|
||||||
|
final User user,
|
||||||
|
final String sshKey,
|
||||||
|
) async {
|
||||||
|
late Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.post(
|
||||||
|
'/services/ssh/keys/${user.login}',
|
||||||
|
data: {
|
||||||
|
'public_key': sshKey,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse<void>(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: null,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
|
||||||
|
return ApiResponse<void>(
|
||||||
|
statusCode: code,
|
||||||
|
data: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<void>> addRootSshKey(final String ssh) async {
|
||||||
|
late Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.put(
|
||||||
|
'/services/ssh/key/send',
|
||||||
|
data: {'public_key': ssh},
|
||||||
|
);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse<void>(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: null,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
|
||||||
|
return ApiResponse<void>(
|
||||||
|
statusCode: code,
|
||||||
|
data: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<List<String>>> getUserSshKeys(final User user) async {
|
||||||
|
List<String> res;
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.get('/services/ssh/keys/${user.login}');
|
||||||
|
res = (response.data as List<dynamic>)
|
||||||
|
.map((final e) => e as String)
|
||||||
|
.toList();
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse<List<String>>(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: [],
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return ApiResponse<List<String>>(
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
statusCode: HttpStatus.internalServerError,
|
||||||
|
data: [],
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
|
||||||
|
return ApiResponse<List<String>>(
|
||||||
|
statusCode: code,
|
||||||
|
data: res,
|
||||||
|
errorMessage: response.data is List
|
||||||
|
? null
|
||||||
|
: response.data?.containsKey('error') ?? false
|
||||||
|
? response.data['error']
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<void>> deleteUserSshKey(
|
||||||
|
final User user,
|
||||||
|
final String sshKey,
|
||||||
|
) async {
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.delete(
|
||||||
|
'/services/ssh/keys/${user.login}',
|
||||||
|
data: {'public_key': sshKey},
|
||||||
|
);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse<void>(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: null,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
|
||||||
|
return ApiResponse<void>(
|
||||||
|
statusCode: code,
|
||||||
|
data: null,
|
||||||
|
errorMessage: response.data?.containsKey('error') ?? false
|
||||||
|
? response.data['error']
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteUser(final User user) async {
|
||||||
|
bool res = false;
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.delete('/users/${user.login}');
|
||||||
|
res = response.statusCode == HttpStatus.ok ||
|
||||||
|
response.statusCode == HttpStatus.notFound;
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
res = false;
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
String get rootAddress =>
|
String get rootAddress =>
|
||||||
throw UnimplementedError('not used in with implementation');
|
throw UnimplementedError('not used in with implementation');
|
||||||
|
|
||||||
Future<bool> apply() async {
|
Future<bool> apply() async {
|
||||||
bool res;
|
bool res = false;
|
||||||
Response response;
|
Response response;
|
||||||
|
|
||||||
var client = await getClient();
|
final Dio client = await getClient();
|
||||||
try {
|
try {
|
||||||
response = await client.get(
|
response = await client.get('/system/configuration/apply');
|
||||||
'/system/configuration/apply',
|
|
||||||
);
|
|
||||||
|
|
||||||
res = response.statusCode == HttpStatus.ok;
|
res = response.statusCode == HttpStatus.ok;
|
||||||
} catch (e) {
|
} on DioError catch (e) {
|
||||||
print(e);
|
print(e.message);
|
||||||
res = false;
|
res = false;
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
close(client);
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> switchService(ServiceTypes type, bool needToTurnOn) async {
|
Future<void> switchService(
|
||||||
var client = await getClient();
|
final ServiceTypes type,
|
||||||
client.post('/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}');
|
final bool needToTurnOn,
|
||||||
client.close();
|
) async {
|
||||||
}
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
Future<void> sendSsh(String ssh) async {
|
client.post(
|
||||||
var client = await getClient();
|
'/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}',
|
||||||
client.post(
|
);
|
||||||
'/services/ssh/enable',
|
} on DioError catch (e) {
|
||||||
data: {"public_key": ssh},
|
print(e.message);
|
||||||
);
|
} finally {
|
||||||
client.close();
|
close(client);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<ServiceTypes, bool>> servicesPowerCheck() async {
|
Future<Map<ServiceTypes, bool>> servicesPowerCheck() async {
|
||||||
var client = await getClient();
|
Response response;
|
||||||
Response response = await client.get('/services/status');
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.get('/services/status');
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return {};
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
|
||||||
close(client);
|
|
||||||
return {
|
return {
|
||||||
ServiceTypes.passwordManager: response.data['bitwarden'] == 0,
|
ServiceTypes.passwordManager: response.data['bitwarden'] == 0,
|
||||||
ServiceTypes.git: response.data['gitea'] == 0,
|
ServiceTypes.git: response.data['gitea'] == 0,
|
||||||
|
@ -119,17 +410,510 @@ class ServerApi extends ApiMap {
|
||||||
ServiceTypes.socialNetwork: response.data['pleroma'] == 0,
|
ServiceTypes.socialNetwork: response.data['pleroma'] == 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> uploadBackblazeConfig(final BackblazeBucket bucket) async {
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
client.put(
|
||||||
|
'/services/restic/backblaze/config',
|
||||||
|
data: {
|
||||||
|
'accountId': bucket.applicationKeyId,
|
||||||
|
'accountKey': bucket.applicationKey,
|
||||||
|
'bucket': bucket.bucketName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> startBackup() async {
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
client.put('/services/restic/backup/create');
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Backup>> getBackups() async {
|
||||||
|
Response response;
|
||||||
|
List<Backup> backups = [];
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.get('/services/restic/backup/list');
|
||||||
|
backups =
|
||||||
|
response.data.map<Backup>((final e) => Backup.fromJson(e)).toList();
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
return backups;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<BackupStatus> getBackupStatus() async {
|
||||||
|
Response response;
|
||||||
|
BackupStatus status = BackupStatus(
|
||||||
|
status: BackupStatusEnum.error,
|
||||||
|
errorMessage: 'Network error',
|
||||||
|
progress: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.get('/services/restic/backup/status');
|
||||||
|
status = BackupStatus.fromJson(response.data);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> forceBackupListReload() async {
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
client.get('/services/restic/backup/reload');
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> restoreBackup(final String backupId) async {
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
client.put(
|
||||||
|
'/services/restic/backup/restore',
|
||||||
|
data: {'backupId': backupId},
|
||||||
|
);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> pullConfigurationUpdate() async {
|
||||||
|
Response response;
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.get('/system/configuration/pull');
|
||||||
|
result = (response.statusCode != null)
|
||||||
|
? (response.statusCode == HttpStatus.ok)
|
||||||
|
: false;
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> reboot() async {
|
||||||
|
Response response;
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.get('/system/reboot');
|
||||||
|
result = (response.statusCode != null)
|
||||||
|
? (response.statusCode == HttpStatus.ok)
|
||||||
|
: false;
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> upgrade() async {
|
||||||
|
Response response;
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.get('/system/configuration/upgrade');
|
||||||
|
result = (response.statusCode != null)
|
||||||
|
? (response.statusCode == HttpStatus.ok)
|
||||||
|
: false;
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AutoUpgradeSettings> getAutoUpgradeSettings() async {
|
||||||
|
Response response;
|
||||||
|
AutoUpgradeSettings settings = const AutoUpgradeSettings(
|
||||||
|
enable: false,
|
||||||
|
allowReboot: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.get('/system/configuration/autoUpgrade');
|
||||||
|
if (response.data != null) {
|
||||||
|
settings = AutoUpgradeSettings.fromJson(response.data);
|
||||||
|
}
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateAutoUpgradeSettings(
|
||||||
|
final AutoUpgradeSettings settings,
|
||||||
|
) async {
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
await client.put(
|
||||||
|
'/system/configuration/autoUpgrade',
|
||||||
|
data: settings.toJson(),
|
||||||
|
);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<TimeZoneSettings> getServerTimezone() async {
|
||||||
|
// I am not sure how to initialize TimeZoneSettings with default value...
|
||||||
|
final Dio client = await getClient();
|
||||||
|
final Response response =
|
||||||
|
await client.get('/system/configuration/timezone');
|
||||||
|
close(client);
|
||||||
|
|
||||||
|
return TimeZoneSettings.fromString(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateServerTimezone(final TimeZoneSettings settings) async {
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
await client.put(
|
||||||
|
'/system/configuration/timezone',
|
||||||
|
data: settings.toString(),
|
||||||
|
);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getDkim() async {
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.get('/services/mailserver/dkim');
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode == HttpStatus.notFound || response.data == null) {
|
||||||
|
throw Exception('No DKIM key found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode != HttpStatus.ok) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
final Codec<String, String> base64toString = utf8.fuse(base64);
|
||||||
|
|
||||||
|
return base64toString
|
||||||
|
.decode(response.data)
|
||||||
|
.split('(')[1]
|
||||||
|
.split(')')[0]
|
||||||
|
.replaceAll('"', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<RecoveryKeyStatus?>> getRecoveryTokenStatus() async {
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.get('/auth/recovery_token');
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: const RecoveryKeyStatus(exists: false, valid: false),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
|
||||||
|
return ApiResponse(
|
||||||
|
statusCode: code,
|
||||||
|
data: response.data != null
|
||||||
|
? RecoveryKeyStatus.fromJson(response.data)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<String>> generateRecoveryToken(
|
||||||
|
final DateTime? expiration,
|
||||||
|
final int? uses,
|
||||||
|
) async {
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
final Map data = {};
|
||||||
|
if (expiration != null) {
|
||||||
|
data['expiration'] = '${expiration.toIso8601String()}Z';
|
||||||
|
print(data['expiration']);
|
||||||
|
}
|
||||||
|
if (uses != null) {
|
||||||
|
data['uses'] = uses;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
response = await client.post(
|
||||||
|
'/auth/recovery_token',
|
||||||
|
data: data,
|
||||||
|
);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: '',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
|
||||||
|
return ApiResponse(
|
||||||
|
statusCode: code,
|
||||||
|
data: response.data != null ? response.data['token'] : '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<String>> useRecoveryToken(final DeviceToken token) async {
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.post(
|
||||||
|
'/auth/recovery_token/use',
|
||||||
|
data: {
|
||||||
|
'token': token.token,
|
||||||
|
'device': token.device,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: '',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
|
||||||
|
return ApiResponse(
|
||||||
|
statusCode: code,
|
||||||
|
data: response.data != null ? response.data['token'] : '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<String>> authorizeDevice(final DeviceToken token) async {
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.post(
|
||||||
|
'/auth/new_device/authorize',
|
||||||
|
data: {
|
||||||
|
'token': token.token,
|
||||||
|
'device': token.device,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: '',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
|
||||||
|
return ApiResponse(statusCode: code, data: response.data['token'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<String>> createDeviceToken() async {
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.post('/auth/new_device');
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: '',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
|
||||||
|
return ApiResponse(
|
||||||
|
statusCode: code,
|
||||||
|
data: response.data != null ? response.data['token'] : '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<String>> deleteDeviceToken() async {
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.delete('/auth/new_device');
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: '',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
|
||||||
|
return ApiResponse(statusCode: code, data: response.data ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<List<ApiToken>>> getApiTokens() async {
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.get('/auth/tokens');
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: [],
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
|
||||||
|
return ApiResponse(
|
||||||
|
statusCode: code,
|
||||||
|
data: (response.data != null)
|
||||||
|
? response.data
|
||||||
|
.map<ApiToken>((final e) => ApiToken.fromJson(e))
|
||||||
|
.toList()
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<String>> refreshCurrentApiToken() async {
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.post('/auth/tokens');
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: '',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
|
||||||
|
return ApiResponse(
|
||||||
|
statusCode: code,
|
||||||
|
data: response.data != null ? response.data['token'] : '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<void>> deleteApiToken(final String device) async {
|
||||||
|
Response response;
|
||||||
|
final Dio client = await getClient();
|
||||||
|
try {
|
||||||
|
response = await client.delete(
|
||||||
|
'/auth/tokens',
|
||||||
|
data: {
|
||||||
|
'token_name': device,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
print(e.message);
|
||||||
|
return ApiResponse(
|
||||||
|
errorMessage: e.message,
|
||||||
|
statusCode: e.response?.statusCode ?? HttpStatus.internalServerError,
|
||||||
|
data: null,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int code = response.statusCode ?? HttpStatus.internalServerError;
|
||||||
|
return ApiResponse(statusCode: code, data: null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UrlServerExt on ServiceTypes {
|
extension UrlServerExt on ServiceTypes {
|
||||||
String get url {
|
String get url {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
// case ServiceTypes.mail:
|
// case ServiceTypes.mail:
|
||||||
// return ''; // cannot be swithch off
|
// return ''; // cannot be switch off
|
||||||
// case ServiceTypes.messenger:
|
// case ServiceTypes.messenger:
|
||||||
// return ''; // external service
|
// return ''; // external service
|
||||||
// case ServiceTypes.video:
|
// case ServiceTypes.video:
|
||||||
// return ''; // jeetsu meet not working
|
// return ''; // jitsi meet not working
|
||||||
case ServiceTypes.passwordManager:
|
case ServiceTypes.passwordManager:
|
||||||
return 'bitwarden';
|
return 'bitwarden';
|
||||||
case ServiceTypes.cloud:
|
case ServiceTypes.cloud:
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:ionicons/ionicons.dart';
|
|
||||||
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
||||||
|
|
||||||
|
enum LoadingStatus {
|
||||||
|
uninitialized,
|
||||||
|
refreshing,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
enum InitializingSteps {
|
enum InitializingSteps {
|
||||||
setHeznerKey,
|
setHetznerKey,
|
||||||
setCloudFlareKey,
|
setCloudFlareKey,
|
||||||
setDomainName,
|
setDomainName,
|
||||||
setRootUser,
|
setRootUser,
|
||||||
|
@ -13,6 +19,7 @@ enum InitializingSteps {
|
||||||
startServer,
|
startServer,
|
||||||
checkSystemDnsAndDkimSet,
|
checkSystemDnsAndDkimSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Period { hour, day, month }
|
enum Period { hour, day, month }
|
||||||
|
|
||||||
enum ServiceTypes {
|
enum ServiceTypes {
|
||||||
|
@ -69,6 +76,46 @@ extension ServiceTypesExt on ServiceTypes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get loginInfo {
|
||||||
|
switch (this) {
|
||||||
|
case ServiceTypes.mail:
|
||||||
|
return 'services.mail.login_info'.tr();
|
||||||
|
case ServiceTypes.messenger:
|
||||||
|
return 'services.messenger.login_info'.tr();
|
||||||
|
case ServiceTypes.passwordManager:
|
||||||
|
return 'services.password_manager.login_info'.tr();
|
||||||
|
case ServiceTypes.video:
|
||||||
|
return 'services.video.login_info'.tr();
|
||||||
|
case ServiceTypes.cloud:
|
||||||
|
return 'services.cloud.login_info'.tr();
|
||||||
|
case ServiceTypes.socialNetwork:
|
||||||
|
return 'services.social_network.login_info'.tr();
|
||||||
|
case ServiceTypes.git:
|
||||||
|
return 'services.git.login_info'.tr();
|
||||||
|
case ServiceTypes.vpn:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get subdomain {
|
||||||
|
switch (this) {
|
||||||
|
case ServiceTypes.passwordManager:
|
||||||
|
return 'password';
|
||||||
|
case ServiceTypes.video:
|
||||||
|
return 'meet';
|
||||||
|
case ServiceTypes.cloud:
|
||||||
|
return 'cloud';
|
||||||
|
case ServiceTypes.socialNetwork:
|
||||||
|
return 'social';
|
||||||
|
case ServiceTypes.git:
|
||||||
|
return 'git';
|
||||||
|
case ServiceTypes.vpn:
|
||||||
|
case ServiceTypes.messenger:
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IconData get icon {
|
IconData get icon {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case ServiceTypes.mail:
|
case ServiceTypes.mail:
|
||||||
|
@ -86,9 +133,9 @@ extension ServiceTypesExt on ServiceTypes {
|
||||||
case ServiceTypes.git:
|
case ServiceTypes.git:
|
||||||
return BrandIcons.git;
|
return BrandIcons.git;
|
||||||
case ServiceTypes.vpn:
|
case ServiceTypes.vpn:
|
||||||
return Ionicons.shield_checkmark_outline;
|
return Icons.vpn_lock_outlined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String get txt => this.toString().split('.')[1];
|
String get txt => toString().split('.')[1];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,366 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
|
||||||
import 'package:selfprivacy/logic/get_it/ssh.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/backblaze_credential.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/cloudflare_domain.dart';
|
|
||||||
|
|
||||||
import 'package:selfprivacy/logic/models/server_details.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/user.dart';
|
|
||||||
|
|
||||||
import 'app_config_repository.dart';
|
|
||||||
export 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
part 'app_config_state.dart';
|
|
||||||
|
|
||||||
/// Initializing steps:
|
|
||||||
///
|
|
||||||
/// The set phase.
|
|
||||||
/// 1.1. Hetzner key |setHetznerKey
|
|
||||||
/// 1.2. Cloudflare key |setCloudflareKey
|
|
||||||
/// 1.3. Backblaze Id + Key |setBackblazeKey
|
|
||||||
/// 1.4. Set Domain address |setDomain
|
|
||||||
/// 1.5. Set Root user name password |setRootUser
|
|
||||||
/// 1.6. Set Create server ans set DNS-Records |createServerAndSetDnsRecords
|
|
||||||
/// (without start)
|
|
||||||
///
|
|
||||||
/// The check phase.
|
|
||||||
///
|
|
||||||
/// 2.1. a. wait 60sec checkDnsAndStartServer |startServerIfDnsIsOkay
|
|
||||||
/// b. checkDns
|
|
||||||
/// c. if dns is okay start server
|
|
||||||
///
|
|
||||||
/// 2.2. a. wait 60sec |resetServerIfServerIsOkay
|
|
||||||
/// b. checkServer
|
|
||||||
/// c. if server is ok wait 30 sec
|
|
||||||
/// d. reset server
|
|
||||||
///
|
|
||||||
/// 2.3. a. wait 60sec |oneMoreReset()
|
|
||||||
/// d. reset server
|
|
||||||
///
|
|
||||||
/// 2.4. a. wait 30sec |finishCheckIfServerIsOkay
|
|
||||||
/// b. checkServer
|
|
||||||
/// c. if server is okay set that fully checked
|
|
||||||
|
|
||||||
class AppConfigCubit extends Cubit<AppConfigState> {
|
|
||||||
AppConfigCubit() : super(AppConfigEmpty());
|
|
||||||
|
|
||||||
final repository = AppConfigRepository();
|
|
||||||
|
|
||||||
Future<void> load() async {
|
|
||||||
var state = await repository.load();
|
|
||||||
|
|
||||||
if (state is AppConfigFinished) {
|
|
||||||
emit(state);
|
|
||||||
} else if (state is AppConfigNotFinished) {
|
|
||||||
if (state.progress == 6) {
|
|
||||||
startServerIfDnsIsOkay(state: state, isImmediate: true);
|
|
||||||
} else if (state.progress == 7) {
|
|
||||||
resetServerIfServerIsOkay(state: state, isImmediate: true);
|
|
||||||
} else if (state.progress == 8) {
|
|
||||||
oneMoreReset(state: state, isImmediate: true);
|
|
||||||
} else if (state.progress == 9) {
|
|
||||||
finishCheckIfServerIsOkay(state: state, isImmediate: true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw 'wrong state';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void startServerIfDnsIsOkay({
|
|
||||||
AppConfigNotFinished? state,
|
|
||||||
bool isImmediate = false,
|
|
||||||
}) async {
|
|
||||||
state = state ?? this.state as AppConfigNotFinished;
|
|
||||||
|
|
||||||
final work = () async {
|
|
||||||
emit(TimerState(dataState: state!, isLoading: true));
|
|
||||||
|
|
||||||
var ip4 = state.hetznerServer!.ip4;
|
|
||||||
var domainName = state.cloudFlareDomain!.domainName;
|
|
||||||
|
|
||||||
var isMatch = await repository.isDnsAddressesMatch(domainName, ip4);
|
|
||||||
|
|
||||||
if (isMatch) {
|
|
||||||
var server = await repository.startServer(
|
|
||||||
state.hetznerServer!,
|
|
||||||
);
|
|
||||||
await repository.saveServerDetails(server);
|
|
||||||
await repository.saveIsServerStarted(true);
|
|
||||||
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
isServerStarted: true,
|
|
||||||
isLoading: false,
|
|
||||||
hetznerServer: server,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
resetServerIfServerIsOkay();
|
|
||||||
} else {
|
|
||||||
startServerIfDnsIsOkay();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isImmediate) {
|
|
||||||
work();
|
|
||||||
} else {
|
|
||||||
var pauseDuration = Duration(seconds: 60);
|
|
||||||
emit(TimerState(
|
|
||||||
dataState: state,
|
|
||||||
timerStart: DateTime.now(),
|
|
||||||
duration: pauseDuration,
|
|
||||||
isLoading: false,
|
|
||||||
));
|
|
||||||
timer = Timer(pauseDuration, work);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void oneMoreReset({
|
|
||||||
AppConfigNotFinished? state,
|
|
||||||
bool isImmediate = false,
|
|
||||||
}) async {
|
|
||||||
var dataState = state ?? this.state as AppConfigNotFinished;
|
|
||||||
|
|
||||||
var work = () async {
|
|
||||||
emit(TimerState(dataState: dataState, isLoading: true));
|
|
||||||
|
|
||||||
var isServerWorking = await repository.isHttpServerWorking();
|
|
||||||
|
|
||||||
if (isServerWorking) {
|
|
||||||
var pauseDuration = Duration(seconds: 30);
|
|
||||||
emit(TimerState(
|
|
||||||
dataState: dataState,
|
|
||||||
timerStart: DateTime.now(),
|
|
||||||
isLoading: false,
|
|
||||||
duration: pauseDuration,
|
|
||||||
));
|
|
||||||
timer = Timer(pauseDuration, () async {
|
|
||||||
var hetznerServerDetails = await repository.restart();
|
|
||||||
await repository.saveIsServerResetedSecondTime(true);
|
|
||||||
await repository.saveServerDetails(hetznerServerDetails);
|
|
||||||
|
|
||||||
emit(
|
|
||||||
dataState.copyWith(
|
|
||||||
isServerResetedSecondTime: true,
|
|
||||||
hetznerServer: hetznerServerDetails,
|
|
||||||
isLoading: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
finishCheckIfServerIsOkay();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
oneMoreReset();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (isImmediate) {
|
|
||||||
work();
|
|
||||||
} else {
|
|
||||||
var pauseDuration = Duration(seconds: 60);
|
|
||||||
emit(
|
|
||||||
TimerState(
|
|
||||||
dataState: dataState,
|
|
||||||
timerStart: DateTime.now(),
|
|
||||||
duration: pauseDuration,
|
|
||||||
isLoading: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
timer = Timer(pauseDuration, work);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void resetServerIfServerIsOkay({
|
|
||||||
AppConfigNotFinished? state,
|
|
||||||
bool isImmediate = false,
|
|
||||||
}) async {
|
|
||||||
var dataState = state ?? this.state as AppConfigNotFinished;
|
|
||||||
|
|
||||||
var work = () async {
|
|
||||||
emit(TimerState(dataState: dataState, isLoading: true));
|
|
||||||
|
|
||||||
var isServerWorking = await repository.isHttpServerWorking();
|
|
||||||
|
|
||||||
if (isServerWorking) {
|
|
||||||
var pauseDuration = Duration(seconds: 30);
|
|
||||||
emit(TimerState(
|
|
||||||
dataState: dataState,
|
|
||||||
timerStart: DateTime.now(),
|
|
||||||
isLoading: false,
|
|
||||||
duration: pauseDuration,
|
|
||||||
));
|
|
||||||
timer = Timer(pauseDuration, () async {
|
|
||||||
var hetznerServerDetails = await repository.restart();
|
|
||||||
await repository.saveIsServerResetedFirstTime(true);
|
|
||||||
await repository.saveServerDetails(hetznerServerDetails);
|
|
||||||
|
|
||||||
emit(
|
|
||||||
dataState.copyWith(
|
|
||||||
isServerResetedFirstTime: true,
|
|
||||||
hetznerServer: hetznerServerDetails,
|
|
||||||
isLoading: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
oneMoreReset();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resetServerIfServerIsOkay();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (isImmediate) {
|
|
||||||
work();
|
|
||||||
} else {
|
|
||||||
var pauseDuration = Duration(seconds: 60);
|
|
||||||
emit(
|
|
||||||
TimerState(
|
|
||||||
dataState: dataState,
|
|
||||||
timerStart: DateTime.now(),
|
|
||||||
duration: pauseDuration,
|
|
||||||
isLoading: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
timer = Timer(pauseDuration, work);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer? timer;
|
|
||||||
|
|
||||||
void finishCheckIfServerIsOkay({
|
|
||||||
AppConfigNotFinished? state,
|
|
||||||
bool isImmediate = false,
|
|
||||||
}) async {
|
|
||||||
state = state ?? this.state as AppConfigNotFinished;
|
|
||||||
|
|
||||||
var work = () async {
|
|
||||||
emit(TimerState(dataState: state!, isLoading: true));
|
|
||||||
|
|
||||||
var isServerWorking = await repository.isHttpServerWorking();
|
|
||||||
|
|
||||||
if (isServerWorking) {
|
|
||||||
await repository.saveHasFinalChecked(true);
|
|
||||||
|
|
||||||
emit(state.finish());
|
|
||||||
} else {
|
|
||||||
finishCheckIfServerIsOkay();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (isImmediate) {
|
|
||||||
work();
|
|
||||||
} else {
|
|
||||||
var pauseDuration = Duration(seconds: 60);
|
|
||||||
emit(
|
|
||||||
TimerState(
|
|
||||||
dataState: state,
|
|
||||||
timerStart: DateTime.now(),
|
|
||||||
duration: pauseDuration,
|
|
||||||
isLoading: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
timer = Timer(pauseDuration, work);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearAppConfig() {
|
|
||||||
closeTimer();
|
|
||||||
|
|
||||||
repository.clearAppConfig();
|
|
||||||
emit(AppConfigEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> serverDelete() async {
|
|
||||||
closeTimer();
|
|
||||||
|
|
||||||
if (state.hetznerServer != null) {
|
|
||||||
await repository.deleteServer(state.cloudFlareDomain!);
|
|
||||||
await getIt<SSHModel>().clear();
|
|
||||||
}
|
|
||||||
await repository.deleteRecords();
|
|
||||||
emit(AppConfigNotFinished(
|
|
||||||
hetznerKey: state.hetznerKey,
|
|
||||||
cloudFlareDomain: state.cloudFlareDomain,
|
|
||||||
cloudFlareKey: state.cloudFlareKey,
|
|
||||||
backblazeCredential: state.backblazeCredential,
|
|
||||||
rootUser: state.rootUser,
|
|
||||||
hetznerServer: null,
|
|
||||||
isServerStarted: false,
|
|
||||||
isServerResetedFirstTime: false,
|
|
||||||
isServerResetedSecondTime: false,
|
|
||||||
isLoading: false,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
void setHetznerKey(String hetznerKey) async {
|
|
||||||
await repository.saveHetznerKey(hetznerKey);
|
|
||||||
emit((state as AppConfigNotFinished).copyWith(hetznerKey: hetznerKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
void setCloudflareKey(String cloudFlareKey) async {
|
|
||||||
await repository.saveCloudFlareKey(cloudFlareKey);
|
|
||||||
emit(
|
|
||||||
(state as AppConfigNotFinished).copyWith(cloudFlareKey: cloudFlareKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
void setBackblazeKey(String keyId, String applicationKey) async {
|
|
||||||
var backblazeCredential = BackblazeCredential(
|
|
||||||
keyId: keyId,
|
|
||||||
applicationKey: applicationKey,
|
|
||||||
);
|
|
||||||
await repository.saveBackblazeKey(backblazeCredential);
|
|
||||||
emit((state as AppConfigNotFinished)
|
|
||||||
.copyWith(backblazeCredential: backblazeCredential));
|
|
||||||
}
|
|
||||||
|
|
||||||
void setDomain(CloudFlareDomain cloudFlareDomain) async {
|
|
||||||
await repository.saveDomain(cloudFlareDomain);
|
|
||||||
emit((state as AppConfigNotFinished)
|
|
||||||
.copyWith(cloudFlareDomain: cloudFlareDomain));
|
|
||||||
}
|
|
||||||
|
|
||||||
void setRootUser(User rootUser) async {
|
|
||||||
await repository.saveRootUser(rootUser);
|
|
||||||
emit((state as AppConfigNotFinished).copyWith(rootUser: rootUser));
|
|
||||||
}
|
|
||||||
|
|
||||||
void createServerAndSetDnsRecords() async {
|
|
||||||
AppConfigNotFinished _stateCopy = state as AppConfigNotFinished;
|
|
||||||
var onSuccess = (HetznerServerDetails serverDetails) async {
|
|
||||||
await repository.createDnsRecords(
|
|
||||||
serverDetails.ip4,
|
|
||||||
state.cloudFlareDomain!,
|
|
||||||
);
|
|
||||||
|
|
||||||
emit((state as AppConfigNotFinished).copyWith(
|
|
||||||
isLoading: false,
|
|
||||||
hetznerServer: serverDetails,
|
|
||||||
));
|
|
||||||
startServerIfDnsIsOkay();
|
|
||||||
};
|
|
||||||
|
|
||||||
var onCancel =
|
|
||||||
() => emit((state as AppConfigNotFinished).copyWith(isLoading: false));
|
|
||||||
|
|
||||||
try {
|
|
||||||
emit((state as AppConfigNotFinished).copyWith(isLoading: true));
|
|
||||||
await repository.createServer(
|
|
||||||
state.rootUser!,
|
|
||||||
state.cloudFlareDomain!.domainName,
|
|
||||||
state.cloudFlareKey!,
|
|
||||||
onCancel: onCancel,
|
|
||||||
onSuccess: onSuccess,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
emit(_stateCopy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
closeTimer();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void closeTimer() {
|
|
||||||
if (timer != null && timer!.isActive) {
|
|
||||||
timer!.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,271 +0,0 @@
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:selfprivacy/config/hive_config.dart';
|
|
||||||
import 'package:selfprivacy/logic/api_maps/cloudflare.dart';
|
|
||||||
import 'package:selfprivacy/logic/api_maps/hetzner.dart';
|
|
||||||
import 'package:selfprivacy/logic/api_maps/server.dart';
|
|
||||||
import 'package:selfprivacy/logic/get_it/api_config.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/backblaze_credential.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/cloudflare_domain.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/server_details.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/user.dart';
|
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
|
||||||
import 'package:selfprivacy/logic/get_it/console.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/message.dart';
|
|
||||||
import 'package:basic_utils/basic_utils.dart';
|
|
||||||
import 'package:selfprivacy/ui/components/action_button/action_button.dart';
|
|
||||||
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart';
|
|
||||||
import 'app_config_cubit.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
|
|
||||||
class AppConfigRepository {
|
|
||||||
Box box = Hive.box(BNames.appConfig);
|
|
||||||
|
|
||||||
Future<AppConfigState> load() async {
|
|
||||||
late AppConfigState res;
|
|
||||||
if (box.get(BNames.hasFinalChecked, defaultValue: false)) {
|
|
||||||
res = AppConfigFinished(
|
|
||||||
hetznerKey: getIt<ApiConfigModel>().hetznerKey!,
|
|
||||||
cloudFlareKey: getIt<ApiConfigModel>().cloudFlareKey!,
|
|
||||||
cloudFlareDomain: getIt<ApiConfigModel>().cloudFlareDomain!,
|
|
||||||
backblazeCredential: getIt<ApiConfigModel>().backblazeCredential!,
|
|
||||||
hetznerServer: getIt<ApiConfigModel>().hetznerServer!,
|
|
||||||
rootUser: box.get(BNames.rootUser),
|
|
||||||
isServerStarted: box.get(BNames.isServerStarted, defaultValue: false),
|
|
||||||
isServerResetedFirstTime:
|
|
||||||
box.get(BNames.isServerResetedFirstTime, defaultValue: false),
|
|
||||||
isServerResetedSecondTime:
|
|
||||||
box.get(BNames.isServerResetedSecondTime, defaultValue: false),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res = AppConfigNotFinished(
|
|
||||||
hetznerKey: getIt<ApiConfigModel>().hetznerKey,
|
|
||||||
cloudFlareKey: getIt<ApiConfigModel>().cloudFlareKey,
|
|
||||||
cloudFlareDomain: getIt<ApiConfigModel>().cloudFlareDomain,
|
|
||||||
backblazeCredential: getIt<ApiConfigModel>().backblazeCredential,
|
|
||||||
hetznerServer: getIt<ApiConfigModel>().hetznerServer,
|
|
||||||
rootUser: box.get(BNames.rootUser),
|
|
||||||
isServerStarted: box.get(BNames.isServerStarted, defaultValue: false),
|
|
||||||
isServerResetedFirstTime:
|
|
||||||
box.get(BNames.isServerResetedFirstTime, defaultValue: false),
|
|
||||||
isServerResetedSecondTime:
|
|
||||||
box.get(BNames.isServerResetedSecondTime, defaultValue: false),
|
|
||||||
isLoading: box.get(BNames.isLoading, defaultValue: false),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearAppConfig() {
|
|
||||||
box.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<HetznerServerDetails> startServer(
|
|
||||||
HetznerServerDetails hetznerServer,
|
|
||||||
) async {
|
|
||||||
var hetznerApi = HetznerApi();
|
|
||||||
var serverDetails = await hetznerApi.powerOn();
|
|
||||||
|
|
||||||
return serverDetails;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> isDnsAddressesMatch(String? domainName, String? ip4) async {
|
|
||||||
var addresses = <String>[
|
|
||||||
'$domainName',
|
|
||||||
'api.$domainName',
|
|
||||||
'cloud.$domainName',
|
|
||||||
'meet.$domainName',
|
|
||||||
'password.$domainName'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (var address in addresses) {
|
|
||||||
var lookupRecordRes = await DnsUtils.lookupRecord(
|
|
||||||
address,
|
|
||||||
RRecordType.A,
|
|
||||||
provider: DnsApiProvider.CLOUDFLARE,
|
|
||||||
);
|
|
||||||
getIt.get<ConsoleModel>().addMessage(
|
|
||||||
Message(
|
|
||||||
text:
|
|
||||||
'DnsLookup: address: $address, $RRecordType, provider: CLOUDFLARE, ip4: $ip4',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
getIt.get<ConsoleModel>().addMessage(
|
|
||||||
Message(
|
|
||||||
text:
|
|
||||||
'DnsLookup: ${lookupRecordRes == null ? 'empty' : (lookupRecordRes[0].data != ip4 ? 'wrong ip4' : 'right ip4')}',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (lookupRecordRes == null ||
|
|
||||||
lookupRecordRes.isEmpty ||
|
|
||||||
lookupRecordRes[0].data != ip4) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> createServer(
|
|
||||||
User rootUser,
|
|
||||||
String domainName,
|
|
||||||
String cloudFlareKey, {
|
|
||||||
required void Function() onCancel,
|
|
||||||
required Future<void> Function(HetznerServerDetails serverDetails)
|
|
||||||
onSuccess,
|
|
||||||
}) async {
|
|
||||||
var hetznerApi = HetznerApi();
|
|
||||||
late HetznerDataBase dataBase;
|
|
||||||
|
|
||||||
try {
|
|
||||||
dataBase = await hetznerApi.createVolume();
|
|
||||||
|
|
||||||
var serverDetails = await hetznerApi.createServer(
|
|
||||||
cloudFlareKey: cloudFlareKey,
|
|
||||||
rootUser: rootUser,
|
|
||||||
domainName: domainName,
|
|
||||||
dataBase: dataBase,
|
|
||||||
);
|
|
||||||
saveServerDetails(serverDetails);
|
|
||||||
onSuccess(serverDetails);
|
|
||||||
} on DioError catch (e) {
|
|
||||||
if (e.response!.data['error']['code'] == 'uniqueness_error') {
|
|
||||||
var nav = getIt.get<NavigationService>();
|
|
||||||
nav.showPopUpDialog(
|
|
||||||
BrandAlert(
|
|
||||||
title: 'modals.1'.tr(),
|
|
||||||
contentText: 'modals.2'.tr(),
|
|
||||||
acitons: [
|
|
||||||
ActionButton(
|
|
||||||
text: 'basis.delete'.tr(),
|
|
||||||
isRed: true,
|
|
||||||
onPressed: () async {
|
|
||||||
await hetznerApi.deleteSelfprivacyServerAndAllVolumes(
|
|
||||||
domainName: domainName);
|
|
||||||
|
|
||||||
var serverDetails = await hetznerApi.createServer(
|
|
||||||
cloudFlareKey: cloudFlareKey,
|
|
||||||
rootUser: rootUser,
|
|
||||||
domainName: domainName,
|
|
||||||
dataBase: dataBase,
|
|
||||||
);
|
|
||||||
|
|
||||||
await saveServerDetails(serverDetails);
|
|
||||||
onSuccess(serverDetails);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ActionButton(
|
|
||||||
text: 'basis.cancel'.tr(),
|
|
||||||
onPressed: () {
|
|
||||||
onCancel();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> createDnsRecords(
|
|
||||||
String ip4,
|
|
||||||
CloudFlareDomain cloudFlareDomain,
|
|
||||||
) async {
|
|
||||||
var cloudflareApi = CloudflareApi();
|
|
||||||
|
|
||||||
await cloudflareApi.removeSimilarRecords(
|
|
||||||
ip4: ip4,
|
|
||||||
cloudFlareDomain: cloudFlareDomain,
|
|
||||||
);
|
|
||||||
|
|
||||||
await cloudflareApi.createMultipleDnsRecords(
|
|
||||||
ip4: ip4,
|
|
||||||
cloudFlareDomain: cloudFlareDomain,
|
|
||||||
);
|
|
||||||
|
|
||||||
await HetznerApi().createReverseDns(
|
|
||||||
ip4: ip4,
|
|
||||||
domainName: cloudFlareDomain.domainName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> isHttpServerWorking() async {
|
|
||||||
var api = ServerApi();
|
|
||||||
var isHttpServerWorking = await api.isHttpServerWorking();
|
|
||||||
return isHttpServerWorking;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<HetznerServerDetails> restart() async {
|
|
||||||
var hetznerApi = HetznerApi();
|
|
||||||
return await hetznerApi.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<HetznerServerDetails> powerOn() async {
|
|
||||||
var hetznerApi = HetznerApi();
|
|
||||||
return await hetznerApi.powerOn();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveServerDetails(HetznerServerDetails serverDetails) async {
|
|
||||||
await getIt<ApiConfigModel>().storeServerDetails(serverDetails);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveHetznerKey(String key) async {
|
|
||||||
await getIt<ApiConfigModel>().storeHetznerKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveBackblazeKey(BackblazeCredential backblazeCredential) async {
|
|
||||||
await getIt<ApiConfigModel>().storeBackblazeCredential(backblazeCredential);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveCloudFlareKey(String key) async {
|
|
||||||
await getIt<ApiConfigModel>().storeCloudFlareKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveDomain(CloudFlareDomain cloudFlareDomain) async {
|
|
||||||
await getIt<ApiConfigModel>().storeCloudFlareDomain(cloudFlareDomain);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveIsServerStarted(bool value) async {
|
|
||||||
await box.put(BNames.isServerStarted, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveIsServerResetedFirstTime(bool value) async {
|
|
||||||
await box.put(BNames.isServerResetedFirstTime, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveIsServerResetedSecondTime(bool value) async {
|
|
||||||
await box.put(BNames.isServerResetedSecondTime, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveRootUser(User rootUser) async {
|
|
||||||
await box.put(BNames.rootUser, rootUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveHasFinalChecked(bool value) async {
|
|
||||||
await box.put(BNames.hasFinalChecked, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteServer(CloudFlareDomain cloudFlareDomain) async {
|
|
||||||
var hetznerApi = HetznerApi();
|
|
||||||
var cloudFlare = CloudflareApi();
|
|
||||||
|
|
||||||
await hetznerApi.deleteSelfprivacyServerAndAllVolumes(
|
|
||||||
domainName: cloudFlareDomain.domainName,
|
|
||||||
);
|
|
||||||
|
|
||||||
await cloudFlare.removeSimilarRecords(cloudFlareDomain: cloudFlareDomain);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteRecords() async {
|
|
||||||
await box.deleteAll([
|
|
||||||
BNames.hetznerServer,
|
|
||||||
BNames.isServerStarted,
|
|
||||||
BNames.isServerResetedFirstTime,
|
|
||||||
BNames.isServerResetedSecondTime,
|
|
||||||
BNames.hasFinalChecked,
|
|
||||||
BNames.isLoading,
|
|
||||||
]);
|
|
||||||
getIt<ApiConfigModel>().init();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,235 +0,0 @@
|
||||||
part of 'app_config_cubit.dart';
|
|
||||||
|
|
||||||
abstract class AppConfigState extends Equatable {
|
|
||||||
const AppConfigState({
|
|
||||||
required this.hetznerKey,
|
|
||||||
required this.cloudFlareKey,
|
|
||||||
required this.backblazeCredential,
|
|
||||||
required this.cloudFlareDomain,
|
|
||||||
required this.rootUser,
|
|
||||||
required this.hetznerServer,
|
|
||||||
required this.isServerStarted,
|
|
||||||
required this.isServerResetedFirstTime,
|
|
||||||
required this.isServerResetedSecondTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [
|
|
||||||
hetznerKey,
|
|
||||||
cloudFlareKey,
|
|
||||||
backblazeCredential,
|
|
||||||
cloudFlareDomain,
|
|
||||||
rootUser,
|
|
||||||
hetznerServer,
|
|
||||||
isServerStarted,
|
|
||||||
isServerResetedFirstTime,
|
|
||||||
];
|
|
||||||
|
|
||||||
final String? hetznerKey;
|
|
||||||
final String? cloudFlareKey;
|
|
||||||
final BackblazeCredential? backblazeCredential;
|
|
||||||
final CloudFlareDomain? cloudFlareDomain;
|
|
||||||
final User? rootUser;
|
|
||||||
final HetznerServerDetails? hetznerServer;
|
|
||||||
final bool isServerStarted;
|
|
||||||
final bool isServerResetedFirstTime;
|
|
||||||
final bool isServerResetedSecondTime;
|
|
||||||
|
|
||||||
bool get isHetznerFilled => hetznerKey != null;
|
|
||||||
bool get isCloudFlareFilled => cloudFlareKey != null;
|
|
||||||
bool get isBackblazeFilled => backblazeCredential != null;
|
|
||||||
bool get isDomainFilled => cloudFlareDomain != null;
|
|
||||||
bool get isUserFilled => rootUser != null;
|
|
||||||
bool get isServerCreated => hetznerServer != null;
|
|
||||||
|
|
||||||
// bool get isFullyInitilized => _fulfilementList.every((el) => el!);
|
|
||||||
int get progress => _fulfilementList.where((el) => el!).length;
|
|
||||||
|
|
||||||
int get porgressBar {
|
|
||||||
if (progress < 6) {
|
|
||||||
return progress;
|
|
||||||
} else if (progress < 10) {
|
|
||||||
return 6;
|
|
||||||
} else {
|
|
||||||
return 7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<bool?> get _fulfilementList {
|
|
||||||
var res = [
|
|
||||||
isHetznerFilled,
|
|
||||||
isCloudFlareFilled,
|
|
||||||
isBackblazeFilled,
|
|
||||||
isDomainFilled,
|
|
||||||
isUserFilled,
|
|
||||||
isServerCreated,
|
|
||||||
isServerStarted,
|
|
||||||
isServerResetedFirstTime,
|
|
||||||
isServerResetedSecondTime,
|
|
||||||
];
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TimerState extends AppConfigNotFinished {
|
|
||||||
TimerState({
|
|
||||||
required this.dataState,
|
|
||||||
this.timerStart,
|
|
||||||
this.duration,
|
|
||||||
required bool isLoading,
|
|
||||||
}) : super(
|
|
||||||
hetznerKey: dataState.hetznerKey,
|
|
||||||
cloudFlareKey: dataState.cloudFlareKey,
|
|
||||||
backblazeCredential: dataState.backblazeCredential,
|
|
||||||
cloudFlareDomain: dataState.cloudFlareDomain,
|
|
||||||
rootUser: dataState.rootUser,
|
|
||||||
hetznerServer: dataState.hetznerServer,
|
|
||||||
isServerStarted: dataState.isServerStarted,
|
|
||||||
isServerResetedFirstTime: dataState.isServerResetedFirstTime,
|
|
||||||
isServerResetedSecondTime: dataState.isServerResetedSecondTime,
|
|
||||||
isLoading: isLoading,
|
|
||||||
);
|
|
||||||
|
|
||||||
final AppConfigNotFinished dataState;
|
|
||||||
final DateTime? timerStart;
|
|
||||||
final Duration? duration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [
|
|
||||||
dataState,
|
|
||||||
timerStart,
|
|
||||||
duration,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppConfigNotFinished extends AppConfigState {
|
|
||||||
final bool isLoading;
|
|
||||||
|
|
||||||
AppConfigNotFinished({
|
|
||||||
String? hetznerKey,
|
|
||||||
String? cloudFlareKey,
|
|
||||||
BackblazeCredential? backblazeCredential,
|
|
||||||
CloudFlareDomain? cloudFlareDomain,
|
|
||||||
User? rootUser,
|
|
||||||
HetznerServerDetails? hetznerServer,
|
|
||||||
required bool isServerStarted,
|
|
||||||
required bool isServerResetedFirstTime,
|
|
||||||
required bool isServerResetedSecondTime,
|
|
||||||
required this.isLoading,
|
|
||||||
}) : super(
|
|
||||||
hetznerKey: hetznerKey,
|
|
||||||
cloudFlareKey: cloudFlareKey,
|
|
||||||
backblazeCredential: backblazeCredential,
|
|
||||||
cloudFlareDomain: cloudFlareDomain,
|
|
||||||
rootUser: rootUser,
|
|
||||||
hetznerServer: hetznerServer,
|
|
||||||
isServerStarted: isServerStarted,
|
|
||||||
isServerResetedFirstTime: isServerResetedFirstTime,
|
|
||||||
isServerResetedSecondTime: isServerResetedSecondTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [
|
|
||||||
hetznerKey,
|
|
||||||
cloudFlareKey,
|
|
||||||
backblazeCredential,
|
|
||||||
cloudFlareDomain,
|
|
||||||
rootUser,
|
|
||||||
hetznerServer,
|
|
||||||
isServerStarted,
|
|
||||||
isServerResetedFirstTime,
|
|
||||||
isLoading
|
|
||||||
];
|
|
||||||
|
|
||||||
AppConfigNotFinished copyWith({
|
|
||||||
String? hetznerKey,
|
|
||||||
String? cloudFlareKey,
|
|
||||||
BackblazeCredential? backblazeCredential,
|
|
||||||
CloudFlareDomain? cloudFlareDomain,
|
|
||||||
User? rootUser,
|
|
||||||
HetznerServerDetails? hetznerServer,
|
|
||||||
bool? isServerStarted,
|
|
||||||
bool? isServerResetedFirstTime,
|
|
||||||
bool? isServerResetedSecondTime,
|
|
||||||
bool? isLoading,
|
|
||||||
}) =>
|
|
||||||
AppConfigNotFinished(
|
|
||||||
hetznerKey: hetznerKey ?? this.hetznerKey,
|
|
||||||
cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey,
|
|
||||||
backblazeCredential: backblazeCredential ?? this.backblazeCredential,
|
|
||||||
cloudFlareDomain: cloudFlareDomain ?? this.cloudFlareDomain,
|
|
||||||
rootUser: rootUser ?? this.rootUser,
|
|
||||||
hetznerServer: hetznerServer ?? this.hetznerServer,
|
|
||||||
isServerStarted: isServerStarted ?? this.isServerStarted,
|
|
||||||
isServerResetedFirstTime:
|
|
||||||
isServerResetedFirstTime ?? this.isServerResetedFirstTime,
|
|
||||||
isServerResetedSecondTime:
|
|
||||||
isServerResetedSecondTime ?? this.isServerResetedSecondTime,
|
|
||||||
isLoading: isLoading ?? this.isLoading,
|
|
||||||
);
|
|
||||||
|
|
||||||
AppConfigFinished finish() => AppConfigFinished(
|
|
||||||
hetznerKey: hetznerKey!,
|
|
||||||
cloudFlareKey: cloudFlareKey!,
|
|
||||||
backblazeCredential: backblazeCredential!,
|
|
||||||
cloudFlareDomain: cloudFlareDomain!,
|
|
||||||
rootUser: rootUser!,
|
|
||||||
hetznerServer: hetznerServer!,
|
|
||||||
isServerStarted: isServerStarted,
|
|
||||||
isServerResetedFirstTime: isServerResetedFirstTime,
|
|
||||||
isServerResetedSecondTime: isServerResetedSecondTime,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppConfigEmpty extends AppConfigNotFinished {
|
|
||||||
AppConfigEmpty()
|
|
||||||
: super(
|
|
||||||
hetznerKey: null,
|
|
||||||
cloudFlareKey: null,
|
|
||||||
backblazeCredential: null,
|
|
||||||
cloudFlareDomain: null,
|
|
||||||
rootUser: null,
|
|
||||||
hetznerServer: null,
|
|
||||||
isServerStarted: false,
|
|
||||||
isServerResetedFirstTime: false,
|
|
||||||
isServerResetedSecondTime: false,
|
|
||||||
isLoading: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppConfigFinished extends AppConfigState {
|
|
||||||
const AppConfigFinished({
|
|
||||||
required String hetznerKey,
|
|
||||||
required String cloudFlareKey,
|
|
||||||
required BackblazeCredential backblazeCredential,
|
|
||||||
required CloudFlareDomain cloudFlareDomain,
|
|
||||||
required User rootUser,
|
|
||||||
required HetznerServerDetails hetznerServer,
|
|
||||||
required bool isServerStarted,
|
|
||||||
required bool isServerResetedFirstTime,
|
|
||||||
required bool isServerResetedSecondTime,
|
|
||||||
}) : super(
|
|
||||||
hetznerKey: hetznerKey,
|
|
||||||
cloudFlareKey: cloudFlareKey,
|
|
||||||
backblazeCredential: backblazeCredential,
|
|
||||||
cloudFlareDomain: cloudFlareDomain,
|
|
||||||
rootUser: rootUser,
|
|
||||||
hetznerServer: hetznerServer,
|
|
||||||
isServerStarted: isServerStarted,
|
|
||||||
isServerResetedFirstTime: isServerResetedFirstTime,
|
|
||||||
isServerResetedSecondTime: isServerResetedSecondTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [
|
|
||||||
hetznerKey,
|
|
||||||
cloudFlareKey,
|
|
||||||
backblazeCredential,
|
|
||||||
cloudFlareDomain,
|
|
||||||
rootUser,
|
|
||||||
hetznerServer,
|
|
||||||
isServerStarted,
|
|
||||||
isServerResetedFirstTime,
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -1,32 +1,33 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
|
|
||||||
export 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
export 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
|
|
||||||
part 'authentication_dependend_state.dart';
|
part 'authentication_dependend_state.dart';
|
||||||
|
|
||||||
abstract class AppConfigDependendCubit<T extends AppConfigDependendState>
|
abstract class ServerInstallationDependendCubit<
|
||||||
extends Cubit<T> {
|
T extends ServerInstallationDependendState> extends Cubit<T> {
|
||||||
AppConfigDependendCubit(
|
ServerInstallationDependendCubit(
|
||||||
this.appConfigCubit,
|
this.serverInstallationCubit,
|
||||||
T initState,
|
final T initState,
|
||||||
) : super(initState) {
|
) : super(initState) {
|
||||||
authCubitSubscription = appConfigCubit.stream.listen(checkAuthStatus);
|
authCubitSubscription =
|
||||||
checkAuthStatus(appConfigCubit.state);
|
serverInstallationCubit.stream.listen(checkAuthStatus);
|
||||||
|
checkAuthStatus(serverInstallationCubit.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkAuthStatus(AppConfigState state) {
|
void checkAuthStatus(final ServerInstallationState state) {
|
||||||
if (state is AppConfigFinished) {
|
if (state is ServerInstallationFinished) {
|
||||||
load();
|
load();
|
||||||
} else if (state is AppConfigEmpty) {
|
} else if (state is ServerInstallationEmpty) {
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
late StreamSubscription authCubitSubscription;
|
late StreamSubscription authCubitSubscription;
|
||||||
final AppConfigCubit appConfigCubit;
|
final ServerInstallationCubit serverInstallationCubit;
|
||||||
|
|
||||||
void load();
|
void load();
|
||||||
void clear();
|
void clear();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
part of 'authentication_dependend_cubit.dart';
|
part of 'authentication_dependend_cubit.dart';
|
||||||
|
|
||||||
abstract class AppConfigDependendState extends Equatable {
|
abstract class ServerInstallationDependendState extends Equatable {
|
||||||
const AppConfigDependendState();
|
const ServerInstallationDependendState();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +1,44 @@
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:selfprivacy/config/hive_config.dart';
|
import 'package:selfprivacy/config/hive_config.dart';
|
||||||
|
|
||||||
export 'package:provider/provider.dart';
|
export 'package:provider/provider.dart';
|
||||||
|
|
||||||
part 'app_settings_state.dart';
|
part 'app_settings_state.dart';
|
||||||
|
|
||||||
class AppSettingsCubit extends Cubit<AppSettingsState> {
|
class AppSettingsCubit extends Cubit<AppSettingsState> {
|
||||||
AppSettingsCubit({
|
AppSettingsCubit({
|
||||||
required bool isDarkModeOn,
|
required final bool isDarkModeOn,
|
||||||
required bool isOnbordingShowing,
|
required final bool isOnboardingShowing,
|
||||||
}) : super(
|
}) : super(
|
||||||
AppSettingsState(
|
AppSettingsState(
|
||||||
isDarkModeOn: isDarkModeOn,
|
isDarkModeOn: isDarkModeOn,
|
||||||
isOnbordingShowing: isOnbordingShowing,
|
isOnboardingShowing: isOnboardingShowing,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Box box = Hive.box(BNames.appSettings);
|
Box box = Hive.box(BNames.appSettingsBox);
|
||||||
|
|
||||||
void load() {
|
void load() {
|
||||||
bool? isDarkModeOn = box.get(BNames.isDarkModeOn);
|
final bool? isDarkModeOn = box.get(BNames.isDarkModeOn);
|
||||||
bool? isOnbordingShowing = box.get(BNames.isOnbordingShowing);
|
final bool? isOnboardingShowing = box.get(BNames.isOnboardingShowing);
|
||||||
emit(state.copyWith(
|
emit(
|
||||||
isDarkModeOn: isDarkModeOn,
|
state.copyWith(
|
||||||
isOnbordingShowing: isOnbordingShowing,
|
isDarkModeOn: isDarkModeOn,
|
||||||
));
|
isOnboardingShowing: isOnboardingShowing,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateDarkMode({required bool isDarkModeOn}) {
|
void updateDarkMode({required final bool isDarkModeOn}) {
|
||||||
box.put(BNames.isDarkModeOn, isDarkModeOn);
|
box.put(BNames.isDarkModeOn, isDarkModeOn);
|
||||||
emit(state.copyWith(isDarkModeOn: isDarkModeOn));
|
emit(state.copyWith(isDarkModeOn: isDarkModeOn));
|
||||||
}
|
}
|
||||||
|
|
||||||
void turnOffOnboarding() {
|
void turnOffOnboarding() {
|
||||||
box.put(BNames.isOnbordingShowing, false);
|
box.put(BNames.isOnboardingShowing, false);
|
||||||
|
|
||||||
emit(state.copyWith(isOnbordingShowing: false));
|
emit(state.copyWith(isOnboardingShowing: false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,21 @@ part of 'app_settings_cubit.dart';
|
||||||
class AppSettingsState extends Equatable {
|
class AppSettingsState extends Equatable {
|
||||||
const AppSettingsState({
|
const AppSettingsState({
|
||||||
required this.isDarkModeOn,
|
required this.isDarkModeOn,
|
||||||
required this.isOnbordingShowing,
|
required this.isOnboardingShowing,
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool isDarkModeOn;
|
final bool isDarkModeOn;
|
||||||
final bool isOnbordingShowing;
|
final bool isOnboardingShowing;
|
||||||
|
|
||||||
AppSettingsState copyWith({isDarkModeOn, isOnbordingShowing}) =>
|
AppSettingsState copyWith({
|
||||||
|
final bool? isDarkModeOn,
|
||||||
|
final bool? isOnboardingShowing,
|
||||||
|
}) =>
|
||||||
AppSettingsState(
|
AppSettingsState(
|
||||||
isDarkModeOn: isDarkModeOn ?? this.isDarkModeOn,
|
isDarkModeOn: isDarkModeOn ?? this.isDarkModeOn,
|
||||||
isOnbordingShowing: isOnbordingShowing ?? this.isOnbordingShowing,
|
isOnboardingShowing: isOnboardingShowing ?? this.isOnboardingShowing,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [isDarkModeOn, isOnbordingShowing];
|
List<Object> get props => [isDarkModeOn, isOnboardingShowing];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
|
import 'package:selfprivacy/logic/api_maps/backblaze.dart';
|
||||||
|
import 'package:selfprivacy/logic/api_maps/server.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/backup.dart';
|
||||||
|
|
||||||
|
part 'backups_state.dart';
|
||||||
|
|
||||||
|
class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
|
||||||
|
BackupsCubit(final ServerInstallationCubit serverInstallationCubit)
|
||||||
|
: super(
|
||||||
|
serverInstallationCubit,
|
||||||
|
const BackupsState(preventActions: true),
|
||||||
|
);
|
||||||
|
|
||||||
|
final ServerApi api = ServerApi();
|
||||||
|
final BackblazeApi backblaze = BackblazeApi();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> load() async {
|
||||||
|
if (serverInstallationCubit.state is ServerInstallationFinished) {
|
||||||
|
final BackblazeBucket? bucket = getIt<ApiConfigModel>().backblazeBucket;
|
||||||
|
if (bucket == null) {
|
||||||
|
emit(
|
||||||
|
const BackupsState(
|
||||||
|
isInitialized: false,
|
||||||
|
preventActions: false,
|
||||||
|
refreshing: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final BackupStatus status = await api.getBackupStatus();
|
||||||
|
switch (status.status) {
|
||||||
|
case BackupStatusEnum.noKey:
|
||||||
|
case BackupStatusEnum.notInitialized:
|
||||||
|
emit(
|
||||||
|
BackupsState(
|
||||||
|
backups: const [],
|
||||||
|
isInitialized: true,
|
||||||
|
preventActions: false,
|
||||||
|
progress: 0,
|
||||||
|
status: status.status,
|
||||||
|
refreshing: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case BackupStatusEnum.initializing:
|
||||||
|
emit(
|
||||||
|
BackupsState(
|
||||||
|
backups: const [],
|
||||||
|
isInitialized: true,
|
||||||
|
preventActions: false,
|
||||||
|
progress: 0,
|
||||||
|
status: status.status,
|
||||||
|
refreshTimer: const Duration(seconds: 10),
|
||||||
|
refreshing: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case BackupStatusEnum.initialized:
|
||||||
|
case BackupStatusEnum.error:
|
||||||
|
final List<Backup> backups = await api.getBackups();
|
||||||
|
emit(
|
||||||
|
BackupsState(
|
||||||
|
backups: backups,
|
||||||
|
isInitialized: true,
|
||||||
|
preventActions: false,
|
||||||
|
progress: status.progress,
|
||||||
|
status: status.status,
|
||||||
|
error: status.errorMessage ?? '',
|
||||||
|
refreshing: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case BackupStatusEnum.backingUp:
|
||||||
|
case BackupStatusEnum.restoring:
|
||||||
|
final List<Backup> backups = await api.getBackups();
|
||||||
|
emit(
|
||||||
|
BackupsState(
|
||||||
|
backups: backups,
|
||||||
|
isInitialized: true,
|
||||||
|
preventActions: true,
|
||||||
|
progress: status.progress,
|
||||||
|
status: status.status,
|
||||||
|
error: status.errorMessage ?? '',
|
||||||
|
refreshTimer: const Duration(seconds: 5),
|
||||||
|
refreshing: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
emit(const BackupsState());
|
||||||
|
}
|
||||||
|
Timer(state.refreshTimer, () => updateBackups(useTimer: true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createBucket() async {
|
||||||
|
emit(state.copyWith(preventActions: true));
|
||||||
|
final String domain = serverInstallationCubit.state.serverDomain!.domainName
|
||||||
|
.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
|
||||||
|
final int serverId = serverInstallationCubit.state.serverDetails!.id;
|
||||||
|
String bucketName = 'selfprivacy-$domain-$serverId';
|
||||||
|
// If bucket name is too long, shorten it
|
||||||
|
if (bucketName.length > 49) {
|
||||||
|
bucketName = bucketName.substring(0, 49);
|
||||||
|
}
|
||||||
|
final String bucketId = await backblaze.createBucket(bucketName);
|
||||||
|
|
||||||
|
final BackblazeApplicationKey key = await backblaze.createKey(bucketId);
|
||||||
|
final BackblazeBucket bucket = BackblazeBucket(
|
||||||
|
bucketId: bucketId,
|
||||||
|
bucketName: bucketName,
|
||||||
|
applicationKey: key.applicationKey,
|
||||||
|
applicationKeyId: key.applicationKeyId,
|
||||||
|
);
|
||||||
|
|
||||||
|
await getIt<ApiConfigModel>().storeBackblazeBucket(bucket);
|
||||||
|
await api.uploadBackblazeConfig(bucket);
|
||||||
|
await updateBackups();
|
||||||
|
|
||||||
|
emit(state.copyWith(isInitialized: true, preventActions: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reuploadKey() async {
|
||||||
|
emit(state.copyWith(preventActions: true));
|
||||||
|
final BackblazeBucket? bucket = getIt<ApiConfigModel>().backblazeBucket;
|
||||||
|
if (bucket == null) {
|
||||||
|
emit(state.copyWith(isInitialized: false));
|
||||||
|
} else {
|
||||||
|
await api.uploadBackblazeConfig(bucket);
|
||||||
|
emit(state.copyWith(isInitialized: true, preventActions: false));
|
||||||
|
getIt<NavigationService>().showSnackBar('providers.backup.reuploadedKey');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration refreshTimeFromState(final BackupStatusEnum status) {
|
||||||
|
switch (status) {
|
||||||
|
case BackupStatusEnum.backingUp:
|
||||||
|
case BackupStatusEnum.restoring:
|
||||||
|
return const Duration(seconds: 5);
|
||||||
|
case BackupStatusEnum.initializing:
|
||||||
|
return const Duration(seconds: 10);
|
||||||
|
default:
|
||||||
|
return const Duration(seconds: 60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateBackups({final bool useTimer = false}) async {
|
||||||
|
emit(state.copyWith(refreshing: true));
|
||||||
|
final List<Backup> backups = await api.getBackups();
|
||||||
|
final BackupStatus status = await api.getBackupStatus();
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
backups: backups,
|
||||||
|
progress: status.progress,
|
||||||
|
status: status.status,
|
||||||
|
error: status.errorMessage,
|
||||||
|
refreshTimer: refreshTimeFromState(status.status),
|
||||||
|
refreshing: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (useTimer) {
|
||||||
|
Timer(state.refreshTimer, () => updateBackups(useTimer: true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> forceUpdateBackups() async {
|
||||||
|
emit(state.copyWith(preventActions: true));
|
||||||
|
await api.forceBackupListReload();
|
||||||
|
getIt<NavigationService>()
|
||||||
|
.showSnackBar('providers.backup.refetchingList'.tr());
|
||||||
|
emit(state.copyWith(preventActions: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createBackup() async {
|
||||||
|
emit(state.copyWith(preventActions: true));
|
||||||
|
await api.startBackup();
|
||||||
|
await updateBackups();
|
||||||
|
emit(state.copyWith(preventActions: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> restoreBackup(final String backupId) async {
|
||||||
|
emit(state.copyWith(preventActions: true));
|
||||||
|
await api.restoreBackup(backupId);
|
||||||
|
emit(state.copyWith(preventActions: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void clear() async {
|
||||||
|
emit(const BackupsState());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
part of 'backups_cubit.dart';
|
||||||
|
|
||||||
|
class BackupsState extends ServerInstallationDependendState {
|
||||||
|
const BackupsState({
|
||||||
|
this.isInitialized = false,
|
||||||
|
this.backups = const [],
|
||||||
|
this.progress = 0.0,
|
||||||
|
this.status = BackupStatusEnum.noKey,
|
||||||
|
this.preventActions = true,
|
||||||
|
this.error = '',
|
||||||
|
this.refreshTimer = const Duration(seconds: 60),
|
||||||
|
this.refreshing = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isInitialized;
|
||||||
|
final List<Backup> backups;
|
||||||
|
final double progress;
|
||||||
|
final BackupStatusEnum status;
|
||||||
|
final bool preventActions;
|
||||||
|
final String error;
|
||||||
|
final Duration refreshTimer;
|
||||||
|
final bool refreshing;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [
|
||||||
|
isInitialized,
|
||||||
|
backups,
|
||||||
|
progress,
|
||||||
|
preventActions,
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
refreshTimer,
|
||||||
|
refreshing
|
||||||
|
];
|
||||||
|
|
||||||
|
BackupsState copyWith({
|
||||||
|
final bool? isInitialized,
|
||||||
|
final List<Backup>? backups,
|
||||||
|
final double? progress,
|
||||||
|
final BackupStatusEnum? status,
|
||||||
|
final bool? preventActions,
|
||||||
|
final String? error,
|
||||||
|
final Duration? refreshTimer,
|
||||||
|
final bool? refreshing,
|
||||||
|
}) =>
|
||||||
|
BackupsState(
|
||||||
|
isInitialized: isInitialized ?? this.isInitialized,
|
||||||
|
backups: backups ?? this.backups,
|
||||||
|
progress: progress ?? this.progress,
|
||||||
|
status: status ?? this.status,
|
||||||
|
preventActions: preventActions ?? this.preventActions,
|
||||||
|
error: error ?? this.error,
|
||||||
|
refreshTimer: refreshTimer ?? this.refreshTimer,
|
||||||
|
refreshing: refreshing ?? this.refreshing,
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
|
import 'package:selfprivacy/logic/api_maps/server.dart';
|
||||||
|
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/api_token.dart';
|
||||||
|
|
||||||
|
part 'devices_state.dart';
|
||||||
|
|
||||||
|
class ApiDevicesCubit
|
||||||
|
extends ServerInstallationDependendCubit<ApiDevicesState> {
|
||||||
|
ApiDevicesCubit(final ServerInstallationCubit serverInstallationCubit)
|
||||||
|
: super(serverInstallationCubit, const ApiDevicesState.initial());
|
||||||
|
|
||||||
|
final ServerApi api = ServerApi();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void load() async {
|
||||||
|
if (serverInstallationCubit.state is ServerInstallationFinished) {
|
||||||
|
final List<ApiToken>? devices = await _getApiTokens();
|
||||||
|
if (devices != null) {
|
||||||
|
emit(ApiDevicesState(devices, LoadingStatus.success));
|
||||||
|
} else {
|
||||||
|
emit(const ApiDevicesState([], LoadingStatus.error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
emit(const ApiDevicesState([], LoadingStatus.refreshing));
|
||||||
|
final List<ApiToken>? devices = await _getApiTokens();
|
||||||
|
if (devices != null) {
|
||||||
|
emit(ApiDevicesState(devices, LoadingStatus.success));
|
||||||
|
} else {
|
||||||
|
emit(const ApiDevicesState([], LoadingStatus.error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ApiToken>?> _getApiTokens() async {
|
||||||
|
final ApiResponse<List<ApiToken>> response = await api.getApiTokens();
|
||||||
|
if (response.isSuccess) {
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteDevice(final ApiToken device) async {
|
||||||
|
final ApiResponse<void> response = await api.deleteApiToken(device.name);
|
||||||
|
if (response.isSuccess) {
|
||||||
|
emit(
|
||||||
|
ApiDevicesState(
|
||||||
|
state.devices.where((final d) => d.name != device.name).toList(),
|
||||||
|
LoadingStatus.success,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
getIt<NavigationService>()
|
||||||
|
.showSnackBar(response.errorMessage ?? 'Error deleting device');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getNewDeviceKey() async {
|
||||||
|
final ApiResponse<String> response = await api.createDeviceToken();
|
||||||
|
if (response.isSuccess) {
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
getIt<NavigationService>().showSnackBar(
|
||||||
|
response.errorMessage ?? 'Error getting new device key',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void clear() {
|
||||||
|
emit(const ApiDevicesState.initial());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
part of 'devices_cubit.dart';
|
||||||
|
|
||||||
|
class ApiDevicesState extends ServerInstallationDependendState {
|
||||||
|
const ApiDevicesState(this._devices, this.status);
|
||||||
|
|
||||||
|
const ApiDevicesState.initial() : this(const [], LoadingStatus.uninitialized);
|
||||||
|
final List<ApiToken> _devices;
|
||||||
|
final LoadingStatus status;
|
||||||
|
|
||||||
|
List<ApiToken> get devices => _devices;
|
||||||
|
ApiToken get thisDevice => _devices.firstWhere(
|
||||||
|
(final device) => device.isCaller,
|
||||||
|
orElse: () => ApiToken(
|
||||||
|
name: 'Error fetching device',
|
||||||
|
isCaller: true,
|
||||||
|
date: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<ApiToken> get otherDevices =>
|
||||||
|
_devices.where((final device) => !device.isCaller).toList();
|
||||||
|
|
||||||
|
ApiDevicesState copyWith({
|
||||||
|
final List<ApiToken>? devices,
|
||||||
|
final LoadingStatus? status,
|
||||||
|
}) =>
|
||||||
|
ApiDevicesState(
|
||||||
|
devices ?? _devices,
|
||||||
|
status ?? this.status,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [_devices];
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/dns_records.dart';
|
||||||
|
|
||||||
|
import 'package:selfprivacy/logic/api_maps/cloudflare.dart';
|
||||||
|
import 'package:selfprivacy/logic/api_maps/server.dart';
|
||||||
|
|
||||||
|
part 'dns_records_state.dart';
|
||||||
|
|
||||||
|
class DnsRecordsCubit
|
||||||
|
extends ServerInstallationDependendCubit<DnsRecordsState> {
|
||||||
|
DnsRecordsCubit(final ServerInstallationCubit serverInstallationCubit)
|
||||||
|
: super(
|
||||||
|
serverInstallationCubit,
|
||||||
|
const DnsRecordsState(dnsState: DnsRecordsStatus.refreshing),
|
||||||
|
);
|
||||||
|
|
||||||
|
final ServerApi api = ServerApi();
|
||||||
|
final CloudflareApi cloudflare = CloudflareApi();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> load() async {
|
||||||
|
emit(
|
||||||
|
DnsRecordsState(
|
||||||
|
dnsState: DnsRecordsStatus.refreshing,
|
||||||
|
dnsRecords: _getDesiredDnsRecords(
|
||||||
|
serverInstallationCubit.state.serverDomain?.domainName,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
print('Loading DNS status');
|
||||||
|
if (serverInstallationCubit.state is ServerInstallationFinished) {
|
||||||
|
final ServerDomain? domain = serverInstallationCubit.state.serverDomain;
|
||||||
|
final String? ipAddress =
|
||||||
|
serverInstallationCubit.state.serverDetails?.ip4;
|
||||||
|
if (domain != null && ipAddress != null) {
|
||||||
|
final List<DnsRecord> records =
|
||||||
|
await cloudflare.getDnsRecords(cloudFlareDomain: domain);
|
||||||
|
final String? dkimPublicKey = await api.getDkim();
|
||||||
|
final List<DesiredDnsRecord> desiredRecords =
|
||||||
|
_getDesiredDnsRecords(domain.domainName, ipAddress, dkimPublicKey);
|
||||||
|
final List<DesiredDnsRecord> foundRecords = [];
|
||||||
|
for (final DesiredDnsRecord record in desiredRecords) {
|
||||||
|
if (record.description ==
|
||||||
|
'providers.domain.record_description.dkim') {
|
||||||
|
final DnsRecord foundRecord = records.firstWhere(
|
||||||
|
(final r) => r.name == record.name && r.type == record.type,
|
||||||
|
orElse: () => DnsRecord(
|
||||||
|
name: record.name,
|
||||||
|
type: record.type,
|
||||||
|
content: '',
|
||||||
|
ttl: 800,
|
||||||
|
proxied: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// remove all spaces and tabulators from
|
||||||
|
// the foundRecord.content and the record.content
|
||||||
|
// to compare them
|
||||||
|
final String? foundContent =
|
||||||
|
foundRecord.content?.replaceAll(RegExp(r'\s+'), '');
|
||||||
|
final String content =
|
||||||
|
record.content.replaceAll(RegExp(r'\s+'), '');
|
||||||
|
if (foundContent == content) {
|
||||||
|
foundRecords.add(record.copyWith(isSatisfied: true));
|
||||||
|
} else {
|
||||||
|
foundRecords.add(record.copyWith(isSatisfied: false));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (records.any(
|
||||||
|
(final r) =>
|
||||||
|
r.name == record.name &&
|
||||||
|
r.type == record.type &&
|
||||||
|
r.content == record.content,
|
||||||
|
)) {
|
||||||
|
foundRecords.add(record.copyWith(isSatisfied: true));
|
||||||
|
} else {
|
||||||
|
foundRecords.add(record.copyWith(isSatisfied: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit(
|
||||||
|
DnsRecordsState(
|
||||||
|
dnsRecords: foundRecords,
|
||||||
|
dnsState: foundRecords.any((final r) => r.isSatisfied == false)
|
||||||
|
? DnsRecordsStatus.error
|
||||||
|
: DnsRecordsStatus.good,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
emit(const DnsRecordsState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onChange(final Change<DnsRecordsState> change) {
|
||||||
|
// print(change);
|
||||||
|
super.onChange(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clear() async {
|
||||||
|
emit(const DnsRecordsState(dnsState: DnsRecordsStatus.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing));
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fix() async {
|
||||||
|
emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing));
|
||||||
|
final ServerDomain? domain = serverInstallationCubit.state.serverDomain;
|
||||||
|
final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4;
|
||||||
|
final String? dkimPublicKey = await api.getDkim();
|
||||||
|
await cloudflare.removeSimilarRecords(cloudFlareDomain: domain!);
|
||||||
|
await cloudflare.createMultipleDnsRecords(
|
||||||
|
cloudFlareDomain: domain,
|
||||||
|
ip4: ipAddress,
|
||||||
|
);
|
||||||
|
await cloudflare.setDkim(dkimPublicKey ?? '', domain);
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DesiredDnsRecord> _getDesiredDnsRecords(
|
||||||
|
final String? domainName,
|
||||||
|
final String? ipAddress,
|
||||||
|
final String? dkimPublicKey,
|
||||||
|
) {
|
||||||
|
if (domainName == null || ipAddress == null || dkimPublicKey == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: domainName,
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.root',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'api.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.api',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'cloud.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.cloud',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'git.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.git',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'meet.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.meet',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'social.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.social',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'password.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.password',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'vpn.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.vpn',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: domainName,
|
||||||
|
content: domainName,
|
||||||
|
description: 'providers.domain.record_description.mx',
|
||||||
|
type: 'MX',
|
||||||
|
category: DnsRecordsCategory.email,
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: '_dmarc.$domainName',
|
||||||
|
content: 'v=DMARC1; p=none',
|
||||||
|
description: 'providers.domain.record_description.dmarc',
|
||||||
|
type: 'TXT',
|
||||||
|
category: DnsRecordsCategory.email,
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: domainName,
|
||||||
|
content: 'v=spf1 a mx ip4:$ipAddress -all',
|
||||||
|
description: 'providers.domain.record_description.spf',
|
||||||
|
type: 'TXT',
|
||||||
|
category: DnsRecordsCategory.email,
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'selector._domainkey.$domainName',
|
||||||
|
content: dkimPublicKey,
|
||||||
|
description: 'providers.domain.record_description.dkim',
|
||||||
|
type: 'TXT',
|
||||||
|
category: DnsRecordsCategory.email,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
part of 'dns_records_cubit.dart';
|
||||||
|
|
||||||
|
enum DnsRecordsStatus {
|
||||||
|
uninitialized,
|
||||||
|
refreshing,
|
||||||
|
good,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DnsRecordsCategory {
|
||||||
|
services,
|
||||||
|
email,
|
||||||
|
other,
|
||||||
|
}
|
||||||
|
|
||||||
|
class DnsRecordsState extends ServerInstallationDependendState {
|
||||||
|
const DnsRecordsState({
|
||||||
|
this.dnsState = DnsRecordsStatus.uninitialized,
|
||||||
|
this.dnsRecords = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
final DnsRecordsStatus dnsState;
|
||||||
|
final List<DesiredDnsRecord> dnsRecords;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [
|
||||||
|
dnsState,
|
||||||
|
dnsRecords,
|
||||||
|
];
|
||||||
|
|
||||||
|
DnsRecordsState copyWith({
|
||||||
|
final DnsRecordsStatus? dnsState,
|
||||||
|
final List<DesiredDnsRecord>? dnsRecords,
|
||||||
|
}) =>
|
||||||
|
DnsRecordsState(
|
||||||
|
dnsState: dnsState ?? this.dnsState,
|
||||||
|
dnsRecords: dnsRecords ?? this.dnsRecords,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DesiredDnsRecord {
|
||||||
|
const DesiredDnsRecord({
|
||||||
|
required this.name,
|
||||||
|
required this.content,
|
||||||
|
this.type = 'A',
|
||||||
|
this.description = '',
|
||||||
|
this.category = DnsRecordsCategory.services,
|
||||||
|
this.isSatisfied = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final String type;
|
||||||
|
final String content;
|
||||||
|
final String description;
|
||||||
|
final DnsRecordsCategory category;
|
||||||
|
final bool isSatisfied;
|
||||||
|
|
||||||
|
DesiredDnsRecord copyWith({
|
||||||
|
final String? name,
|
||||||
|
final String? type,
|
||||||
|
final String? content,
|
||||||
|
final String? description,
|
||||||
|
final DnsRecordsCategory? category,
|
||||||
|
final bool? isSatisfied,
|
||||||
|
}) =>
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: name ?? this.name,
|
||||||
|
type: type ?? this.type,
|
||||||
|
content: content ?? this.content,
|
||||||
|
description: description ?? this.description,
|
||||||
|
category: category ?? this.category,
|
||||||
|
isSatisfied: isSatisfied ?? this.isSatisfied,
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
|
||||||
|
|
||||||
|
class FieldCubitFactory {
|
||||||
|
FieldCubitFactory(this.context);
|
||||||
|
|
||||||
|
/// A common user login field.
|
||||||
|
///
|
||||||
|
/// - Available characters are lowercase a-z, digits and underscore _
|
||||||
|
/// - Must start with either a-z or underscore
|
||||||
|
/// - Must be no longer than 'userMaxLength' characters
|
||||||
|
/// - Must not be empty
|
||||||
|
/// - Must not be a reserved root login
|
||||||
|
/// - Must be unique
|
||||||
|
FieldCubit<String> createUserLoginField() {
|
||||||
|
final RegExp userAllowedRegExp = RegExp(r'^[a-z_][a-z0-9_]+$');
|
||||||
|
const int userMaxLength = 31;
|
||||||
|
return FieldCubit(
|
||||||
|
initalValue: '',
|
||||||
|
validations: [
|
||||||
|
ValidationModel<String>(
|
||||||
|
(final String s) => s.toLowerCase() == 'root',
|
||||||
|
'validations.root_name'.tr(),
|
||||||
|
),
|
||||||
|
ValidationModel(
|
||||||
|
(final String login) =>
|
||||||
|
context.read<UsersCubit>().state.isLoginRegistered(login),
|
||||||
|
'validations.user_already_exist'.tr(),
|
||||||
|
),
|
||||||
|
RequiredStringValidation('validations.required'.tr()),
|
||||||
|
LengthStringLongerValidation(userMaxLength),
|
||||||
|
ValidationModel<String>(
|
||||||
|
(final String s) => !userAllowedRegExp.hasMatch(s),
|
||||||
|
'validations.invalid_format'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A common user password field.
|
||||||
|
///
|
||||||
|
/// - Must fail on the regural expression of invalid matches: [\n\r\s]+
|
||||||
|
/// - Must not be empty
|
||||||
|
FieldCubit<String> createUserPasswordField() {
|
||||||
|
final RegExp passwordForbiddenRegExp = RegExp(r'[\n\r\s]+');
|
||||||
|
return FieldCubit(
|
||||||
|
initalValue: '',
|
||||||
|
validations: [
|
||||||
|
RequiredStringValidation('validations.required'.tr()),
|
||||||
|
ValidationModel<String>(
|
||||||
|
passwordForbiddenRegExp.hasMatch,
|
||||||
|
'validations.invalid_format'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldCubit<String> createRequiredStringField() => FieldCubit(
|
||||||
|
initalValue: '',
|
||||||
|
validations: [
|
||||||
|
RequiredStringValidation('validations.required'.tr()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final BuildContext context;
|
||||||
|
}
|
|
@ -1,52 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:cubit_form/cubit_form.dart';
|
|
||||||
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/user.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
|
|
||||||
class RootUserFormCubit extends FormCubit {
|
|
||||||
RootUserFormCubit(this.initializingCubit) {
|
|
||||||
var userRegExp = RegExp(r"\W");
|
|
||||||
var passwordRegExp = RegExp(r"[\n\r\s]+");
|
|
||||||
|
|
||||||
userName = FieldCubit(
|
|
||||||
initalValue: '',
|
|
||||||
validations: [
|
|
||||||
ValidationModel<String>(
|
|
||||||
(s) => s.toLowerCase() == 'root', 'validations.root_name'.tr()),
|
|
||||||
RequiredStringValidation('validations.required'.tr()),
|
|
||||||
ValidationModel<String>(
|
|
||||||
(s) => userRegExp.hasMatch(s), 'validations.invalid_format'.tr()),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
password = FieldCubit(
|
|
||||||
initalValue: '',
|
|
||||||
validations: [
|
|
||||||
RequiredStringValidation('validations.required'.tr()),
|
|
||||||
ValidationModel<String>(
|
|
||||||
(s) => passwordRegExp.hasMatch(s), 'validations.invalid_format'.tr()),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
isVisible = FieldCubit(initalValue: false);
|
|
||||||
|
|
||||||
super.addFields([userName, password, isVisible]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<void> onSubmit() async {
|
|
||||||
var user = User(
|
|
||||||
login: userName.state.value,
|
|
||||||
password: password.state.value,
|
|
||||||
);
|
|
||||||
initializingCubit.setRootUser(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
final AppConfigCubit initializingCubit;
|
|
||||||
|
|
||||||
late final FieldCubit<String> userName;
|
|
||||||
late final FieldCubit<String> password;
|
|
||||||
late final FieldCubit<bool> isVisible;
|
|
||||||
}
|
|
|
@ -1,20 +1,17 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:cubit_form/cubit_form.dart';
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/backblaze.dart';
|
import 'package:selfprivacy/logic/api_maps/backblaze.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/models/backblaze_credential.dart';
|
import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
class BackblazeFormCubit extends FormCubit {
|
class BackblazeFormCubit extends FormCubit {
|
||||||
BackblazeFormCubit(this.initializingCubit) {
|
BackblazeFormCubit(this.serverInstallationCubit) {
|
||||||
//var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]");
|
//var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]");
|
||||||
keyId = FieldCubit(
|
keyId = FieldCubit(
|
||||||
initalValue: '',
|
initalValue: '',
|
||||||
validations: [
|
validations: [
|
||||||
RequiredStringValidation('validations.required'.tr()),
|
RequiredStringValidation('validations.required'.tr()),
|
||||||
//ValidationModel<String>(
|
|
||||||
//(s) => regExp.hasMatch(s), 'invalid key format'),
|
|
||||||
//LegnthStringValidationWithLenghShowing(64, 'length is [] shoud be 64')
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -22,9 +19,6 @@ class BackblazeFormCubit extends FormCubit {
|
||||||
initalValue: '',
|
initalValue: '',
|
||||||
validations: [
|
validations: [
|
||||||
RequiredStringValidation('required'),
|
RequiredStringValidation('required'),
|
||||||
//ValidationModel<String>(
|
|
||||||
//(s) => regExp.hasMatch(s), 'invalid key format'),
|
|
||||||
//LegnthStringValidationWithLenghShowing(64, 'length is [] shoud be 64')
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -33,13 +27,13 @@ class BackblazeFormCubit extends FormCubit {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> onSubmit() async {
|
FutureOr<void> onSubmit() async {
|
||||||
initializingCubit.setBackblazeKey(
|
serverInstallationCubit.setBackblazeKey(
|
||||||
keyId.state.value,
|
keyId.state.value,
|
||||||
applicationKey.state.value,
|
applicationKey.state.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final AppConfigCubit initializingCubit;
|
final ServerInstallationCubit serverInstallationCubit;
|
||||||
|
|
||||||
late final FieldCubit<String> keyId;
|
late final FieldCubit<String> keyId;
|
||||||
late final FieldCubit<String> applicationKey;
|
late final FieldCubit<String> applicationKey;
|
||||||
|
@ -47,16 +41,17 @@ class BackblazeFormCubit extends FormCubit {
|
||||||
@override
|
@override
|
||||||
FutureOr<bool> asyncValidation() async {
|
FutureOr<bool> asyncValidation() async {
|
||||||
late bool isKeyValid;
|
late bool isKeyValid;
|
||||||
BackblazeApi apiClient = BackblazeApi(isWithToken: false);
|
final BackblazeApi apiClient = BackblazeApi(isWithToken: false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String encodedApiKey = encodedBackblazeKey(
|
final String encodedApiKey = encodedBackblazeKey(
|
||||||
keyId.state.value,
|
keyId.state.value,
|
||||||
applicationKey.state.value,
|
applicationKey.state.value,
|
||||||
);
|
);
|
||||||
isKeyValid = await apiClient.isValid(encodedApiKey);
|
isKeyValid = await apiClient.isValid(encodedApiKey);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
|
isKeyValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isKeyValid) {
|
if (!isKeyValid) {
|
|
@ -1,22 +1,23 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:cubit_form/cubit_form.dart';
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/cloudflare.dart';
|
|
||||||
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
|
||||||
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:selfprivacy/logic/api_maps/cloudflare.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
|
||||||
|
|
||||||
class CloudFlareFormCubit extends FormCubit {
|
class CloudFlareFormCubit extends FormCubit {
|
||||||
CloudFlareFormCubit(this.initializingCubit) {
|
CloudFlareFormCubit(this.initializingCubit) {
|
||||||
var regExp = RegExp(r"\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]");
|
final RegExp regExp = RegExp(r'\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]');
|
||||||
apiKey = FieldCubit(
|
apiKey = FieldCubit(
|
||||||
initalValue: '',
|
initalValue: '',
|
||||||
validations: [
|
validations: [
|
||||||
RequiredStringValidation('validations.required'.tr()),
|
RequiredStringValidation('validations.required'.tr()),
|
||||||
ValidationModel<String>(
|
ValidationModel<String>(
|
||||||
(s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
|
regExp.hasMatch,
|
||||||
LegnthStringValidationWithLenghShowing(
|
'validations.key_format'.tr(),
|
||||||
40, 'validations.length'.tr(args: ["40"]))
|
),
|
||||||
|
LengthStringNotEqualValidation(40)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -28,14 +29,14 @@ class CloudFlareFormCubit extends FormCubit {
|
||||||
initializingCubit.setCloudflareKey(apiKey.state.value);
|
initializingCubit.setCloudflareKey(apiKey.state.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
final AppConfigCubit initializingCubit;
|
final ServerInstallationCubit initializingCubit;
|
||||||
|
|
||||||
late final FieldCubit<String> apiKey;
|
late final FieldCubit<String> apiKey;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<bool> asyncValidation() async {
|
FutureOr<bool> asyncValidation() async {
|
||||||
late bool isKeyValid;
|
late bool isKeyValid;
|
||||||
CloudflareApi apiClient = CloudflareApi(isWithToken: false);
|
final CloudflareApi apiClient = CloudflareApi(isWithToken: false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isKeyValid = await apiClient.isValid(apiKey.state.value);
|
isKeyValid = await apiClient.isValid(apiKey.state.value);
|
||||||
|
@ -49,9 +50,4 @@ class CloudFlareFormCubit extends FormCubit {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,18 +1,18 @@
|
||||||
import 'package:cubit_form/cubit_form.dart';
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/cloudflare.dart';
|
import 'package:selfprivacy/logic/api_maps/cloudflare.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/models/cloudflare_domain.dart';
|
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||||
|
|
||||||
class DomainSetupCubit extends Cubit<DomainSetupState> {
|
class DomainSetupCubit extends Cubit<DomainSetupState> {
|
||||||
DomainSetupCubit(this.initializingCubit) : super(Initial());
|
DomainSetupCubit(this.serverInstallationCubit) : super(Initial());
|
||||||
|
|
||||||
final AppConfigCubit initializingCubit;
|
final ServerInstallationCubit serverInstallationCubit;
|
||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
emit(Loading(LoadingTypes.loadingDomain));
|
emit(Loading(LoadingTypes.loadingDomain));
|
||||||
var api = CloudflareApi();
|
final CloudflareApi api = CloudflareApi();
|
||||||
|
|
||||||
var list = await api.domainList();
|
final List<String> list = await api.domainList();
|
||||||
if (list.isEmpty) {
|
if (list.isEmpty) {
|
||||||
emit(Empty());
|
emit(Empty());
|
||||||
} else if (list.length == 1) {
|
} else if (list.length == 1) {
|
||||||
|
@ -23,26 +23,25 @@ class DomainSetupCubit extends Cubit<DomainSetupState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() => super.close();
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveDomain() async {
|
Future<void> saveDomain() async {
|
||||||
assert(state is Loaded, 'wrong state');
|
assert(state is Loaded, 'wrong state');
|
||||||
var domainName = (state as Loaded).domain;
|
final String domainName = (state as Loaded).domain;
|
||||||
var api = CloudflareApi();
|
final CloudflareApi api = CloudflareApi();
|
||||||
|
|
||||||
emit(Loading(LoadingTypes.saving));
|
emit(Loading(LoadingTypes.saving));
|
||||||
|
|
||||||
var zoneId = await api.getZoneId(domainName);
|
final String zoneId = await api.getZoneId(domainName);
|
||||||
|
|
||||||
var domain = CloudFlareDomain(
|
final ServerDomain domain = ServerDomain(
|
||||||
domainName: domainName,
|
domainName: domainName,
|
||||||
zoneId: zoneId,
|
zoneId: zoneId,
|
||||||
|
provider: DnsProvider.cloudflare,
|
||||||
);
|
);
|
||||||
|
|
||||||
initializingCubit.setDomain(domain);
|
serverInstallationCubit.setDomain(domain);
|
||||||
emit(DomainSetted());
|
emit(DomainSet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,9 +61,8 @@ class Loading extends DomainSetupState {
|
||||||
enum LoadingTypes { loadingDomain, saving }
|
enum LoadingTypes { loadingDomain, saving }
|
||||||
|
|
||||||
class Loaded extends DomainSetupState {
|
class Loaded extends DomainSetupState {
|
||||||
final String domain;
|
|
||||||
|
|
||||||
Loaded(this.domain);
|
Loaded(this.domain);
|
||||||
|
final String domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DomainSetted extends DomainSetupState {}
|
class DomainSet extends DomainSetupState {}
|
|
@ -1,21 +1,23 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:cubit_form/cubit_form.dart';
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/hetzner.dart';
|
|
||||||
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
|
|
||||||
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:selfprivacy/logic/api_maps/hetzner.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
|
||||||
|
|
||||||
class HetznerFormCubit extends FormCubit {
|
class HetznerFormCubit extends FormCubit {
|
||||||
HetznerFormCubit(this.initializingCubit) {
|
HetznerFormCubit(this.serverInstallationCubit) {
|
||||||
var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]");
|
final RegExp regExp = RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]');
|
||||||
apiKey = FieldCubit(
|
apiKey = FieldCubit(
|
||||||
initalValue: '',
|
initalValue: '',
|
||||||
validations: [
|
validations: [
|
||||||
RequiredStringValidation('validations.required'.tr()),
|
RequiredStringValidation('validations.required'.tr()),
|
||||||
ValidationModel<String>(
|
ValidationModel<String>(
|
||||||
(s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
|
regExp.hasMatch,
|
||||||
LegnthStringValidationWithLenghShowing(64, 'validations.length'.tr(args: ["64"]))
|
'validations.key_format'.tr(),
|
||||||
|
),
|
||||||
|
LengthStringNotEqualValidation(64)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -24,17 +26,17 @@ class HetznerFormCubit extends FormCubit {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> onSubmit() async {
|
FutureOr<void> onSubmit() async {
|
||||||
initializingCubit.setHetznerKey(apiKey.state.value);
|
serverInstallationCubit.setHetznerKey(apiKey.state.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
final AppConfigCubit initializingCubit;
|
final ServerInstallationCubit serverInstallationCubit;
|
||||||
|
|
||||||
late final FieldCubit<String> apiKey;
|
late final FieldCubit<String> apiKey;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<bool> asyncValidation() async {
|
FutureOr<bool> asyncValidation() async {
|
||||||
late bool isKeyValid;
|
late bool isKeyValid;
|
||||||
HetznerApi apiClient = HetznerApi(isWithToken: false);
|
final HetznerApi apiClient = HetznerApi(isWithToken: false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isKeyValid = await apiClient.isValid(apiKey.state.value);
|
isKeyValid = await apiClient.isValid(apiKey.state.value);
|
|
@ -0,0 +1,35 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||||
|
|
||||||
|
class RootUserFormCubit extends FormCubit {
|
||||||
|
RootUserFormCubit(
|
||||||
|
this.serverInstallationCubit,
|
||||||
|
final FieldCubitFactory fieldFactory,
|
||||||
|
) {
|
||||||
|
userName = fieldFactory.createUserLoginField();
|
||||||
|
password = fieldFactory.createUserPasswordField();
|
||||||
|
|
||||||
|
isVisible = FieldCubit(initalValue: false);
|
||||||
|
|
||||||
|
super.addFields([userName, password, isVisible]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> onSubmit() async {
|
||||||
|
final User user = User(
|
||||||
|
login: userName.state.value,
|
||||||
|
password: password.state.value,
|
||||||
|
);
|
||||||
|
serverInstallationCubit.setRootUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ServerInstallationCubit serverInstallationCubit;
|
||||||
|
|
||||||
|
late final FieldCubit<String> userName;
|
||||||
|
late final FieldCubit<String> password;
|
||||||
|
late final FieldCubit<bool> isVisible;
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
|
||||||
|
|
||||||
|
class RecoveryDeviceFormCubit extends FormCubit {
|
||||||
|
RecoveryDeviceFormCubit(
|
||||||
|
this.installationCubit,
|
||||||
|
final FieldCubitFactory fieldFactory,
|
||||||
|
this.recoveryMethod,
|
||||||
|
) {
|
||||||
|
tokenField = fieldFactory.createRequiredStringField();
|
||||||
|
|
||||||
|
super.addFields([tokenField]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> onSubmit() async {
|
||||||
|
late final String token;
|
||||||
|
// Trim spaces and make lowercase
|
||||||
|
if (recoveryMethod == ServerRecoveryMethods.recoveryKey ||
|
||||||
|
recoveryMethod == ServerRecoveryMethods.newDeviceKey) {
|
||||||
|
token = tokenField.state.value.trim().toLowerCase();
|
||||||
|
} else {
|
||||||
|
token = tokenField.state.value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
installationCubit.tryToRecover(token, recoveryMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ServerInstallationCubit installationCubit;
|
||||||
|
late final FieldCubit<String> tokenField;
|
||||||
|
final ServerRecoveryMethods recoveryMethod;
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:selfprivacy/logic/api_maps/server.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
|
||||||
|
|
||||||
|
class RecoveryDomainFormCubit extends FormCubit {
|
||||||
|
RecoveryDomainFormCubit(
|
||||||
|
this.initializingCubit,
|
||||||
|
final FieldCubitFactory fieldFactory,
|
||||||
|
) {
|
||||||
|
serverDomainField = fieldFactory.createRequiredStringField();
|
||||||
|
|
||||||
|
super.addFields([serverDomainField]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> onSubmit() async {
|
||||||
|
initializingCubit
|
||||||
|
.submitDomainForAccessRecovery(serverDomainField.state.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<bool> asyncValidation() async {
|
||||||
|
final ServerApi api = ServerApi(
|
||||||
|
hasLogger: false,
|
||||||
|
isWithToken: false,
|
||||||
|
overrideDomain: serverDomainField.state.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
// API version doesn't require access token,
|
||||||
|
// so if the entered domain is indeed valid
|
||||||
|
// and the server by it is reachable, we will
|
||||||
|
// be able to confirm the input
|
||||||
|
|
||||||
|
final bool domainValid = await api.getApiVersion() != null;
|
||||||
|
if (!domainValid) {
|
||||||
|
serverDomainField.setError('recovering.domain_recover_error'.tr());
|
||||||
|
}
|
||||||
|
|
||||||
|
return domainValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
FutureOr<void> setCustomError(final String error) {
|
||||||
|
serverDomainField.setError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ServerInstallationCubit initializingCubit;
|
||||||
|
late final FieldCubit<String> serverDomainField;
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/job.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||||
|
|
||||||
|
class SshFormCubit extends FormCubit {
|
||||||
|
SshFormCubit({
|
||||||
|
required this.jobsCubit,
|
||||||
|
required this.user,
|
||||||
|
}) {
|
||||||
|
final RegExp keyRegExp = RegExp(
|
||||||
|
r'^(ssh-rsa AAAAB3NzaC1yc2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5)[0-9A-Za-z+/]+[=]{0,3}( .*)?$',
|
||||||
|
);
|
||||||
|
|
||||||
|
key = FieldCubit(
|
||||||
|
initalValue: '',
|
||||||
|
validations: [
|
||||||
|
ValidationModel(
|
||||||
|
(final String newKey) =>
|
||||||
|
user.sshKeys.any((final String key) => key == newKey),
|
||||||
|
'validations.key_already_exists'.tr(),
|
||||||
|
),
|
||||||
|
RequiredStringValidation('validations.required'.tr()),
|
||||||
|
ValidationModel<String>(
|
||||||
|
(final String s) {
|
||||||
|
print(s);
|
||||||
|
print(keyRegExp.hasMatch(s));
|
||||||
|
return !keyRegExp.hasMatch(s);
|
||||||
|
},
|
||||||
|
'validations.invalid_format'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
super.addFields([key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> onSubmit() {
|
||||||
|
print(key.state.isValid);
|
||||||
|
jobsCubit.addJob(CreateSSHKeyJob(user: user, publicKey: key.state.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
late FieldCubit<String> key;
|
||||||
|
|
||||||
|
final JobsCubit jobsCubit;
|
||||||
|
final User user;
|
||||||
|
}
|
|
@ -1,43 +1,25 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:cubit_form/cubit_form.dart';
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/models/job.dart';
|
import 'package:selfprivacy/logic/models/job.dart';
|
||||||
import 'package:selfprivacy/logic/models/user.dart';
|
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:selfprivacy/utils/password_generator.dart';
|
import 'package:selfprivacy/utils/password_generator.dart';
|
||||||
|
|
||||||
class UserFormCubit extends FormCubit {
|
class UserFormCubit extends FormCubit {
|
||||||
UserFormCubit({
|
UserFormCubit({
|
||||||
required this.jobsCubit,
|
required this.jobsCubit,
|
||||||
required List<User> users,
|
required final FieldCubitFactory fieldFactory,
|
||||||
User? user,
|
final User? user,
|
||||||
}) {
|
}) {
|
||||||
var isEdit = user != null;
|
final bool isEdit = user != null;
|
||||||
|
|
||||||
var userRegExp = RegExp(r"\W");
|
login = fieldFactory.createUserLoginField();
|
||||||
var passwordRegExp = RegExp(r"[\n\r\s]+");
|
login.setValue(isEdit ? user.login : '');
|
||||||
|
password = fieldFactory.createUserPasswordField();
|
||||||
login = FieldCubit(
|
password.setValue(
|
||||||
initalValue: isEdit ? user!.login : '',
|
isEdit ? (user.password ?? '') : StringGenerators.userPassword(),
|
||||||
validations: [
|
|
||||||
ValidationModel(
|
|
||||||
(login) => users.any((user) => user.login == login),
|
|
||||||
'validations.user_alredy_exist'.tr(),
|
|
||||||
),
|
|
||||||
RequiredStringValidation('validations.required'.tr()),
|
|
||||||
ValidationModel<String>(
|
|
||||||
(s) => userRegExp.hasMatch(s), 'validations.invalid_format'.tr()),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
password = FieldCubit(
|
|
||||||
initalValue: isEdit ? user!.password : StringGenerators.userPassword(),
|
|
||||||
validations: [
|
|
||||||
RequiredStringValidation('validations.required'.tr()),
|
|
||||||
ValidationModel<String>((s) => passwordRegExp.hasMatch(s),
|
|
||||||
'validations.invalid_format'.tr()),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
super.addFields([login, password]);
|
super.addFields([login, password]);
|
||||||
|
@ -45,7 +27,7 @@ class UserFormCubit extends FormCubit {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> onSubmit() {
|
FutureOr<void> onSubmit() {
|
||||||
var user = User(
|
final User user = User(
|
||||||
login: login.state.value,
|
login: login.state.value,
|
||||||
password: password.state.value,
|
password: password.state.value,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,13 +1,32 @@
|
||||||
import 'package:cubit_form/cubit_form.dart';
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
class LegnthStringValidationWithLenghShowing extends ValidationModel<String> {
|
abstract class LengthStringValidation extends ValidationModel<String> {
|
||||||
LegnthStringValidationWithLenghShowing(int length, String errorText)
|
LengthStringValidation(super.predicate, super.errorMessage);
|
||||||
: super((n) => n.length != length, errorText);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? check(String val) {
|
String? check(final String val) {
|
||||||
var length = val.length;
|
final int length = val.length;
|
||||||
var errorMassage = this.errorMassage.replaceAll("[]", length.toString());
|
final String errorMessage =
|
||||||
return test(val) ? errorMassage : null;
|
errorMassage.replaceAll('[]', length.toString());
|
||||||
|
return test(val) ? errorMessage : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LengthStringNotEqualValidation extends LengthStringValidation {
|
||||||
|
/// String must be equal to [length]
|
||||||
|
LengthStringNotEqualValidation(final int length)
|
||||||
|
: super(
|
||||||
|
(final n) => n.length != length,
|
||||||
|
'validations.length_not_equal'.tr(args: [length.toString()]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class LengthStringLongerValidation extends LengthStringValidation {
|
||||||
|
/// String must be shorter than or equal to [length]
|
||||||
|
LengthStringLongerValidation(final int length)
|
||||||
|
: super(
|
||||||
|
(final n) => n.length > length,
|
||||||
|
'validations.length_longer'.tr(args: [length.toString()]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
||||||
import 'package:selfprivacy/logic/models/hetzner_metrics.dart';
|
import 'package:selfprivacy/logic/models/hetzner_metrics.dart';
|
||||||
|
|
||||||
import 'hetzner_metrics_repository.dart';
|
import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart';
|
||||||
|
|
||||||
part 'hetzner_metrics_state.dart';
|
part 'hetzner_metrics_state.dart';
|
||||||
|
|
||||||
class HetznerMetricsCubit extends Cubit<HetznerMetricsState> {
|
class HetznerMetricsCubit extends Cubit<HetznerMetricsState> {
|
||||||
HetznerMetricsCubit() : super(HetznerMetricsLoading(Period.day));
|
HetznerMetricsCubit() : super(const HetznerMetricsLoading(Period.day));
|
||||||
|
|
||||||
final repository = HetznerMetricsRepository();
|
final HetznerMetricsRepository repository = HetznerMetricsRepository();
|
||||||
|
|
||||||
Timer? timer;
|
Timer? timer;
|
||||||
|
|
||||||
close() {
|
@override
|
||||||
|
Future<void> close() {
|
||||||
closeTimer();
|
closeTimer();
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
@ -27,7 +28,7 @@ class HetznerMetricsCubit extends Cubit<HetznerMetricsState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void changePeriod(Period period) async {
|
void changePeriod(final Period period) async {
|
||||||
closeTimer();
|
closeTimer();
|
||||||
emit(HetznerMetricsLoading(period));
|
emit(HetznerMetricsLoading(period));
|
||||||
load(period);
|
load(period);
|
||||||
|
@ -37,8 +38,8 @@ class HetznerMetricsCubit extends Cubit<HetznerMetricsState> {
|
||||||
load(state.period);
|
load(state.period);
|
||||||
}
|
}
|
||||||
|
|
||||||
void load(Period period) async {
|
void load(final Period period) async {
|
||||||
var newState = await repository.getMetrics(period);
|
final HetznerMetricsLoaded newState = await repository.getMetrics(period);
|
||||||
timer = Timer(
|
timer = Timer(
|
||||||
Duration(seconds: newState.stepInSeconds.toInt()),
|
Duration(seconds: newState.stepInSeconds.toInt()),
|
||||||
() => load(newState.period),
|
() => load(newState.period),
|
||||||
|
|
|
@ -2,40 +2,40 @@ import 'package:selfprivacy/logic/api_maps/hetzner.dart';
|
||||||
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
||||||
import 'package:selfprivacy/logic/models/hetzner_metrics.dart';
|
import 'package:selfprivacy/logic/models/hetzner_metrics.dart';
|
||||||
|
|
||||||
import 'hetzner_metrics_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart';
|
||||||
|
|
||||||
class HetznerMetricsRepository {
|
class HetznerMetricsRepository {
|
||||||
Future<HetznerMetricsLoaded> getMetrics(Period period) async {
|
Future<HetznerMetricsLoaded> getMetrics(final Period period) async {
|
||||||
var end = DateTime.now();
|
final DateTime end = DateTime.now();
|
||||||
DateTime start;
|
DateTime start;
|
||||||
|
|
||||||
switch (period) {
|
switch (period) {
|
||||||
case Period.hour:
|
case Period.hour:
|
||||||
start = end.subtract(Duration(hours: 1));
|
start = end.subtract(const Duration(hours: 1));
|
||||||
break;
|
break;
|
||||||
case Period.day:
|
case Period.day:
|
||||||
start = end.subtract(Duration(days: 1));
|
start = end.subtract(const Duration(days: 1));
|
||||||
break;
|
break;
|
||||||
case Period.month:
|
case Period.month:
|
||||||
start = end.subtract(Duration(days: 15));
|
start = end.subtract(const Duration(days: 15));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var api = HetznerApi(hasLoger: true);
|
final HetznerApi api = HetznerApi(hasLogger: true);
|
||||||
|
|
||||||
var results = await Future.wait([
|
final List<Map<String, dynamic>> results = await Future.wait([
|
||||||
api.getMetrics(start, end, 'cpu'),
|
api.getMetrics(start, end, 'cpu'),
|
||||||
api.getMetrics(start, end, 'network'),
|
api.getMetrics(start, end, 'network'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
var cpuMetricsData = results[0]["metrics"];
|
final cpuMetricsData = results[0]['metrics'];
|
||||||
var networkMetricsData = results[1]["metrics"];
|
final networkMetricsData = results[1]['metrics'];
|
||||||
|
|
||||||
return HetznerMetricsLoaded(
|
return HetznerMetricsLoaded(
|
||||||
period: period,
|
period: period,
|
||||||
start: start,
|
start: start,
|
||||||
end: end,
|
end: end,
|
||||||
stepInSeconds: cpuMetricsData["step"],
|
stepInSeconds: cpuMetricsData['step'],
|
||||||
cpu: timeSeriesSerializer(cpuMetricsData, 'cpu'),
|
cpu: timeSeriesSerializer(cpuMetricsData, 'cpu'),
|
||||||
ppsIn: timeSeriesSerializer(networkMetricsData, 'network.0.pps.in'),
|
ppsIn: timeSeriesSerializer(networkMetricsData, 'network.0.pps.in'),
|
||||||
ppsOut: timeSeriesSerializer(networkMetricsData, 'network.0.pps.out'),
|
ppsOut: timeSeriesSerializer(networkMetricsData, 'network.0.pps.out'),
|
||||||
|
@ -50,7 +50,11 @@ class HetznerMetricsRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TimeSeriesData> timeSeriesSerializer(
|
List<TimeSeriesData> timeSeriesSerializer(
|
||||||
Map<String, dynamic> json, String type) {
|
final Map<String, dynamic> json,
|
||||||
List list = json["time_series"][type]["values"];
|
final String type,
|
||||||
return list.map((el) => TimeSeriesData(el[0], double.parse(el[1]))).toList();
|
) {
|
||||||
|
final List list = json['time_series'][type]['values'];
|
||||||
|
return list
|
||||||
|
.map((final el) => TimeSeriesData(el[0], double.parse(el[1])))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,8 @@ abstract class HetznerMetricsState extends Equatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
class HetznerMetricsLoading extends HetznerMetricsState {
|
class HetznerMetricsLoading extends HetznerMetricsState {
|
||||||
HetznerMetricsLoading(this.period);
|
const HetznerMetricsLoading(this.period);
|
||||||
|
@override
|
||||||
final Period period;
|
final Period period;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -15,7 +16,7 @@ class HetznerMetricsLoading extends HetznerMetricsState {
|
||||||
}
|
}
|
||||||
|
|
||||||
class HetznerMetricsLoaded extends HetznerMetricsState {
|
class HetznerMetricsLoaded extends HetznerMetricsState {
|
||||||
HetznerMetricsLoaded({
|
const HetznerMetricsLoaded({
|
||||||
required this.period,
|
required this.period,
|
||||||
required this.start,
|
required this.start,
|
||||||
required this.end,
|
required this.end,
|
||||||
|
@ -27,6 +28,7 @@ class HetznerMetricsLoaded extends HetznerMetricsState {
|
||||||
required this.bandwidthOut,
|
required this.bandwidthOut,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
final Period period;
|
final Period period;
|
||||||
final DateTime start;
|
final DateTime start;
|
||||||
final DateTime end;
|
final DateTime end;
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/server.dart';
|
import 'package:selfprivacy/logic/api_maps/server.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/get_it/ssh.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/job.dart';
|
import 'package:selfprivacy/logic/models/job.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/user.dart';
|
|
||||||
export 'package:provider/provider.dart';
|
export 'package:provider/provider.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
|
|
||||||
part 'jobs_state.dart';
|
part 'jobs_state.dart';
|
||||||
|
|
||||||
|
@ -18,12 +17,12 @@ class JobsCubit extends Cubit<JobsState> {
|
||||||
required this.servicesCubit,
|
required this.servicesCubit,
|
||||||
}) : super(JobsStateEmpty());
|
}) : super(JobsStateEmpty());
|
||||||
|
|
||||||
final api = ServerApi();
|
final ServerApi api = ServerApi();
|
||||||
final UsersCubit usersCubit;
|
final UsersCubit usersCubit;
|
||||||
final ServicesCubit servicesCubit;
|
final ServicesCubit servicesCubit;
|
||||||
|
|
||||||
void addJob(Job job) {
|
void addJob(final Job job) {
|
||||||
var newJobsList = <Job>[];
|
final List<Job> newJobsList = <Job>[];
|
||||||
if (state is JobsStateWithJobs) {
|
if (state is JobsStateWithJobs) {
|
||||||
newJobsList.addAll((state as JobsStateWithJobs).jobList);
|
newJobsList.addAll((state as JobsStateWithJobs).jobList);
|
||||||
}
|
}
|
||||||
|
@ -32,21 +31,22 @@ class JobsCubit extends Cubit<JobsState> {
|
||||||
emit(JobsStateWithJobs(newJobsList));
|
emit(JobsStateWithJobs(newJobsList));
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeJob(String id) {
|
void removeJob(final String id) {
|
||||||
final newState = (state as JobsStateWithJobs).removeById(id);
|
final JobsState newState = (state as JobsStateWithJobs).removeById(id);
|
||||||
emit(newState);
|
emit(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
void createOrRemoveServiceToggleJob(ServiceToggleJob job) {
|
void createOrRemoveServiceToggleJob(final ToggleJob job) {
|
||||||
var newJobsList = <Job>[];
|
final List<Job> newJobsList = <Job>[];
|
||||||
if (state is JobsStateWithJobs) {
|
if (state is JobsStateWithJobs) {
|
||||||
newJobsList.addAll((state as JobsStateWithJobs).jobList);
|
newJobsList.addAll((state as JobsStateWithJobs).jobList);
|
||||||
}
|
}
|
||||||
var needToRemoveJob =
|
final bool needToRemoveJob = newJobsList
|
||||||
newJobsList.any((el) => el is ServiceToggleJob && el.type == job.type);
|
.any((final el) => el is ServiceToggleJob && el.type == job.type);
|
||||||
if (needToRemoveJob) {
|
if (needToRemoveJob) {
|
||||||
var removingJob = newJobsList
|
final Job removingJob = newJobsList.firstWhere(
|
||||||
.firstWhere(((el) => el is ServiceToggleJob && el.type == job.type));
|
(final el) => el is ServiceToggleJob && el.type == job.type,
|
||||||
|
);
|
||||||
removeJob(removingJob.id);
|
removeJob(removingJob.id);
|
||||||
} else {
|
} else {
|
||||||
newJobsList.add(job);
|
newJobsList.add(job);
|
||||||
|
@ -55,12 +55,13 @@ class JobsCubit extends Cubit<JobsState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void createShhJobIfNotExist(CreateSSHKeyJob job) {
|
void createShhJobIfNotExist(final CreateSSHKeyJob job) {
|
||||||
var newJobsList = <Job>[];
|
final List<Job> newJobsList = <Job>[];
|
||||||
if (state is JobsStateWithJobs) {
|
if (state is JobsStateWithJobs) {
|
||||||
newJobsList.addAll((state as JobsStateWithJobs).jobList);
|
newJobsList.addAll((state as JobsStateWithJobs).jobList);
|
||||||
}
|
}
|
||||||
var isExistInJobList = newJobsList.any((el) => el is CreateSSHKeyJob);
|
final bool isExistInJobList =
|
||||||
|
newJobsList.any((final el) => el is CreateSSHKeyJob);
|
||||||
if (!isExistInJobList) {
|
if (!isExistInJobList) {
|
||||||
newJobsList.add(job);
|
newJobsList.add(job);
|
||||||
getIt<NavigationService>().showSnackBar('jobs.jobAdded'.tr());
|
getIt<NavigationService>().showSnackBar('jobs.jobAdded'.tr());
|
||||||
|
@ -68,35 +69,64 @@ class JobsCubit extends Cubit<JobsState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> rebootServer() async {
|
||||||
|
emit(JobsStateLoading());
|
||||||
|
final bool isSuccessful = await api.reboot();
|
||||||
|
if (isSuccessful) {
|
||||||
|
getIt<NavigationService>().showSnackBar('jobs.rebootSuccess'.tr());
|
||||||
|
} else {
|
||||||
|
getIt<NavigationService>().showSnackBar('jobs.rebootFailed'.tr());
|
||||||
|
}
|
||||||
|
emit(JobsStateEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> upgradeServer() async {
|
||||||
|
emit(JobsStateLoading());
|
||||||
|
final bool isPullSuccessful = await api.pullConfigurationUpdate();
|
||||||
|
final bool isSuccessful = await api.upgrade();
|
||||||
|
if (isSuccessful) {
|
||||||
|
if (!isPullSuccessful) {
|
||||||
|
getIt<NavigationService>().showSnackBar('jobs.configPullFailed'.tr());
|
||||||
|
} else {
|
||||||
|
getIt<NavigationService>().showSnackBar('jobs.upgradeSuccess'.tr());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getIt<NavigationService>().showSnackBar('jobs.upgradeFailed'.tr());
|
||||||
|
}
|
||||||
|
emit(JobsStateEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> applyAll() async {
|
Future<void> applyAll() async {
|
||||||
if (state is JobsStateWithJobs) {
|
if (state is JobsStateWithJobs) {
|
||||||
var jobs = (state as JobsStateWithJobs).jobList;
|
final List<Job> jobs = (state as JobsStateWithJobs).jobList;
|
||||||
emit(JobsStateLoading());
|
emit(JobsStateLoading());
|
||||||
var newUsers = <User>[];
|
bool hasServiceJobs = false;
|
||||||
var hasServiceJobs = false;
|
for (final Job job in jobs) {
|
||||||
for (var job in jobs) {
|
|
||||||
if (job is CreateUserJob) {
|
if (job is CreateUserJob) {
|
||||||
newUsers.add(job.user);
|
await usersCubit.createUser(job.user);
|
||||||
await api.createUser(job.user);
|
}
|
||||||
} else if (job is ServiceToggleJob) {
|
if (job is DeleteUserJob) {
|
||||||
|
await usersCubit.deleteUser(job.user);
|
||||||
|
}
|
||||||
|
if (job is ServiceToggleJob) {
|
||||||
hasServiceJobs = true;
|
hasServiceJobs = true;
|
||||||
await api.switchService(job.type, job.needToTurnOn);
|
await api.switchService(job.type, job.needToTurnOn);
|
||||||
}
|
}
|
||||||
if (job is CreateSSHKeyJob) {
|
if (job is CreateSSHKeyJob) {
|
||||||
await getIt<SSHModel>().generateKeys();
|
await usersCubit.addSshKey(job.user, job.publicKey);
|
||||||
api.sendSsh(getIt<SSHModel>().savedPubKey!);
|
}
|
||||||
|
if (job is DeleteSSHKeyJob) {
|
||||||
|
await usersCubit.deleteSshKey(job.user, job.publicKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
usersCubit.addUsers(newUsers);
|
await api.pullConfigurationUpdate();
|
||||||
await api.apply();
|
await api.apply();
|
||||||
if (hasServiceJobs) {
|
if (hasServiceJobs) {
|
||||||
await servicesCubit.load();
|
await servicesCubit.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(JobsStateEmpty());
|
emit(JobsStateEmpty());
|
||||||
|
|
||||||
getIt<NavigationService>().navigator!.pop();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,9 @@ class JobsStateWithJobs extends JobsState {
|
||||||
JobsStateWithJobs(this.jobList);
|
JobsStateWithJobs(this.jobList);
|
||||||
final List<Job> jobList;
|
final List<Job> jobList;
|
||||||
|
|
||||||
JobsState removeById(String id) {
|
JobsState removeById(final String id) {
|
||||||
var newJobsList = jobList.where((element) => element.id != id).toList();
|
final List<Job> newJobsList =
|
||||||
|
jobList.where((final element) => element.id != id).toList();
|
||||||
|
|
||||||
if (newJobsList.isEmpty) {
|
if (newJobsList.isEmpty) {
|
||||||
return JobsStateEmpty();
|
return JobsStateEmpty();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:selfprivacy/logic/models/provider.dart';
|
import 'package:selfprivacy/logic/models/provider.dart';
|
||||||
import 'package:selfprivacy/logic/models/state_types.dart';
|
import 'package:selfprivacy/logic/models/state_types.dart';
|
||||||
|
@ -11,10 +11,9 @@ part 'providers_state.dart';
|
||||||
class ProvidersCubit extends Cubit<ProvidersState> {
|
class ProvidersCubit extends Cubit<ProvidersState> {
|
||||||
ProvidersCubit() : super(InitialProviderState());
|
ProvidersCubit() : super(InitialProviderState());
|
||||||
|
|
||||||
void connect(ProviderModel provider) {
|
void connect(final ProviderModel provider) {
|
||||||
var newState = state.updateElement(provider, StateType.stable);
|
final ProvidersState newState =
|
||||||
|
state.updateElement(provider, StateType.stable);
|
||||||
emit(newState);
|
emit(newState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,18 +5,23 @@ class ProvidersState extends Equatable {
|
||||||
|
|
||||||
final List<ProviderModel> all;
|
final List<ProviderModel> all;
|
||||||
|
|
||||||
ProvidersState updateElement(ProviderModel provider, StateType newState) {
|
ProvidersState updateElement(
|
||||||
var newList = [...all];
|
final ProviderModel provider,
|
||||||
var index = newList.indexOf(provider);
|
final StateType newState,
|
||||||
|
) {
|
||||||
|
final List<ProviderModel> newList = [...all];
|
||||||
|
final int index = newList.indexOf(provider);
|
||||||
newList[index] = provider.updateState(newState);
|
newList[index] = provider.updateState(newState);
|
||||||
return ProvidersState(newList);
|
return ProvidersState(newList);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ProviderModel> get connected =>
|
List<ProviderModel> get connected => all
|
||||||
all.where((service) => service.state != StateType.uninitialized).toList();
|
.where((final service) => service.state != StateType.uninitialized)
|
||||||
|
.toList();
|
||||||
|
|
||||||
List<ProviderModel> get uninitialized =>
|
List<ProviderModel> get uninitialized => all
|
||||||
all.where((service) => service.state == StateType.uninitialized).toList();
|
.where((final service) => service.state == StateType.uninitialized)
|
||||||
|
.toList();
|
||||||
|
|
||||||
bool get isFullyInitialized => uninitialized.isEmpty;
|
bool get isFullyInitialized => uninitialized.isEmpty;
|
||||||
|
|
||||||
|
@ -29,7 +34,7 @@ class InitialProviderState extends ProvidersState {
|
||||||
: super(
|
: super(
|
||||||
ProviderType.values
|
ProviderType.values
|
||||||
.map(
|
.map(
|
||||||
(type) => ProviderModel(
|
(final type) => ProviderModel(
|
||||||
state: StateType.uninitialized,
|
state: StateType.uninitialized,
|
||||||
type: type,
|
type: type,
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
import 'package:selfprivacy/logic/api_maps/server.dart';
|
||||||
|
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';
|
||||||
|
|
||||||
|
part 'recovery_key_state.dart';
|
||||||
|
|
||||||
|
class RecoveryKeyCubit
|
||||||
|
extends ServerInstallationDependendCubit<RecoveryKeyState> {
|
||||||
|
RecoveryKeyCubit(final ServerInstallationCubit serverInstallationCubit)
|
||||||
|
: super(serverInstallationCubit, const RecoveryKeyState.initial());
|
||||||
|
|
||||||
|
final ServerApi api = ServerApi();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void load() async {
|
||||||
|
if (serverInstallationCubit.state is ServerInstallationFinished) {
|
||||||
|
final RecoveryKeyStatus? status = await _getRecoveryKeyStatus();
|
||||||
|
if (status == null) {
|
||||||
|
emit(state.copyWith(loadingStatus: LoadingStatus.error));
|
||||||
|
} else {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: status,
|
||||||
|
loadingStatus: LoadingStatus.success,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<RecoveryKeyStatus?> _getRecoveryKeyStatus() async {
|
||||||
|
final ApiResponse<RecoveryKeyStatus?> response =
|
||||||
|
await api.getRecoveryTokenStatus();
|
||||||
|
if (response.isSuccess) {
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
emit(state.copyWith(loadingStatus: LoadingStatus.refreshing));
|
||||||
|
final RecoveryKeyStatus? status = await _getRecoveryKeyStatus();
|
||||||
|
if (status == null) {
|
||||||
|
emit(state.copyWith(loadingStatus: LoadingStatus.error));
|
||||||
|
} else {
|
||||||
|
emit(
|
||||||
|
state.copyWith(status: status, loadingStatus: LoadingStatus.success),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> generateRecoveryKey({
|
||||||
|
final DateTime? expirationDate,
|
||||||
|
final int? numberOfUses,
|
||||||
|
}) async {
|
||||||
|
final ApiResponse<String> response =
|
||||||
|
await api.generateRecoveryToken(expirationDate, numberOfUses);
|
||||||
|
if (response.isSuccess) {
|
||||||
|
refresh();
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
throw GenerationError(response.errorMessage ?? 'Unknown error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void clear() {
|
||||||
|
emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GenerationError extends Error {
|
||||||
|
GenerationError(this.message);
|
||||||
|
final String message;
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
part of 'recovery_key_cubit.dart';
|
||||||
|
|
||||||
|
class RecoveryKeyState extends ServerInstallationDependendState {
|
||||||
|
const RecoveryKeyState(this._status, this.loadingStatus);
|
||||||
|
|
||||||
|
const RecoveryKeyState.initial()
|
||||||
|
: this(
|
||||||
|
const RecoveryKeyStatus(exists: false, valid: false),
|
||||||
|
LoadingStatus.refreshing,
|
||||||
|
);
|
||||||
|
|
||||||
|
final RecoveryKeyStatus _status;
|
||||||
|
final LoadingStatus loadingStatus;
|
||||||
|
|
||||||
|
bool get exists => _status.exists;
|
||||||
|
bool get isValid => _status.valid;
|
||||||
|
DateTime? get generatedAt => _status.date;
|
||||||
|
DateTime? get expiresAt => _status.expiration;
|
||||||
|
int? get usesLeft => _status.usesLeft;
|
||||||
|
|
||||||
|
bool get isInvalidBecauseExpired =>
|
||||||
|
_status.expiration != null &&
|
||||||
|
_status.expiration!.isBefore(DateTime.now());
|
||||||
|
|
||||||
|
bool get isInvalidBecauseUsed =>
|
||||||
|
_status.usesLeft != null && _status.usesLeft == 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [_status, loadingStatus];
|
||||||
|
|
||||||
|
RecoveryKeyState copyWith({
|
||||||
|
final RecoveryKeyStatus? status,
|
||||||
|
final LoadingStatus? loadingStatus,
|
||||||
|
}) =>
|
||||||
|
RecoveryKeyState(
|
||||||
|
status ?? _status,
|
||||||
|
loadingStatus ?? this.loadingStatus,
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_repository.dart';
|
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_repository.dart';
|
||||||
import 'package:selfprivacy/logic/models/hetzner_server_info.dart';
|
import 'package:selfprivacy/logic/models/json/auto_upgrade_settings.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/timezone_settings.dart';
|
||||||
|
|
||||||
part 'server_detailed_info_state.dart';
|
part 'server_detailed_info_state.dart';
|
||||||
|
|
||||||
|
@ -12,11 +14,18 @@ class ServerDetailsCubit extends Cubit<ServerDetailsState> {
|
||||||
ServerDetailsRepository repository = ServerDetailsRepository();
|
ServerDetailsRepository repository = ServerDetailsRepository();
|
||||||
|
|
||||||
void check() async {
|
void check() async {
|
||||||
var isReadyToCheck = getIt<ApiConfigModel>().hetznerServer != null;
|
final bool isReadyToCheck = getIt<ApiConfigModel>().serverDetails != null;
|
||||||
if (isReadyToCheck) {
|
if (isReadyToCheck) {
|
||||||
emit(ServerDetailsLoading());
|
emit(ServerDetailsLoading());
|
||||||
var data = await repository.load();
|
final ServerDetailsRepositoryDto data = await repository.load();
|
||||||
emit(Loaded(serverInfo: data, checkTime: DateTime.now()));
|
emit(
|
||||||
|
Loaded(
|
||||||
|
serverInfo: data.hetznerServerInfo,
|
||||||
|
autoUpgradeSettings: data.autoUpgradeSettings,
|
||||||
|
serverTimezone: data.serverTimezone,
|
||||||
|
checkTime: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
emit(ServerDetailsNotReady());
|
emit(ServerDetailsNotReady());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,32 @@
|
||||||
import 'package:selfprivacy/logic/api_maps/hetzner.dart';
|
import 'package:selfprivacy/logic/api_maps/hetzner.dart';
|
||||||
import 'package:selfprivacy/logic/models/hetzner_server_info.dart';
|
import 'package:selfprivacy/logic/api_maps/server.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/auto_upgrade_settings.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/timezone_settings.dart';
|
||||||
|
|
||||||
class ServerDetailsRepository {
|
class ServerDetailsRepository {
|
||||||
Future<HetznerServerInfo> load() async {
|
HetznerApi hetznerAPi = HetznerApi();
|
||||||
var client = HetznerApi();
|
ServerApi selfprivacyServer = ServerApi();
|
||||||
return await client.getInfo();
|
|
||||||
|
Future<ServerDetailsRepositoryDto> load() async {
|
||||||
|
print('load');
|
||||||
|
return ServerDetailsRepositoryDto(
|
||||||
|
autoUpgradeSettings: await selfprivacyServer.getAutoUpgradeSettings(),
|
||||||
|
hetznerServerInfo: await hetznerAPi.getInfo(),
|
||||||
|
serverTimezone: await selfprivacyServer.getServerTimezone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ServerDetailsRepositoryDto {
|
||||||
|
ServerDetailsRepositoryDto({
|
||||||
|
required this.hetznerServerInfo,
|
||||||
|
required this.serverTimezone,
|
||||||
|
required this.autoUpgradeSettings,
|
||||||
|
});
|
||||||
|
final HetznerServerInfo hetznerServerInfo;
|
||||||
|
|
||||||
|
final TimeZoneSettings serverTimezone;
|
||||||
|
|
||||||
|
final AutoUpgradeSettings autoUpgradeSettings;
|
||||||
|
}
|
||||||
|
|
|
@ -16,14 +16,24 @@ class ServerDetailsNotReady extends ServerDetailsState {}
|
||||||
class Loading extends ServerDetailsState {}
|
class Loading extends ServerDetailsState {}
|
||||||
|
|
||||||
class Loaded extends ServerDetailsState {
|
class Loaded extends ServerDetailsState {
|
||||||
final HetznerServerInfo serverInfo;
|
const Loaded({
|
||||||
final DateTime checkTime;
|
|
||||||
|
|
||||||
Loaded({
|
|
||||||
required this.serverInfo,
|
required this.serverInfo,
|
||||||
|
required this.serverTimezone,
|
||||||
|
required this.autoUpgradeSettings,
|
||||||
required this.checkTime,
|
required this.checkTime,
|
||||||
});
|
});
|
||||||
|
final HetznerServerInfo serverInfo;
|
||||||
|
|
||||||
|
final TimeZoneSettings serverTimezone;
|
||||||
|
|
||||||
|
final AutoUpgradeSettings autoUpgradeSettings;
|
||||||
|
final DateTime checkTime;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [serverInfo, checkTime];
|
List<Object> get props => [
|
||||||
|
serverInfo,
|
||||||
|
serverTimezone,
|
||||||
|
autoUpgradeSettings,
|
||||||
|
checkTime,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,626 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/server_basic_info.dart';
|
||||||
|
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart';
|
||||||
|
|
||||||
|
export 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
part '../server_installation/server_installation_state.dart';
|
||||||
|
|
||||||
|
class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
||||||
|
ServerInstallationCubit() : super(const ServerInstallationEmpty());
|
||||||
|
|
||||||
|
final ServerInstallationRepository repository =
|
||||||
|
ServerInstallationRepository();
|
||||||
|
|
||||||
|
Timer? timer;
|
||||||
|
|
||||||
|
Future<void> load() async {
|
||||||
|
final ServerInstallationState state = await repository.load();
|
||||||
|
|
||||||
|
if (state is ServerInstallationFinished) {
|
||||||
|
emit(state);
|
||||||
|
} else if (state is ServerInstallationNotFinished) {
|
||||||
|
if (state.progress == ServerSetupProgress.serverCreated) {
|
||||||
|
startServerIfDnsIsOkay(state: state);
|
||||||
|
} else if (state.progress == ServerSetupProgress.serverStarted) {
|
||||||
|
resetServerIfServerIsOkay(state: state);
|
||||||
|
} else if (state.progress == ServerSetupProgress.serverResetedFirstTime) {
|
||||||
|
oneMoreReset(state: state);
|
||||||
|
} else if (state.progress ==
|
||||||
|
ServerSetupProgress.serverResetedSecondTime) {
|
||||||
|
finishCheckIfServerIsOkay(state: state);
|
||||||
|
} else {
|
||||||
|
emit(state);
|
||||||
|
}
|
||||||
|
} else if (state is ServerInstallationRecovery) {
|
||||||
|
emit(state);
|
||||||
|
} else {
|
||||||
|
throw 'wrong state';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setHetznerKey(final String hetznerKey) async {
|
||||||
|
await repository.saveHetznerKey(hetznerKey);
|
||||||
|
|
||||||
|
if (state is ServerInstallationRecovery) {
|
||||||
|
emit(
|
||||||
|
(state as ServerInstallationRecovery).copyWith(
|
||||||
|
hetznerKey: hetznerKey,
|
||||||
|
currentStep: RecoveryStep.serverSelection,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(
|
||||||
|
(state as ServerInstallationNotFinished).copyWith(hetznerKey: hetznerKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCloudflareKey(final String cloudFlareKey) async {
|
||||||
|
if (state is ServerInstallationRecovery) {
|
||||||
|
setAndValidateCloudflareToken(cloudFlareKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await repository.saveCloudFlareKey(cloudFlareKey);
|
||||||
|
emit(
|
||||||
|
(state as ServerInstallationNotFinished)
|
||||||
|
.copyWith(cloudFlareKey: cloudFlareKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setBackblazeKey(final String keyId, final String applicationKey) async {
|
||||||
|
final BackblazeCredential backblazeCredential = BackblazeCredential(
|
||||||
|
keyId: keyId,
|
||||||
|
applicationKey: applicationKey,
|
||||||
|
);
|
||||||
|
await repository.saveBackblazeKey(backblazeCredential);
|
||||||
|
if (state is ServerInstallationRecovery) {
|
||||||
|
finishRecoveryProcess(backblazeCredential);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit(
|
||||||
|
(state as ServerInstallationNotFinished)
|
||||||
|
.copyWith(backblazeCredential: backblazeCredential),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDomain(final ServerDomain serverDomain) async {
|
||||||
|
await repository.saveDomain(serverDomain);
|
||||||
|
emit(
|
||||||
|
(state as ServerInstallationNotFinished)
|
||||||
|
.copyWith(serverDomain: serverDomain),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRootUser(final User rootUser) async {
|
||||||
|
await repository.saveRootUser(rootUser);
|
||||||
|
emit((state as ServerInstallationNotFinished).copyWith(rootUser: rootUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
void createServerAndSetDnsRecords() async {
|
||||||
|
final ServerInstallationNotFinished stateCopy =
|
||||||
|
state as ServerInstallationNotFinished;
|
||||||
|
void onCancel() => emit(
|
||||||
|
(state as ServerInstallationNotFinished).copyWith(isLoading: false),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> onSuccess(final ServerHostingDetails serverDetails) async {
|
||||||
|
await repository.createDnsRecords(
|
||||||
|
serverDetails.ip4,
|
||||||
|
state.serverDomain!,
|
||||||
|
onCancel: onCancel,
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
(state as ServerInstallationNotFinished).copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
serverDetails: serverDetails,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
runDelayed(startServerIfDnsIsOkay, const Duration(seconds: 30), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
emit((state as ServerInstallationNotFinished).copyWith(isLoading: true));
|
||||||
|
await repository.createServer(
|
||||||
|
state.rootUser!,
|
||||||
|
state.serverDomain!.domainName,
|
||||||
|
state.cloudFlareKey!,
|
||||||
|
state.backblazeCredential!,
|
||||||
|
onCancel: onCancel,
|
||||||
|
onSuccess: onSuccess,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(stateCopy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void startServerIfDnsIsOkay({
|
||||||
|
final ServerInstallationNotFinished? state,
|
||||||
|
}) async {
|
||||||
|
final ServerInstallationNotFinished dataState =
|
||||||
|
state ?? this.state as ServerInstallationNotFinished;
|
||||||
|
|
||||||
|
emit(TimerState(dataState: dataState, isLoading: true));
|
||||||
|
|
||||||
|
final String ip4 = dataState.serverDetails!.ip4;
|
||||||
|
final String domainName = dataState.serverDomain!.domainName;
|
||||||
|
|
||||||
|
final Map<String, bool> matches = await repository.isDnsAddressesMatch(
|
||||||
|
domainName,
|
||||||
|
ip4,
|
||||||
|
dataState.dnsMatches ?? {},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matches.values.every((final bool value) => value)) {
|
||||||
|
final ServerHostingDetails server = await repository.startServer(
|
||||||
|
dataState.serverDetails!,
|
||||||
|
);
|
||||||
|
await repository.saveServerDetails(server);
|
||||||
|
await repository.saveIsServerStarted(true);
|
||||||
|
|
||||||
|
final ServerInstallationNotFinished newState = dataState.copyWith(
|
||||||
|
isServerStarted: true,
|
||||||
|
isLoading: false,
|
||||||
|
serverDetails: server,
|
||||||
|
);
|
||||||
|
emit(newState);
|
||||||
|
runDelayed(
|
||||||
|
resetServerIfServerIsOkay,
|
||||||
|
const Duration(seconds: 60),
|
||||||
|
newState,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final ServerInstallationNotFinished newState = dataState.copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
dnsMatches: matches,
|
||||||
|
);
|
||||||
|
emit(newState);
|
||||||
|
runDelayed(
|
||||||
|
startServerIfDnsIsOkay,
|
||||||
|
const Duration(seconds: 30),
|
||||||
|
newState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetServerIfServerIsOkay({
|
||||||
|
final ServerInstallationNotFinished? state,
|
||||||
|
}) async {
|
||||||
|
final ServerInstallationNotFinished dataState =
|
||||||
|
state ?? this.state as ServerInstallationNotFinished;
|
||||||
|
|
||||||
|
emit(TimerState(dataState: dataState, isLoading: true));
|
||||||
|
|
||||||
|
final bool isServerWorking = await repository.isHttpServerWorking();
|
||||||
|
|
||||||
|
if (isServerWorking) {
|
||||||
|
const Duration pauseDuration = Duration(seconds: 30);
|
||||||
|
emit(
|
||||||
|
TimerState(
|
||||||
|
dataState: dataState,
|
||||||
|
timerStart: DateTime.now(),
|
||||||
|
isLoading: false,
|
||||||
|
duration: pauseDuration,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
timer = Timer(pauseDuration, () async {
|
||||||
|
final ServerHostingDetails hetznerServerDetails =
|
||||||
|
await repository.restart();
|
||||||
|
await repository.saveIsServerResetedFirstTime(true);
|
||||||
|
await repository.saveServerDetails(hetznerServerDetails);
|
||||||
|
|
||||||
|
final ServerInstallationNotFinished newState = dataState.copyWith(
|
||||||
|
isServerResetedFirstTime: true,
|
||||||
|
serverDetails: hetznerServerDetails,
|
||||||
|
isLoading: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(newState);
|
||||||
|
runDelayed(oneMoreReset, const Duration(seconds: 60), newState);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
runDelayed(
|
||||||
|
resetServerIfServerIsOkay,
|
||||||
|
const Duration(seconds: 60),
|
||||||
|
dataState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void oneMoreReset({final ServerInstallationNotFinished? state}) async {
|
||||||
|
final ServerInstallationNotFinished dataState =
|
||||||
|
state ?? this.state as ServerInstallationNotFinished;
|
||||||
|
|
||||||
|
emit(TimerState(dataState: dataState, isLoading: true));
|
||||||
|
|
||||||
|
final bool isServerWorking = await repository.isHttpServerWorking();
|
||||||
|
|
||||||
|
if (isServerWorking) {
|
||||||
|
const Duration pauseDuration = Duration(seconds: 30);
|
||||||
|
emit(
|
||||||
|
TimerState(
|
||||||
|
dataState: dataState,
|
||||||
|
timerStart: DateTime.now(),
|
||||||
|
isLoading: false,
|
||||||
|
duration: pauseDuration,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
timer = Timer(pauseDuration, () async {
|
||||||
|
final ServerHostingDetails hetznerServerDetails =
|
||||||
|
await repository.restart();
|
||||||
|
await repository.saveIsServerResetedSecondTime(true);
|
||||||
|
await repository.saveServerDetails(hetznerServerDetails);
|
||||||
|
|
||||||
|
final ServerInstallationNotFinished newState = dataState.copyWith(
|
||||||
|
isServerResetedSecondTime: true,
|
||||||
|
serverDetails: hetznerServerDetails,
|
||||||
|
isLoading: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(newState);
|
||||||
|
runDelayed(
|
||||||
|
finishCheckIfServerIsOkay,
|
||||||
|
const Duration(seconds: 60),
|
||||||
|
newState,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
runDelayed(oneMoreReset, const Duration(seconds: 60), dataState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void finishCheckIfServerIsOkay({
|
||||||
|
final ServerInstallationNotFinished? state,
|
||||||
|
}) async {
|
||||||
|
final ServerInstallationNotFinished dataState =
|
||||||
|
state ?? this.state as ServerInstallationNotFinished;
|
||||||
|
|
||||||
|
emit(TimerState(dataState: dataState, isLoading: true));
|
||||||
|
|
||||||
|
final bool isServerWorking = await repository.isHttpServerWorking();
|
||||||
|
|
||||||
|
if (isServerWorking) {
|
||||||
|
await repository.createDkimRecord(dataState.serverDomain!);
|
||||||
|
await repository.saveHasFinalChecked(true);
|
||||||
|
|
||||||
|
emit(dataState.finish());
|
||||||
|
} else {
|
||||||
|
runDelayed(
|
||||||
|
finishCheckIfServerIsOkay,
|
||||||
|
const Duration(seconds: 60),
|
||||||
|
dataState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void runDelayed(
|
||||||
|
final void Function() work,
|
||||||
|
final Duration delay,
|
||||||
|
final ServerInstallationNotFinished? state,
|
||||||
|
) async {
|
||||||
|
final ServerInstallationNotFinished dataState =
|
||||||
|
state ?? this.state as ServerInstallationNotFinished;
|
||||||
|
|
||||||
|
emit(
|
||||||
|
TimerState(
|
||||||
|
dataState: dataState,
|
||||||
|
timerStart: DateTime.now(),
|
||||||
|
duration: delay,
|
||||||
|
isLoading: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
timer = Timer(delay, work);
|
||||||
|
}
|
||||||
|
|
||||||
|
void submitDomainForAccessRecovery(final String domain) async {
|
||||||
|
final ServerDomain serverDomain = ServerDomain(
|
||||||
|
domainName: domain,
|
||||||
|
provider: DnsProvider.unknown,
|
||||||
|
zoneId: '',
|
||||||
|
);
|
||||||
|
final ServerRecoveryCapabilities recoveryCapabilities =
|
||||||
|
await repository.getRecoveryCapabilities(serverDomain);
|
||||||
|
|
||||||
|
await repository.saveDomain(serverDomain);
|
||||||
|
await repository.saveIsRecoveringServer(true);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
ServerInstallationRecovery(
|
||||||
|
serverDomain: serverDomain,
|
||||||
|
recoveryCapabilities: recoveryCapabilities,
|
||||||
|
currentStep: RecoveryStep.selecting,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tryToRecover(
|
||||||
|
final String token,
|
||||||
|
final ServerRecoveryMethods method,
|
||||||
|
) async {
|
||||||
|
final ServerInstallationRecovery dataState =
|
||||||
|
state as ServerInstallationRecovery;
|
||||||
|
final ServerDomain? serverDomain = dataState.serverDomain;
|
||||||
|
if (serverDomain == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Future<ServerHostingDetails> Function(
|
||||||
|
ServerDomain,
|
||||||
|
String,
|
||||||
|
ServerRecoveryCapabilities,
|
||||||
|
) recoveryFunction;
|
||||||
|
switch (method) {
|
||||||
|
case ServerRecoveryMethods.newDeviceKey:
|
||||||
|
recoveryFunction = repository.authorizeByNewDeviceKey;
|
||||||
|
break;
|
||||||
|
case ServerRecoveryMethods.recoveryKey:
|
||||||
|
recoveryFunction = repository.authorizeByRecoveryKey;
|
||||||
|
break;
|
||||||
|
case ServerRecoveryMethods.oldToken:
|
||||||
|
recoveryFunction = repository.authorizeByApiToken;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw Exception('Unknown recovery method');
|
||||||
|
}
|
||||||
|
final ServerHostingDetails serverDetails = await recoveryFunction(
|
||||||
|
serverDomain,
|
||||||
|
token,
|
||||||
|
dataState.recoveryCapabilities,
|
||||||
|
);
|
||||||
|
await repository.saveServerDetails(serverDetails);
|
||||||
|
emit(
|
||||||
|
dataState.copyWith(
|
||||||
|
serverDetails: serverDetails,
|
||||||
|
currentStep: RecoveryStep.hetznerToken,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} on ServerAuthorizationException {
|
||||||
|
getIt<NavigationService>()
|
||||||
|
.showSnackBar('recovering.authorization_failed'.tr());
|
||||||
|
return;
|
||||||
|
} on IpNotFoundException {
|
||||||
|
getIt<NavigationService>()
|
||||||
|
.showSnackBar('recovering.domain_recover_error'.tr());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void revertRecoveryStep() {
|
||||||
|
if (state is ServerInstallationEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final ServerInstallationRecovery dataState =
|
||||||
|
state as ServerInstallationRecovery;
|
||||||
|
switch (dataState.currentStep) {
|
||||||
|
case RecoveryStep.selecting:
|
||||||
|
repository.deleteDomain();
|
||||||
|
emit(const ServerInstallationEmpty());
|
||||||
|
break;
|
||||||
|
case RecoveryStep.recoveryKey:
|
||||||
|
case RecoveryStep.newDeviceKey:
|
||||||
|
case RecoveryStep.oldToken:
|
||||||
|
emit(
|
||||||
|
dataState.copyWith(
|
||||||
|
currentStep: RecoveryStep.selecting,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case RecoveryStep.serverSelection:
|
||||||
|
repository.deleteHetznerKey();
|
||||||
|
emit(
|
||||||
|
dataState.copyWith(
|
||||||
|
currentStep: RecoveryStep.hetznerToken,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
// We won't revert steps after client is authorized
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectRecoveryMethod(final ServerRecoveryMethods method) {
|
||||||
|
final ServerInstallationRecovery dataState =
|
||||||
|
state as ServerInstallationRecovery;
|
||||||
|
switch (method) {
|
||||||
|
case ServerRecoveryMethods.newDeviceKey:
|
||||||
|
emit(
|
||||||
|
dataState.copyWith(
|
||||||
|
currentStep: RecoveryStep.newDeviceKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ServerRecoveryMethods.recoveryKey:
|
||||||
|
emit(
|
||||||
|
dataState.copyWith(
|
||||||
|
currentStep: RecoveryStep.recoveryKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ServerRecoveryMethods.oldToken:
|
||||||
|
emit(
|
||||||
|
dataState.copyWith(
|
||||||
|
currentStep: RecoveryStep.oldToken,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ServerBasicInfoWithValidators>>
|
||||||
|
getServersOnHetznerAccount() async {
|
||||||
|
final ServerInstallationRecovery dataState =
|
||||||
|
state as ServerInstallationRecovery;
|
||||||
|
final List<ServerBasicInfo> servers =
|
||||||
|
await repository.getServersOnHetznerAccount();
|
||||||
|
final Iterable<ServerBasicInfoWithValidators> validated = servers.map(
|
||||||
|
(final ServerBasicInfo server) =>
|
||||||
|
ServerBasicInfoWithValidators.fromServerBasicInfo(
|
||||||
|
serverBasicInfo: server,
|
||||||
|
isIpValid: server.ip == dataState.serverDetails?.ip4,
|
||||||
|
isReverseDnsValid:
|
||||||
|
server.reverseDns == dataState.serverDomain?.domainName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return validated.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setServerId(final ServerBasicInfo server) async {
|
||||||
|
final ServerInstallationRecovery dataState =
|
||||||
|
state as ServerInstallationRecovery;
|
||||||
|
final ServerDomain? serverDomain = dataState.serverDomain;
|
||||||
|
if (serverDomain == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final ServerHostingDetails serverDetails = ServerHostingDetails(
|
||||||
|
ip4: server.ip,
|
||||||
|
id: server.id,
|
||||||
|
createTime: server.created,
|
||||||
|
volume: ServerVolume(
|
||||||
|
id: server.volumeId,
|
||||||
|
name: 'recovered_volume',
|
||||||
|
),
|
||||||
|
apiToken: dataState.serverDetails!.apiToken,
|
||||||
|
provider: ServerProvider.hetzner,
|
||||||
|
);
|
||||||
|
await repository.saveDomain(serverDomain);
|
||||||
|
await repository.saveServerDetails(serverDetails);
|
||||||
|
emit(
|
||||||
|
dataState.copyWith(
|
||||||
|
serverDetails: serverDetails,
|
||||||
|
currentStep: RecoveryStep.cloudflareToken,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setAndValidateCloudflareToken(final String token) async {
|
||||||
|
final ServerInstallationRecovery dataState =
|
||||||
|
state as ServerInstallationRecovery;
|
||||||
|
final ServerDomain? serverDomain = dataState.serverDomain;
|
||||||
|
if (serverDomain == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String? zoneId =
|
||||||
|
await repository.getDomainId(token, serverDomain.domainName);
|
||||||
|
if (zoneId == null) {
|
||||||
|
getIt<NavigationService>()
|
||||||
|
.showSnackBar('recovering.domain_not_available_on_token'.tr());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await repository.saveDomain(
|
||||||
|
ServerDomain(
|
||||||
|
domainName: serverDomain.domainName,
|
||||||
|
zoneId: zoneId,
|
||||||
|
provider: DnsProvider.cloudflare,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await repository.saveCloudFlareKey(token);
|
||||||
|
emit(
|
||||||
|
dataState.copyWith(
|
||||||
|
serverDomain: ServerDomain(
|
||||||
|
domainName: serverDomain.domainName,
|
||||||
|
zoneId: zoneId,
|
||||||
|
provider: DnsProvider.cloudflare,
|
||||||
|
),
|
||||||
|
cloudFlareKey: token,
|
||||||
|
currentStep: RecoveryStep.backblazeToken,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void finishRecoveryProcess(
|
||||||
|
final BackblazeCredential backblazeCredential,
|
||||||
|
) async {
|
||||||
|
await repository.saveIsServerStarted(true);
|
||||||
|
await repository.saveIsServerResetedFirstTime(true);
|
||||||
|
await repository.saveIsServerResetedSecondTime(true);
|
||||||
|
await repository.saveHasFinalChecked(true);
|
||||||
|
await repository.saveIsRecoveringServer(false);
|
||||||
|
final User mainUser = await repository.getMainUser();
|
||||||
|
await repository.saveRootUser(mainUser);
|
||||||
|
final ServerInstallationRecovery updatedState =
|
||||||
|
(state as ServerInstallationRecovery).copyWith(
|
||||||
|
backblazeCredential: backblazeCredential,
|
||||||
|
rootUser: mainUser,
|
||||||
|
);
|
||||||
|
emit(updatedState.finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onChange(final Change<ServerInstallationState> change) {
|
||||||
|
super.onChange(change);
|
||||||
|
print('================================');
|
||||||
|
print('ServerInstallationState changed!');
|
||||||
|
print('Current type: ${change.nextState.runtimeType}');
|
||||||
|
print('Hetzner key: ${change.nextState.hetznerKey}');
|
||||||
|
print('Cloudflare key: ${change.nextState.cloudFlareKey}');
|
||||||
|
print('Domain: ${change.nextState.serverDomain}');
|
||||||
|
print('BackblazeCredential: ${change.nextState.backblazeCredential}');
|
||||||
|
if (change.nextState is ServerInstallationRecovery) {
|
||||||
|
print(
|
||||||
|
'Recovery Step: ${(change.nextState as ServerInstallationRecovery).currentStep}',
|
||||||
|
);
|
||||||
|
print(
|
||||||
|
'Recovery Capabilities: ${(change.nextState as ServerInstallationRecovery).recoveryCapabilities}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (change.nextState is TimerState) {
|
||||||
|
print('Timer: ${(change.nextState as TimerState).duration}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearAppConfig() {
|
||||||
|
closeTimer();
|
||||||
|
|
||||||
|
repository.clearAppConfig();
|
||||||
|
emit(const ServerInstallationEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> serverDelete() async {
|
||||||
|
closeTimer();
|
||||||
|
|
||||||
|
if (state.serverDetails != null) {
|
||||||
|
await repository.deleteServer(state.serverDomain!);
|
||||||
|
}
|
||||||
|
await repository.deleteServerRelatedRecords();
|
||||||
|
emit(
|
||||||
|
ServerInstallationNotFinished(
|
||||||
|
hetznerKey: state.hetznerKey,
|
||||||
|
serverDomain: state.serverDomain,
|
||||||
|
cloudFlareKey: state.cloudFlareKey,
|
||||||
|
backblazeCredential: state.backblazeCredential,
|
||||||
|
rootUser: state.rootUser,
|
||||||
|
serverDetails: null,
|
||||||
|
isServerStarted: false,
|
||||||
|
isServerResetedFirstTime: false,
|
||||||
|
isServerResetedSecondTime: false,
|
||||||
|
isLoading: false,
|
||||||
|
dnsMatches: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
closeTimer();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void closeTimer() {
|
||||||
|
if (timer != null && timer!.isActive) {
|
||||||
|
timer!.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,683 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:basic_utils/basic_utils.dart';
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:pub_semver/pub_semver.dart';
|
||||||
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
|
import 'package:selfprivacy/config/hive_config.dart';
|
||||||
|
import 'package:selfprivacy/logic/api_maps/cloudflare.dart';
|
||||||
|
import 'package:selfprivacy/logic/api_maps/hetzner.dart';
|
||||||
|
import 'package:selfprivacy/logic/api_maps/server.dart';
|
||||||
|
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/device_token.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/message.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/server_basic_info.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/action_button/action_button.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart';
|
||||||
|
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
|
|
||||||
|
class IpNotFoundException implements Exception {
|
||||||
|
IpNotFoundException(this.message);
|
||||||
|
final String message;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerAuthorizationException implements Exception {
|
||||||
|
ServerAuthorizationException(this.message);
|
||||||
|
final String message;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerInstallationRepository {
|
||||||
|
Box box = Hive.box(BNames.serverInstallationBox);
|
||||||
|
Box<User> usersBox = Hive.box(BNames.usersBox);
|
||||||
|
|
||||||
|
Future<ServerInstallationState> load() async {
|
||||||
|
final String? hetznerToken = getIt<ApiConfigModel>().hetznerKey;
|
||||||
|
final String? cloudflareToken = getIt<ApiConfigModel>().cloudFlareKey;
|
||||||
|
final ServerDomain? serverDomain = getIt<ApiConfigModel>().serverDomain;
|
||||||
|
final BackblazeCredential? backblazeCredential =
|
||||||
|
getIt<ApiConfigModel>().backblazeCredential;
|
||||||
|
final ServerHostingDetails? serverDetails =
|
||||||
|
getIt<ApiConfigModel>().serverDetails;
|
||||||
|
|
||||||
|
if (box.get(BNames.hasFinalChecked, defaultValue: false)) {
|
||||||
|
return ServerInstallationFinished(
|
||||||
|
hetznerKey: hetznerToken!,
|
||||||
|
cloudFlareKey: cloudflareToken!,
|
||||||
|
serverDomain: serverDomain!,
|
||||||
|
backblazeCredential: backblazeCredential!,
|
||||||
|
serverDetails: serverDetails!,
|
||||||
|
rootUser: box.get(BNames.rootUser),
|
||||||
|
isServerStarted: box.get(BNames.isServerStarted, defaultValue: false),
|
||||||
|
isServerResetedFirstTime:
|
||||||
|
box.get(BNames.isServerResetedFirstTime, defaultValue: false),
|
||||||
|
isServerResetedSecondTime:
|
||||||
|
box.get(BNames.isServerResetedSecondTime, defaultValue: false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (box.get(BNames.isRecoveringServer, defaultValue: false) &&
|
||||||
|
serverDomain != null) {
|
||||||
|
return ServerInstallationRecovery(
|
||||||
|
hetznerKey: hetznerToken,
|
||||||
|
cloudFlareKey: cloudflareToken,
|
||||||
|
serverDomain: serverDomain,
|
||||||
|
backblazeCredential: backblazeCredential,
|
||||||
|
serverDetails: serverDetails,
|
||||||
|
rootUser: box.get(BNames.rootUser),
|
||||||
|
currentStep: _getCurrentRecoveryStep(
|
||||||
|
hetznerToken,
|
||||||
|
cloudflareToken,
|
||||||
|
serverDomain,
|
||||||
|
serverDetails,
|
||||||
|
),
|
||||||
|
recoveryCapabilities: await getRecoveryCapabilities(serverDomain),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ServerInstallationNotFinished(
|
||||||
|
hetznerKey: hetznerToken,
|
||||||
|
cloudFlareKey: cloudflareToken,
|
||||||
|
serverDomain: serverDomain,
|
||||||
|
backblazeCredential: backblazeCredential,
|
||||||
|
serverDetails: serverDetails,
|
||||||
|
rootUser: box.get(BNames.rootUser),
|
||||||
|
isServerStarted: box.get(BNames.isServerStarted, defaultValue: false),
|
||||||
|
isServerResetedFirstTime:
|
||||||
|
box.get(BNames.isServerResetedFirstTime, defaultValue: false),
|
||||||
|
isServerResetedSecondTime:
|
||||||
|
box.get(BNames.isServerResetedSecondTime, defaultValue: false),
|
||||||
|
isLoading: box.get(BNames.isLoading, defaultValue: false),
|
||||||
|
dnsMatches: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
RecoveryStep _getCurrentRecoveryStep(
|
||||||
|
final String? hetznerToken,
|
||||||
|
final String? cloudflareToken,
|
||||||
|
final ServerDomain serverDomain,
|
||||||
|
final ServerHostingDetails? serverDetails,
|
||||||
|
) {
|
||||||
|
if (serverDetails != null) {
|
||||||
|
if (hetznerToken != null) {
|
||||||
|
if (serverDetails.provider != ServerProvider.unknown) {
|
||||||
|
if (serverDomain.provider != DnsProvider.unknown) {
|
||||||
|
return RecoveryStep.backblazeToken;
|
||||||
|
}
|
||||||
|
return RecoveryStep.cloudflareToken;
|
||||||
|
}
|
||||||
|
return RecoveryStep.serverSelection;
|
||||||
|
}
|
||||||
|
return RecoveryStep.hetznerToken;
|
||||||
|
}
|
||||||
|
return RecoveryStep.selecting;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearAppConfig() {
|
||||||
|
box.clear();
|
||||||
|
usersBox.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ServerHostingDetails> startServer(
|
||||||
|
final ServerHostingDetails hetznerServer,
|
||||||
|
) async {
|
||||||
|
final HetznerApi hetznerApi = HetznerApi();
|
||||||
|
final ServerHostingDetails serverDetails = await hetznerApi.powerOn();
|
||||||
|
|
||||||
|
return serverDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getDomainId(final String token, final String domain) async {
|
||||||
|
final CloudflareApi cloudflareApi = CloudflareApi(
|
||||||
|
isWithToken: false,
|
||||||
|
customToken: token,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final String domainId = await cloudflareApi.getZoneId(domain);
|
||||||
|
return domainId;
|
||||||
|
} on DomainNotFoundException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, bool>> isDnsAddressesMatch(
|
||||||
|
final String? domainName,
|
||||||
|
final String? ip4,
|
||||||
|
final Map<String, bool> skippedMatches,
|
||||||
|
) async {
|
||||||
|
final List<String> addresses = <String>[
|
||||||
|
'$domainName',
|
||||||
|
'api.$domainName',
|
||||||
|
'cloud.$domainName',
|
||||||
|
'meet.$domainName',
|
||||||
|
'password.$domainName'
|
||||||
|
];
|
||||||
|
|
||||||
|
final Map<String, bool> matches = <String, bool>{};
|
||||||
|
|
||||||
|
for (final String address in addresses) {
|
||||||
|
if (skippedMatches[address] ?? false) {
|
||||||
|
matches[address] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final List<RRecord>? lookupRecordRes = await DnsUtils.lookupRecord(
|
||||||
|
address,
|
||||||
|
RRecordType.A,
|
||||||
|
provider: DnsApiProvider.CLOUDFLARE,
|
||||||
|
);
|
||||||
|
getIt.get<ConsoleModel>().addMessage(
|
||||||
|
Message(
|
||||||
|
text:
|
||||||
|
'DnsLookup: address: $address, $RRecordType, provider: CLOUDFLARE, ip4: $ip4',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
getIt.get<ConsoleModel>().addMessage(
|
||||||
|
Message(
|
||||||
|
text:
|
||||||
|
'DnsLookup: ${lookupRecordRes == null ? 'empty' : (lookupRecordRes[0].data != ip4 ? 'wrong ip4' : 'right ip4')}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (lookupRecordRes == null ||
|
||||||
|
lookupRecordRes.isEmpty ||
|
||||||
|
lookupRecordRes[0].data != ip4) {
|
||||||
|
matches[address] = false;
|
||||||
|
} else {
|
||||||
|
matches[address] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createServer(
|
||||||
|
final User rootUser,
|
||||||
|
final String domainName,
|
||||||
|
final String cloudFlareKey,
|
||||||
|
final BackblazeCredential backblazeCredential, {
|
||||||
|
required final void Function() onCancel,
|
||||||
|
required final Future<void> Function(ServerHostingDetails serverDetails)
|
||||||
|
onSuccess,
|
||||||
|
}) async {
|
||||||
|
final HetznerApi hetznerApi = HetznerApi();
|
||||||
|
late ServerVolume dataBase;
|
||||||
|
|
||||||
|
try {
|
||||||
|
dataBase = await hetznerApi.createVolume();
|
||||||
|
|
||||||
|
final ServerHostingDetails? serverDetails = await hetznerApi.createServer(
|
||||||
|
cloudFlareKey: cloudFlareKey,
|
||||||
|
rootUser: rootUser,
|
||||||
|
domainName: domainName,
|
||||||
|
dataBase: dataBase,
|
||||||
|
);
|
||||||
|
if (serverDetails == null) {
|
||||||
|
print('Server is not initialized!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveServerDetails(serverDetails);
|
||||||
|
onSuccess(serverDetails);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
if (e.response!.data['error']['code'] == 'uniqueness_error') {
|
||||||
|
final NavigationService nav = getIt.get<NavigationService>();
|
||||||
|
nav.showPopUpDialog(
|
||||||
|
BrandAlert(
|
||||||
|
title: 'modals.1'.tr(),
|
||||||
|
contentText: 'modals.2'.tr(),
|
||||||
|
actions: [
|
||||||
|
ActionButton(
|
||||||
|
text: 'basis.delete'.tr(),
|
||||||
|
isRed: true,
|
||||||
|
onPressed: () async {
|
||||||
|
await hetznerApi.deleteSelfprivacyServerAndAllVolumes(
|
||||||
|
domainName: domainName,
|
||||||
|
);
|
||||||
|
|
||||||
|
final ServerHostingDetails? serverDetails =
|
||||||
|
await hetznerApi.createServer(
|
||||||
|
cloudFlareKey: cloudFlareKey,
|
||||||
|
rootUser: rootUser,
|
||||||
|
domainName: domainName,
|
||||||
|
dataBase: dataBase,
|
||||||
|
);
|
||||||
|
if (serverDetails == null) {
|
||||||
|
print('Server is not initialized!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await saveServerDetails(serverDetails);
|
||||||
|
onSuccess(serverDetails);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ActionButton(
|
||||||
|
text: 'basis.cancel'.tr(),
|
||||||
|
onPressed: onCancel,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createDnsRecords(
|
||||||
|
final String ip4,
|
||||||
|
final ServerDomain cloudFlareDomain, {
|
||||||
|
required final void Function() onCancel,
|
||||||
|
}) async {
|
||||||
|
final CloudflareApi cloudflareApi = CloudflareApi();
|
||||||
|
|
||||||
|
await cloudflareApi.removeSimilarRecords(
|
||||||
|
ip4: ip4,
|
||||||
|
cloudFlareDomain: cloudFlareDomain,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cloudflareApi.createMultipleDnsRecords(
|
||||||
|
ip4: ip4,
|
||||||
|
cloudFlareDomain: cloudFlareDomain,
|
||||||
|
);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
final HetznerApi hetznerApi = HetznerApi();
|
||||||
|
final NavigationService nav = getIt.get<NavigationService>();
|
||||||
|
nav.showPopUpDialog(
|
||||||
|
BrandAlert(
|
||||||
|
title: e.response!.data['errors'][0]['code'] == 1038
|
||||||
|
? 'modals.10'.tr()
|
||||||
|
: 'providers.domain.states.error'.tr(),
|
||||||
|
contentText: 'modals.6'.tr(),
|
||||||
|
actions: [
|
||||||
|
ActionButton(
|
||||||
|
text: 'basis.delete'.tr(),
|
||||||
|
isRed: true,
|
||||||
|
onPressed: () async {
|
||||||
|
await hetznerApi.deleteSelfprivacyServerAndAllVolumes(
|
||||||
|
domainName: cloudFlareDomain.domainName,
|
||||||
|
);
|
||||||
|
|
||||||
|
onCancel();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ActionButton(
|
||||||
|
text: 'basis.cancel'.tr(),
|
||||||
|
onPressed: onCancel,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await HetznerApi().createReverseDns(
|
||||||
|
ip4: ip4,
|
||||||
|
domainName: cloudFlareDomain.domainName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createDkimRecord(final ServerDomain cloudFlareDomain) async {
|
||||||
|
final CloudflareApi cloudflareApi = CloudflareApi();
|
||||||
|
final ServerApi api = ServerApi();
|
||||||
|
|
||||||
|
final String? dkimRecordString = await api.getDkim();
|
||||||
|
|
||||||
|
await cloudflareApi.setDkim(dkimRecordString ?? '', cloudFlareDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isHttpServerWorking() async {
|
||||||
|
final ServerApi api = ServerApi();
|
||||||
|
final bool isHttpServerWorking = await api.isHttpServerWorking();
|
||||||
|
try {
|
||||||
|
await api.getDkim();
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isHttpServerWorking;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ServerHostingDetails> restart() async {
|
||||||
|
final HetznerApi hetznerApi = HetznerApi();
|
||||||
|
return hetznerApi.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ServerHostingDetails> powerOn() async {
|
||||||
|
final HetznerApi hetznerApi = HetznerApi();
|
||||||
|
return hetznerApi.powerOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ServerRecoveryCapabilities> getRecoveryCapabilities(
|
||||||
|
final ServerDomain serverDomain,
|
||||||
|
) async {
|
||||||
|
final ServerApi serverApi = ServerApi(
|
||||||
|
isWithToken: false,
|
||||||
|
overrideDomain: serverDomain.domainName,
|
||||||
|
);
|
||||||
|
final String? serverApiVersion = await serverApi.getApiVersion();
|
||||||
|
if (serverApiVersion == null) {
|
||||||
|
return ServerRecoveryCapabilities.none;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final Version parsedVersion = Version.parse(serverApiVersion);
|
||||||
|
if (!VersionConstraint.parse('>=1.2.0').allows(parsedVersion)) {
|
||||||
|
return ServerRecoveryCapabilities.legacy;
|
||||||
|
}
|
||||||
|
return ServerRecoveryCapabilities.loginTokens;
|
||||||
|
} on FormatException {
|
||||||
|
return ServerRecoveryCapabilities.none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getServerIpFromDomain(final ServerDomain serverDomain) async {
|
||||||
|
final List<RRecord>? lookup = await DnsUtils.lookupRecord(
|
||||||
|
serverDomain.domainName,
|
||||||
|
RRecordType.A,
|
||||||
|
provider: DnsApiProvider.CLOUDFLARE,
|
||||||
|
);
|
||||||
|
if (lookup == null || lookup.isEmpty) {
|
||||||
|
throw IpNotFoundException('No IP found for domain $serverDomain');
|
||||||
|
}
|
||||||
|
return lookup[0].data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getDeviceName() async {
|
||||||
|
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||||
|
if (kIsWeb) {
|
||||||
|
return deviceInfo.webBrowserInfo.then(
|
||||||
|
(final WebBrowserInfo value) =>
|
||||||
|
'${value.browserName} ${value.platform}',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
return deviceInfo.androidInfo.then(
|
||||||
|
(final AndroidDeviceInfo value) =>
|
||||||
|
'${value.model} ${value.version.release}',
|
||||||
|
);
|
||||||
|
} else if (Platform.isIOS) {
|
||||||
|
return deviceInfo.iosInfo.then(
|
||||||
|
(final IosDeviceInfo value) =>
|
||||||
|
'${value.utsname.machine} ${value.systemName} ${value.systemVersion}',
|
||||||
|
);
|
||||||
|
} else if (Platform.isLinux) {
|
||||||
|
return deviceInfo.linuxInfo
|
||||||
|
.then((final LinuxDeviceInfo value) => value.prettyName);
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
return deviceInfo.macOsInfo.then(
|
||||||
|
(final MacOsDeviceInfo value) =>
|
||||||
|
'${value.hostName} ${value.computerName}',
|
||||||
|
);
|
||||||
|
} else if (Platform.isWindows) {
|
||||||
|
return deviceInfo.windowsInfo
|
||||||
|
.then((final WindowsDeviceInfo value) => value.computerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'Unidentified';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ServerHostingDetails> authorizeByNewDeviceKey(
|
||||||
|
final ServerDomain serverDomain,
|
||||||
|
final String newDeviceKey,
|
||||||
|
final ServerRecoveryCapabilities recoveryCapabilities,
|
||||||
|
) async {
|
||||||
|
final ServerApi serverApi = ServerApi(
|
||||||
|
isWithToken: false,
|
||||||
|
overrideDomain: serverDomain.domainName,
|
||||||
|
);
|
||||||
|
final String serverIp = await getServerIpFromDomain(serverDomain);
|
||||||
|
final ApiResponse<String> apiResponse = await serverApi.authorizeDevice(
|
||||||
|
DeviceToken(device: await getDeviceName(), token: newDeviceKey),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (apiResponse.isSuccess) {
|
||||||
|
return ServerHostingDetails(
|
||||||
|
apiToken: apiResponse.data,
|
||||||
|
volume: ServerVolume(
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
),
|
||||||
|
provider: ServerProvider.unknown,
|
||||||
|
id: 0,
|
||||||
|
ip4: serverIp,
|
||||||
|
startTime: null,
|
||||||
|
createTime: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ServerAuthorizationException(
|
||||||
|
apiResponse.errorMessage ?? apiResponse.data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ServerHostingDetails> authorizeByRecoveryKey(
|
||||||
|
final ServerDomain serverDomain,
|
||||||
|
final String recoveryKey,
|
||||||
|
final ServerRecoveryCapabilities recoveryCapabilities,
|
||||||
|
) async {
|
||||||
|
final ServerApi serverApi = ServerApi(
|
||||||
|
isWithToken: false,
|
||||||
|
overrideDomain: serverDomain.domainName,
|
||||||
|
);
|
||||||
|
final String serverIp = await getServerIpFromDomain(serverDomain);
|
||||||
|
final ApiResponse<String> apiResponse = await serverApi.useRecoveryToken(
|
||||||
|
DeviceToken(device: await getDeviceName(), token: recoveryKey),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (apiResponse.isSuccess) {
|
||||||
|
return ServerHostingDetails(
|
||||||
|
apiToken: apiResponse.data,
|
||||||
|
volume: ServerVolume(
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
),
|
||||||
|
provider: ServerProvider.unknown,
|
||||||
|
id: 0,
|
||||||
|
ip4: serverIp,
|
||||||
|
startTime: null,
|
||||||
|
createTime: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ServerAuthorizationException(
|
||||||
|
apiResponse.errorMessage ?? apiResponse.data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ServerHostingDetails> authorizeByApiToken(
|
||||||
|
final ServerDomain serverDomain,
|
||||||
|
final String apiToken,
|
||||||
|
final ServerRecoveryCapabilities recoveryCapabilities,
|
||||||
|
) async {
|
||||||
|
final ServerApi serverApi = ServerApi(
|
||||||
|
isWithToken: false,
|
||||||
|
overrideDomain: serverDomain.domainName,
|
||||||
|
customToken: apiToken,
|
||||||
|
);
|
||||||
|
final String serverIp = await getServerIpFromDomain(serverDomain);
|
||||||
|
if (recoveryCapabilities == ServerRecoveryCapabilities.legacy) {
|
||||||
|
final Map<ServiceTypes, bool> apiResponse =
|
||||||
|
await serverApi.servicesPowerCheck();
|
||||||
|
if (apiResponse.isNotEmpty) {
|
||||||
|
return ServerHostingDetails(
|
||||||
|
apiToken: apiToken,
|
||||||
|
volume: ServerVolume(
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
),
|
||||||
|
provider: ServerProvider.unknown,
|
||||||
|
id: 0,
|
||||||
|
ip4: serverIp,
|
||||||
|
startTime: null,
|
||||||
|
createTime: null,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw ServerAuthorizationException(
|
||||||
|
"Couldn't connect to server with this token",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final ApiResponse<String> deviceAuthKey =
|
||||||
|
await serverApi.createDeviceToken();
|
||||||
|
final ApiResponse<String> apiResponse = await serverApi.authorizeDevice(
|
||||||
|
DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (apiResponse.isSuccess) {
|
||||||
|
return ServerHostingDetails(
|
||||||
|
apiToken: apiResponse.data,
|
||||||
|
volume: ServerVolume(
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
),
|
||||||
|
provider: ServerProvider.unknown,
|
||||||
|
id: 0,
|
||||||
|
ip4: serverIp,
|
||||||
|
startTime: null,
|
||||||
|
createTime: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ServerAuthorizationException(
|
||||||
|
apiResponse.errorMessage ?? apiResponse.data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<User> getMainUser() async {
|
||||||
|
final ServerApi serverApi = ServerApi();
|
||||||
|
const User fallbackUser = User(
|
||||||
|
isFoundOnServer: false,
|
||||||
|
note: "Couldn't find main user on server, API is outdated",
|
||||||
|
login: 'UNKNOWN',
|
||||||
|
sshKeys: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
final String? serverApiVersion = await serverApi.getApiVersion();
|
||||||
|
final ApiResponse<List<String>> users =
|
||||||
|
await serverApi.getUsersList(withMainUser: true);
|
||||||
|
if (serverApiVersion == null || !users.isSuccess) {
|
||||||
|
return fallbackUser;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final Version parsedVersion = Version.parse(serverApiVersion);
|
||||||
|
if (!VersionConstraint.parse('>=1.2.5').allows(parsedVersion)) {
|
||||||
|
return fallbackUser;
|
||||||
|
}
|
||||||
|
return User(
|
||||||
|
isFoundOnServer: true,
|
||||||
|
login: users.data[0],
|
||||||
|
);
|
||||||
|
} on FormatException {
|
||||||
|
return fallbackUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ServerBasicInfo>> getServersOnHetznerAccount() async {
|
||||||
|
final HetznerApi hetznerApi = HetznerApi();
|
||||||
|
final List<HetznerServerInfo> servers = await hetznerApi.getServers();
|
||||||
|
return servers
|
||||||
|
.map(
|
||||||
|
(final HetznerServerInfo server) => ServerBasicInfo(
|
||||||
|
id: server.id,
|
||||||
|
name: server.name,
|
||||||
|
ip: server.publicNet.ipv4.ip,
|
||||||
|
reverseDns: server.publicNet.ipv4.reverseDns,
|
||||||
|
created: server.created,
|
||||||
|
volumeId: server.volumes.isNotEmpty ? server.volumes[0] : 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveServerDetails(
|
||||||
|
final ServerHostingDetails serverDetails,
|
||||||
|
) async {
|
||||||
|
await getIt<ApiConfigModel>().storeServerDetails(serverDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveHetznerKey(final String key) async {
|
||||||
|
print('saved');
|
||||||
|
await getIt<ApiConfigModel>().storeHetznerKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteHetznerKey() async {
|
||||||
|
await box.delete(BNames.hetznerKey);
|
||||||
|
getIt<ApiConfigModel>().init();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveBackblazeKey(
|
||||||
|
final BackblazeCredential backblazeCredential,
|
||||||
|
) async {
|
||||||
|
await getIt<ApiConfigModel>().storeBackblazeCredential(backblazeCredential);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveCloudFlareKey(final String key) async {
|
||||||
|
await getIt<ApiConfigModel>().storeCloudFlareKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveDomain(final ServerDomain serverDomain) async {
|
||||||
|
await getIt<ApiConfigModel>().storeServerDomain(serverDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteDomain() async {
|
||||||
|
await box.delete(BNames.serverDomain);
|
||||||
|
getIt<ApiConfigModel>().init();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveIsServerStarted(final bool value) async {
|
||||||
|
await box.put(BNames.isServerStarted, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveIsServerResetedFirstTime(final bool value) async {
|
||||||
|
await box.put(BNames.isServerResetedFirstTime, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveIsServerResetedSecondTime(final bool value) async {
|
||||||
|
await box.put(BNames.isServerResetedSecondTime, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveRootUser(final User rootUser) async {
|
||||||
|
await box.put(BNames.rootUser, rootUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveIsRecoveringServer(final bool value) async {
|
||||||
|
await box.put(BNames.isRecoveringServer, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveHasFinalChecked(final bool value) async {
|
||||||
|
await box.put(BNames.hasFinalChecked, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteServer(final ServerDomain serverDomain) async {
|
||||||
|
final HetznerApi hetznerApi = HetznerApi();
|
||||||
|
final CloudflareApi cloudFlare = CloudflareApi();
|
||||||
|
|
||||||
|
await hetznerApi.deleteSelfprivacyServerAndAllVolumes(
|
||||||
|
domainName: serverDomain.domainName,
|
||||||
|
);
|
||||||
|
|
||||||
|
await box.put(BNames.hasFinalChecked, false);
|
||||||
|
await box.put(BNames.isServerStarted, false);
|
||||||
|
await box.put(BNames.isServerResetedFirstTime, false);
|
||||||
|
await box.put(BNames.isServerResetedSecondTime, false);
|
||||||
|
await box.put(BNames.isLoading, false);
|
||||||
|
await box.put(BNames.serverDetails, null);
|
||||||
|
|
||||||
|
await cloudFlare.removeSimilarRecords(cloudFlareDomain: serverDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteServerRelatedRecords() async {
|
||||||
|
await box.deleteAll([
|
||||||
|
BNames.serverDetails,
|
||||||
|
BNames.isServerStarted,
|
||||||
|
BNames.isServerResetedFirstTime,
|
||||||
|
BNames.isServerResetedSecondTime,
|
||||||
|
BNames.hasFinalChecked,
|
||||||
|
BNames.isLoading,
|
||||||
|
]);
|
||||||
|
getIt<ApiConfigModel>().init();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,322 @@
|
||||||
|
part of '../server_installation/server_installation_cubit.dart';
|
||||||
|
|
||||||
|
abstract class ServerInstallationState extends Equatable {
|
||||||
|
const ServerInstallationState({
|
||||||
|
required this.hetznerKey,
|
||||||
|
required this.cloudFlareKey,
|
||||||
|
required this.backblazeCredential,
|
||||||
|
required this.serverDomain,
|
||||||
|
required this.rootUser,
|
||||||
|
required this.serverDetails,
|
||||||
|
required this.isServerStarted,
|
||||||
|
required this.isServerResetedFirstTime,
|
||||||
|
required this.isServerResetedSecondTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
hetznerKey,
|
||||||
|
cloudFlareKey,
|
||||||
|
backblazeCredential,
|
||||||
|
serverDomain,
|
||||||
|
rootUser,
|
||||||
|
serverDetails,
|
||||||
|
isServerStarted,
|
||||||
|
isServerResetedFirstTime,
|
||||||
|
];
|
||||||
|
|
||||||
|
final String? hetznerKey;
|
||||||
|
final String? cloudFlareKey;
|
||||||
|
final BackblazeCredential? backblazeCredential;
|
||||||
|
final ServerDomain? serverDomain;
|
||||||
|
final User? rootUser;
|
||||||
|
final ServerHostingDetails? serverDetails;
|
||||||
|
final bool isServerStarted;
|
||||||
|
final bool isServerResetedFirstTime;
|
||||||
|
final bool isServerResetedSecondTime;
|
||||||
|
|
||||||
|
bool get isHetznerFilled => hetznerKey != null;
|
||||||
|
bool get isCloudFlareFilled => cloudFlareKey != null;
|
||||||
|
bool get isBackblazeFilled => backblazeCredential != null;
|
||||||
|
bool get isDomainFilled => serverDomain != null;
|
||||||
|
bool get isUserFilled => rootUser != null;
|
||||||
|
bool get isServerCreated => serverDetails != null;
|
||||||
|
|
||||||
|
bool get isFullyInitilized => _fulfilementList.every((final el) => el!);
|
||||||
|
ServerSetupProgress get progress => ServerSetupProgress
|
||||||
|
.values[_fulfilementList.where((final el) => el!).length];
|
||||||
|
|
||||||
|
int get porgressBar {
|
||||||
|
if (progress.index < 6) {
|
||||||
|
return progress.index;
|
||||||
|
} else if (progress.index < 10) {
|
||||||
|
return 6;
|
||||||
|
} else {
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<bool?> get _fulfilementList {
|
||||||
|
final List<bool> res = [
|
||||||
|
isHetznerFilled,
|
||||||
|
isCloudFlareFilled,
|
||||||
|
isBackblazeFilled,
|
||||||
|
isDomainFilled,
|
||||||
|
isUserFilled,
|
||||||
|
isServerCreated,
|
||||||
|
isServerStarted,
|
||||||
|
isServerResetedFirstTime,
|
||||||
|
isServerResetedSecondTime,
|
||||||
|
];
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimerState extends ServerInstallationNotFinished {
|
||||||
|
TimerState({
|
||||||
|
required this.dataState,
|
||||||
|
required final super.isLoading,
|
||||||
|
this.timerStart,
|
||||||
|
this.duration,
|
||||||
|
}) : super(
|
||||||
|
hetznerKey: dataState.hetznerKey,
|
||||||
|
cloudFlareKey: dataState.cloudFlareKey,
|
||||||
|
backblazeCredential: dataState.backblazeCredential,
|
||||||
|
serverDomain: dataState.serverDomain,
|
||||||
|
rootUser: dataState.rootUser,
|
||||||
|
serverDetails: dataState.serverDetails,
|
||||||
|
isServerStarted: dataState.isServerStarted,
|
||||||
|
isServerResetedFirstTime: dataState.isServerResetedFirstTime,
|
||||||
|
isServerResetedSecondTime: dataState.isServerResetedSecondTime,
|
||||||
|
dnsMatches: dataState.dnsMatches,
|
||||||
|
);
|
||||||
|
|
||||||
|
final ServerInstallationNotFinished dataState;
|
||||||
|
final DateTime? timerStart;
|
||||||
|
final Duration? duration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
dataState,
|
||||||
|
timerStart,
|
||||||
|
duration,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ServerSetupProgress {
|
||||||
|
nothingYet,
|
||||||
|
hetznerFilled,
|
||||||
|
cloudFlareFilled,
|
||||||
|
backblazeFilled,
|
||||||
|
domainFilled,
|
||||||
|
userFilled,
|
||||||
|
serverCreated,
|
||||||
|
serverStarted,
|
||||||
|
serverResetedFirstTime,
|
||||||
|
serverResetedSecondTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerInstallationNotFinished extends ServerInstallationState {
|
||||||
|
const ServerInstallationNotFinished({
|
||||||
|
required final super.isServerStarted,
|
||||||
|
required final super.isServerResetedFirstTime,
|
||||||
|
required final super.isServerResetedSecondTime,
|
||||||
|
required final this.isLoading,
|
||||||
|
required this.dnsMatches,
|
||||||
|
final super.hetznerKey,
|
||||||
|
final super.cloudFlareKey,
|
||||||
|
final super.backblazeCredential,
|
||||||
|
final super.serverDomain,
|
||||||
|
final super.rootUser,
|
||||||
|
final super.serverDetails,
|
||||||
|
});
|
||||||
|
final bool isLoading;
|
||||||
|
final Map<String, bool>? dnsMatches;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
hetznerKey,
|
||||||
|
cloudFlareKey,
|
||||||
|
backblazeCredential,
|
||||||
|
serverDomain,
|
||||||
|
rootUser,
|
||||||
|
serverDetails,
|
||||||
|
isServerStarted,
|
||||||
|
isServerResetedFirstTime,
|
||||||
|
isLoading,
|
||||||
|
dnsMatches,
|
||||||
|
];
|
||||||
|
|
||||||
|
ServerInstallationNotFinished copyWith({
|
||||||
|
final String? hetznerKey,
|
||||||
|
final String? cloudFlareKey,
|
||||||
|
final BackblazeCredential? backblazeCredential,
|
||||||
|
final ServerDomain? serverDomain,
|
||||||
|
final User? rootUser,
|
||||||
|
final ServerHostingDetails? serverDetails,
|
||||||
|
final bool? isServerStarted,
|
||||||
|
final bool? isServerResetedFirstTime,
|
||||||
|
final bool? isServerResetedSecondTime,
|
||||||
|
final bool? isLoading,
|
||||||
|
final Map<String, bool>? dnsMatches,
|
||||||
|
}) =>
|
||||||
|
ServerInstallationNotFinished(
|
||||||
|
hetznerKey: hetznerKey ?? this.hetznerKey,
|
||||||
|
cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey,
|
||||||
|
backblazeCredential: backblazeCredential ?? this.backblazeCredential,
|
||||||
|
serverDomain: serverDomain ?? this.serverDomain,
|
||||||
|
rootUser: rootUser ?? this.rootUser,
|
||||||
|
serverDetails: serverDetails ?? this.serverDetails,
|
||||||
|
isServerStarted: isServerStarted ?? this.isServerStarted,
|
||||||
|
isServerResetedFirstTime:
|
||||||
|
isServerResetedFirstTime ?? this.isServerResetedFirstTime,
|
||||||
|
isServerResetedSecondTime:
|
||||||
|
isServerResetedSecondTime ?? this.isServerResetedSecondTime,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
dnsMatches: dnsMatches ?? this.dnsMatches,
|
||||||
|
);
|
||||||
|
|
||||||
|
ServerInstallationFinished finish() => ServerInstallationFinished(
|
||||||
|
hetznerKey: hetznerKey!,
|
||||||
|
cloudFlareKey: cloudFlareKey!,
|
||||||
|
backblazeCredential: backblazeCredential!,
|
||||||
|
serverDomain: serverDomain!,
|
||||||
|
rootUser: rootUser!,
|
||||||
|
serverDetails: serverDetails!,
|
||||||
|
isServerStarted: isServerStarted,
|
||||||
|
isServerResetedFirstTime: isServerResetedFirstTime,
|
||||||
|
isServerResetedSecondTime: isServerResetedSecondTime,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerInstallationEmpty extends ServerInstallationNotFinished {
|
||||||
|
const ServerInstallationEmpty()
|
||||||
|
: super(
|
||||||
|
hetznerKey: null,
|
||||||
|
cloudFlareKey: null,
|
||||||
|
backblazeCredential: null,
|
||||||
|
serverDomain: null,
|
||||||
|
rootUser: null,
|
||||||
|
serverDetails: null,
|
||||||
|
isServerStarted: false,
|
||||||
|
isServerResetedFirstTime: false,
|
||||||
|
isServerResetedSecondTime: false,
|
||||||
|
isLoading: false,
|
||||||
|
dnsMatches: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerInstallationFinished extends ServerInstallationState {
|
||||||
|
const ServerInstallationFinished({
|
||||||
|
required final String super.hetznerKey,
|
||||||
|
required final String super.cloudFlareKey,
|
||||||
|
required final BackblazeCredential super.backblazeCredential,
|
||||||
|
required final ServerDomain super.serverDomain,
|
||||||
|
required final User super.rootUser,
|
||||||
|
required final ServerHostingDetails super.serverDetails,
|
||||||
|
required final super.isServerStarted,
|
||||||
|
required final super.isServerResetedFirstTime,
|
||||||
|
required final super.isServerResetedSecondTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
hetznerKey,
|
||||||
|
cloudFlareKey,
|
||||||
|
backblazeCredential,
|
||||||
|
serverDomain,
|
||||||
|
rootUser,
|
||||||
|
serverDetails,
|
||||||
|
isServerStarted,
|
||||||
|
isServerResetedFirstTime,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RecoveryStep {
|
||||||
|
selecting,
|
||||||
|
recoveryKey,
|
||||||
|
newDeviceKey,
|
||||||
|
oldToken,
|
||||||
|
hetznerToken,
|
||||||
|
serverSelection,
|
||||||
|
cloudflareToken,
|
||||||
|
backblazeToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ServerRecoveryCapabilities {
|
||||||
|
none,
|
||||||
|
legacy,
|
||||||
|
loginTokens,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ServerRecoveryMethods {
|
||||||
|
newDeviceKey,
|
||||||
|
recoveryKey,
|
||||||
|
oldToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerInstallationRecovery extends ServerInstallationState {
|
||||||
|
const ServerInstallationRecovery({
|
||||||
|
required this.currentStep,
|
||||||
|
required this.recoveryCapabilities,
|
||||||
|
final super.hetznerKey,
|
||||||
|
final super.cloudFlareKey,
|
||||||
|
final super.backblazeCredential,
|
||||||
|
final super.serverDomain,
|
||||||
|
final super.rootUser,
|
||||||
|
final super.serverDetails,
|
||||||
|
}) : super(
|
||||||
|
isServerStarted: true,
|
||||||
|
isServerResetedFirstTime: true,
|
||||||
|
isServerResetedSecondTime: true,
|
||||||
|
);
|
||||||
|
final RecoveryStep currentStep;
|
||||||
|
final ServerRecoveryCapabilities recoveryCapabilities;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
hetznerKey,
|
||||||
|
cloudFlareKey,
|
||||||
|
backblazeCredential,
|
||||||
|
serverDomain,
|
||||||
|
rootUser,
|
||||||
|
serverDetails,
|
||||||
|
isServerStarted,
|
||||||
|
isServerResetedFirstTime,
|
||||||
|
currentStep
|
||||||
|
];
|
||||||
|
|
||||||
|
ServerInstallationRecovery copyWith({
|
||||||
|
final String? hetznerKey,
|
||||||
|
final String? cloudFlareKey,
|
||||||
|
final BackblazeCredential? backblazeCredential,
|
||||||
|
final ServerDomain? serverDomain,
|
||||||
|
final User? rootUser,
|
||||||
|
final ServerHostingDetails? serverDetails,
|
||||||
|
final RecoveryStep? currentStep,
|
||||||
|
final ServerRecoveryCapabilities? recoveryCapabilities,
|
||||||
|
}) =>
|
||||||
|
ServerInstallationRecovery(
|
||||||
|
hetznerKey: hetznerKey ?? this.hetznerKey,
|
||||||
|
cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey,
|
||||||
|
backblazeCredential: backblazeCredential ?? this.backblazeCredential,
|
||||||
|
serverDomain: serverDomain ?? this.serverDomain,
|
||||||
|
rootUser: rootUser ?? this.rootUser,
|
||||||
|
serverDetails: serverDetails ?? this.serverDetails,
|
||||||
|
currentStep: currentStep ?? this.currentStep,
|
||||||
|
recoveryCapabilities: recoveryCapabilities ?? this.recoveryCapabilities,
|
||||||
|
);
|
||||||
|
|
||||||
|
ServerInstallationFinished finish() => ServerInstallationFinished(
|
||||||
|
hetznerKey: hetznerKey!,
|
||||||
|
cloudFlareKey: cloudFlareKey!,
|
||||||
|
backblazeCredential: backblazeCredential!,
|
||||||
|
serverDomain: serverDomain!,
|
||||||
|
rootUser: rootUser!,
|
||||||
|
serverDetails: serverDetails!,
|
||||||
|
isServerStarted: true,
|
||||||
|
isServerResetedFirstTime: true,
|
||||||
|
isServerResetedSecondTime: true,
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,21 +1,17 @@
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:selfprivacy/config/hive_config.dart';
|
|
||||||
import 'package:selfprivacy/logic/api_maps/server.dart';
|
import 'package:selfprivacy/logic/api_maps/server.dart';
|
||||||
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
|
||||||
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||||
|
|
||||||
part 'services_state.dart';
|
part 'services_state.dart';
|
||||||
|
|
||||||
class ServicesCubit extends AppConfigDependendCubit<ServicesState> {
|
class ServicesCubit extends ServerInstallationDependendCubit<ServicesState> {
|
||||||
ServicesCubit(AppConfigCubit appConfigCubit)
|
ServicesCubit(final ServerInstallationCubit serverInstallationCubit)
|
||||||
: super(appConfigCubit, ServicesState.allOff());
|
: super(serverInstallationCubit, ServicesState.allOff());
|
||||||
|
final ServerApi api = ServerApi();
|
||||||
Box box = Hive.box(BNames.servicesState);
|
@override
|
||||||
final api = ServerApi();
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
if (appConfigCubit.state is AppConfigFinished) {
|
if (serverInstallationCubit.state is ServerInstallationFinished) {
|
||||||
var statuses = await api.servicesPowerCheck();
|
final Map<ServiceTypes, bool> statuses = await api.servicesPowerCheck();
|
||||||
emit(
|
emit(
|
||||||
ServicesState(
|
ServicesState(
|
||||||
isPasswordManagerEnable: statuses[ServiceTypes.passwordManager]!,
|
isPasswordManagerEnable: statuses[ServiceTypes.passwordManager]!,
|
||||||
|
@ -30,7 +26,6 @@ class ServicesCubit extends AppConfigDependendCubit<ServicesState> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void clear() async {
|
void clear() async {
|
||||||
box.clear();
|
|
||||||
emit(ServicesState.allOff());
|
emit(ServicesState.allOff());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|