diff --git a/.metadata b/.metadata
index f0274b3e..4903dbf5 100644
--- a/.metadata
+++ b/.metadata
@@ -1,10 +1,33 @@
# This file tracks properties of this Flutter project.
# 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:
- revision: 1aafb3a8b9b0c36241c5f5b34ee914770f015818
- channel: stable
+ revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a
+ channel: beta
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'
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 00000000..9d16cb20
--- /dev/null
+++ b/analysis_options.yaml
@@ -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
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 1b726e4e..0963724e 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -26,12 +26,22 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
- compileSdkVersion 31
+ compileSdkVersion flutter.compileSdkVersion
+ ndkVersion flutter.ndkVersion
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
lintOptions {
disable 'InvalidPackage'
}
@@ -39,7 +49,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "pro.kherel.selfprivacy"
- minSdkVersion 18
+ minSdkVersion 21
targetSdkVersion 31
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 00000000..f74085f3
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 00000000..3db14bb5
--- /dev/null
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/build.gradle b/android/build.gradle
index b3afb285..31e95773 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,12 +1,12 @@
buildscript {
- ext.kotlin_version = '1.5.10'
+ ext.kotlin_version = '1.6.10'
repositories {
google()
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.1.0'
+ classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index f8865307..cc5527d7 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
diff --git a/assets/markdown/how_fallback_old-en.md b/assets/markdown/how_fallback_old-en.md
new file mode 100644
index 00000000..c12504e7
--- /dev/null
+++ b/assets/markdown/how_fallback_old-en.md
@@ -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*.
diff --git a/assets/markdown/how_fallback_old-ru.md b/assets/markdown/how_fallback_old-ru.md
new file mode 100644
index 00000000..1d0a43f7
--- /dev/null
+++ b/assets/markdown/how_fallback_old-ru.md
@@ -0,0 +1,3 @@
+Введите в следующем окне токен, полученный из консоли прошлой версии приложения.
+
+Вводить нужно без слова *Bearer*.
diff --git a/assets/markdown/how_fallback_ssh-en.md b/assets/markdown/how_fallback_ssh-en.md
new file mode 100644
index 00000000..ce90e76a
--- /dev/null
+++ b/assets/markdown/how_fallback_ssh-en.md
@@ -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.
diff --git a/assets/markdown/how_fallback_ssh-ru.md b/assets/markdown/how_fallback_ssh-ru.md
new file mode 100644
index 00000000..a1737f08
--- /dev/null
+++ b/assets/markdown/how_fallback_ssh-ru.md
@@ -0,0 +1,19 @@
+Войдите как root пользователь на свой сервер и посмотрите содержимое файла `/etc/nixos/userdata/tokens.json`
+
+```sh
+cat /etc/nixos/userdata/tokens.json
+```
+
+В этом файле будет схожая конструкция:
+
+```json
+{
+ "tokens": [
+ {
+ "token": "токен_который_надо_скопировать",
+ "name": "имя_устройства",
+ "date": "дата"
+ }
+```
+
+Скопируйте токен из файла и вставьте в следующем окне.
diff --git a/assets/markdown/how_fallback_terminal-en.md b/assets/markdown/how_fallback_terminal-en.md
new file mode 100644
index 00000000..77c97efa
--- /dev/null
+++ b/assets/markdown/how_fallback_terminal-en.md
@@ -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.
diff --git a/assets/markdown/how_fallback_terminal-ru.md b/assets/markdown/how_fallback_terminal-ru.md
new file mode 100644
index 00000000..6681191e
--- /dev/null
+++ b/assets/markdown/how_fallback_terminal-ru.md
@@ -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": "дата"
+ }
+```
+
+Скопируйте токен из файла и вставьте в следующем окне.
diff --git a/assets/translations/en.json b/assets/translations/en.json
index 73c3575e..46cee9b8 100644
--- a/assets/translations/en.json
+++ b/assets/translations/en.json
@@ -21,13 +21,15 @@
"saving": "Saving..",
"nickname": "Nickname",
"loading": "Loading...",
- "later": "I will setup it later",
+ "later": "Skip to setup later",
+ "connect_to_existing": "Connect to an existing server",
"reset": "Reset",
"details": "Details",
"no_data": "No data",
"wait": "Wait",
"remove": "Remove",
- "apply": "Apply"
+ "apply": "Apply",
+ "done": "Done"
},
"more": {
"_comment": "'More' tab",
@@ -35,7 +37,7 @@
"about_project": "About us",
"about_app": "About application",
"onboarding": "Onboarding",
- "create_ssh_key": "Create ssh key",
+ "create_ssh_key": "Create SSH key",
"generate_key": "Generate key",
"generate_key_text": "You can generate ssh key",
"console": "Console",
@@ -255,7 +257,7 @@
"initializing": {
"_comment": "initializing page",
"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",
"3": "Connect CloudFlare",
"4": "To manage your domain's DNS",
@@ -273,7 +275,6 @@
"15": "Server created. DNS checks and server boot in progress...",
"16": "Until the next check: ",
"17": "Check",
- "18": "How to obtain Hetzner API Token",
"19": "1 Go via this link ",
"20": "\n",
"21": "One more restart to apply your security certificates.",
@@ -282,6 +283,94 @@
"finish": "Everything is initialized",
"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": {
"_comment": "messages in modals",
"1": "Server with such name, already exist",
@@ -293,7 +382,9 @@
"7": "Yes",
"8": "Remove task",
"9": "Reboot",
- "yes": "Yes"
+ "10": "You cannot use this API for domains with such TLD.",
+ "yes": "Yes",
+ "no": "No"
},
"timer": {
"sec": "{} sec"
diff --git a/assets/translations/ru.json b/assets/translations/ru.json
index ee085ed4..68665e26 100644
--- a/assets/translations/ru.json
+++ b/assets/translations/ru.json
@@ -21,13 +21,15 @@
"saving": "Сохранение…",
"nickname": "Никнейм",
"loading": "Загрузка",
- "later": "Настрою потом",
+ "later": "Пропустить и настроить потом",
+ "connect_to_existing": "Подключиться к существующему серверу",
"reset": "Сбросить",
"details": "Детальная информация",
"no_data": "Нет данных",
"wait": "Загрузка",
"remove": "Удалить",
- "apply": "Подать"
+ "apply": "Подать",
+ "done": "Готово"
},
"more": {
"_comment": "вкладка ещё",
@@ -283,6 +285,90 @@
"finish": "Всё инициализировано.",
"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": {
"_comment": "messages in modals",
"1": "Сервер с таким именем уже существует",
@@ -294,7 +380,9 @@
"7": "Да, удалить",
"8": "Удалить задачу",
"9": "Перезагрузить",
- "yes": "Да"
+ "10": "API не поддерживает домены с таким TLD.",
+ "yes": "Да",
+ "no": "Нет"
},
"timer": {
"sec": "{} сек"
diff --git a/fastlane/metadata/android/en-US/changelogs/0.6.0.txt b/fastlane/metadata/android/en-US/changelogs/0.6.0.txt
new file mode 100644
index 00000000..b8e98f1c
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/0.6.0.txt
@@ -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.
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
index 7b06195e..3ffd726f 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
index 351cf4e5..6886e9b1 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
index bcdbf2e9..e2024cbd 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
index 0526ab1b..3d3b2b37 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ
diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart
index d45144a5..3946d3b9 100644
--- a/lib/config/bloc_config.dart
+++ b/lib/config/bloc_config.dart
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.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/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart';
@@ -10,34 +12,56 @@ import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
class BlocAndProviderConfig extends StatelessWidget {
- const BlocAndProviderConfig({Key? key, this.child}) : super(key: key);
+ const BlocAndProviderConfig({final super.key, this.child});
final Widget? child;
@override
- Widget build(BuildContext context) {
- var isDark = false;
- var appConfigCubit = AppConfigCubit()..load();
- var usersCubit = UsersCubit(appConfigCubit);
- var servicesCubit = ServicesCubit(appConfigCubit);
- var backupsCubit = BackupsCubit(appConfigCubit);
- var dnsRecordsCubit = DnsRecordsCubit(appConfigCubit);
+ Widget build(final BuildContext context) {
+ const isDark = false;
+ final serverInstallationCubit = ServerInstallationCubit()..load();
+ final usersCubit = UsersCubit(serverInstallationCubit);
+ final servicesCubit = ServicesCubit(serverInstallationCubit);
+ final backupsCubit = BackupsCubit(serverInstallationCubit);
+ final dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit);
+ final recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit);
+ final apiDevicesCubit = ApiDevicesCubit(serverInstallationCubit);
return MultiProvider(
providers: [
BlocProvider(
- create: (_) => AppSettingsCubit(
+ create: (final _) => AppSettingsCubit(
isDarkModeOn: isDark,
- isOnbordingShowing: true,
+ isOnboardingShowing: true,
)..load(),
),
- BlocProvider(create: (_) => appConfigCubit, lazy: false),
- BlocProvider(create: (_) => ProvidersCubit()),
- BlocProvider(create: (_) => usersCubit..load(), lazy: false),
- BlocProvider(create: (_) => servicesCubit..load(), lazy: false),
- BlocProvider(create: (_) => backupsCubit..load(), lazy: false),
- BlocProvider(create: (_) => dnsRecordsCubit..load()),
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),
),
],
diff --git a/lib/config/bloc_observer.dart b/lib/config/bloc_observer.dart
index e54b399b..e68923c9 100644
--- a/lib/config/bloc_observer.dart
+++ b/lib/config/bloc_observer.dart
@@ -1,15 +1,20 @@
+import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/ui/components/error/error.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 {
SimpleBlocObserver();
@override
- void onError(BlocBase cubit, Object error, StackTrace stackTrace) {
- final navigator = getIt.get().navigator!;
+ void onError(
+ final BlocBase bloc,
+ final Object error,
+ final StackTrace stackTrace,
+ ) {
+ final NavigatorState navigator = getIt.get().navigator!;
navigator.push(
materialRoute(
@@ -19,6 +24,6 @@ class SimpleBlocObserver extends BlocObserver {
),
),
);
- super.onError(cubit, error, stackTrace);
+ super.onError(bloc, error, stackTrace);
}
}
diff --git a/lib/config/brand_colors.dart b/lib/config/brand_colors.dart
index c68d6dd8..15d1433a 100644
--- a/lib/config/brand_colors.dart
+++ b/lib/config/brand_colors.dart
@@ -10,7 +10,7 @@ class BrandColors {
static const Color gray3 = Color(0xFFFAFAFA);
static const Color gray4 = Color(0xFFDDDDDD);
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 red1 = Color(0xFFFA0E0E);
@@ -20,8 +20,8 @@ class BrandColors {
static const Color green2 = Color(0xFF0F8849);
- static get navBackgroundLight => white.withOpacity(0.8);
- static get navBackgroundDark => black.withOpacity(0.8);
+ static Color get navBackgroundLight => white.withOpacity(0.8);
+ static Color get navBackgroundDark => black.withOpacity(0.8);
static const List uninitializedGradientColors = [
Color(0xFF555555),
@@ -41,14 +41,14 @@ class BrandColors {
Color(0xFFEFD135),
];
- static const primary = blue;
- static const headlineColor = black;
- static const inactive = gray2;
- static const scaffoldBackground = gray3;
- static const inputInactive = gray4;
+ static const Color primary = blue;
+ static const Color headlineColor = black;
+ static const Color inactive = gray2;
+ static const Color scaffoldBackground = gray3;
+ static const Color inputInactive = gray4;
- static const textColor1 = black;
- static const textColor2 = gray1;
- static const dividerColor = gray5;
- static const warning = red1;
+ static const Color textColor1 = black;
+ static const Color textColor2 = gray1;
+ static const Color dividerColor = gray5;
+ static const Color warning = red1;
}
diff --git a/lib/config/brand_theme.dart b/lib/config/brand_theme.dart
index 23755de3..3ad0623c 100644
--- a/lib/config/brand_theme.dart
+++ b/lib/config/brand_theme.dart
@@ -1,14 +1,15 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/text_themes.dart';
-import 'brand_colors.dart';
+import 'package:selfprivacy/config/brand_colors.dart';
-final lightTheme = ThemeData(
+final ThemeData lightTheme = ThemeData(
+ useMaterial3: true,
primaryColor: BrandColors.primary,
fontFamily: 'Inter',
brightness: Brightness.light,
scaffoldBackgroundColor: BrandColors.scaffoldBackground,
- inputDecorationTheme: InputDecorationTheme(
+ inputDecorationTheme: const InputDecorationTheme(
border: InputBorder.none,
contentPadding: EdgeInsets.all(16),
enabledBorder: OutlineInputBorder(
@@ -38,7 +39,7 @@ final lightTheme = ThemeData(
color: BrandColors.red1,
),
),
- listTileTheme: ListTileThemeData(
+ listTileTheme: const ListTileThemeData(
minLeadingWidth: 24.0,
),
textTheme: TextTheme(
@@ -47,25 +48,25 @@ final lightTheme = ThemeData(
headline3: headline3Style,
headline4: headline4Style,
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 = lightTheme.copyWith(
+ThemeData darkTheme = lightTheme.copyWith(
brightness: Brightness.dark,
- scaffoldBackgroundColor: Color(0xFF202120),
- iconTheme: IconThemeData(color: BrandColors.gray3),
+ scaffoldBackgroundColor: const Color(0xFF202120),
+ iconTheme: const IconThemeData(color: BrandColors.gray3),
cardColor: BrandColors.gray1,
- dialogBackgroundColor: Color(0xFF202120),
+ dialogBackgroundColor: const Color(0xFF202120),
textTheme: TextTheme(
headline1: headline1Style.copyWith(color: BrandColors.white),
headline2: headline2Style.copyWith(color: BrandColors.white),
headline3: headline3Style.copyWith(color: BrandColors.white),
headline4: headline4Style.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),
hintStyle: TextStyle(color: BrandColors.white),
border: OutlineInputBorder(
@@ -81,6 +82,7 @@ var darkTheme = lightTheme.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);
diff --git a/lib/config/get_it_config.dart b/lib/config/get_it_config.dart
index eb5c0902..6961ea94 100644
--- a/lib/config/get_it_config.dart
+++ b/lib/config/get_it_config.dart
@@ -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/console.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';
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/timer.dart';
-final getIt = GetIt.instance;
+final GetIt getIt = GetIt.instance;
Future getItSetup() async {
getIt.registerSingleton(NavigationService());
getIt.registerSingleton(ConsoleModel());
getIt.registerSingleton(TimerModel());
- getIt.registerSingleton(SSHModel()..init());
getIt.registerSingleton(ApiConfigModel()..init());
await getIt.allReady();
diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart
index e7ed84e3..03355311 100644
--- a/lib/config/hive_config.dart
+++ b/lib/config/hive_config.dart
@@ -3,72 +3,119 @@ import 'dart:typed_data';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive_flutter/hive_flutter.dart';
-import 'package:selfprivacy/logic/models/backblaze_bucket.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/logic/models/hive/backblaze_bucket.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';
class HiveConfig {
static Future init() async {
await Hive.initFlutter();
Hive.registerAdapter(UserAdapter());
- Hive.registerAdapter(HetznerServerDetailsAdapter());
- Hive.registerAdapter(CloudFlareDomainAdapter());
+ Hive.registerAdapter(ServerHostingDetailsAdapter());
+ Hive.registerAdapter(ServerDomainAdapter());
Hive.registerAdapter(BackblazeCredentialAdapter());
Hive.registerAdapter(BackblazeBucketAdapter());
- Hive.registerAdapter(HetznerDataBaseAdapter());
+ Hive.registerAdapter(ServerVolumeAdapter());
- await Hive.openBox(BNames.appSettings);
- await Hive.openBox(BNames.users);
- await Hive.openBox(BNames.servicesState);
+ Hive.registerAdapter(DnsProviderAdapter());
+ Hive.registerAdapter(ServerProviderAdapter());
- var cipher = HiveAesCipher(await getEncryptedKey(BNames.key));
- await Hive.openBox(BNames.appConfig, encryptionCipher: cipher);
+ await Hive.openBox(BNames.appSettingsBox);
- var sshCipher = HiveAesCipher(await getEncryptedKey(BNames.sshEnckey));
- await Hive.openBox(BNames.sshConfig, encryptionCipher: sshCipher);
+ final HiveAesCipher cipher = HiveAesCipher(
+ await getEncryptedKey(BNames.serverInstallationEncryptionKey),
+ );
+
+ await Hive.openBox(BNames.usersDeprecated);
+ await Hive.openBox(BNames.usersBox, encryptionCipher: cipher);
+
+ final Box deprecatedUsers = Hive.box(BNames.usersDeprecated);
+ if (deprecatedUsers.isNotEmpty) {
+ final Box users = Hive.box(BNames.usersBox);
+ users.addAll(deprecatedUsers.values.toList());
+ deprecatedUsers.clear();
+ }
+
+ await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher);
}
- static Future getEncryptedKey(String encKey) async {
- final secureStorage = FlutterSecureStorage();
- var hasEncryptionKey = await secureStorage.containsKey(key: encKey);
+ static Future getEncryptedKey(final String encKey) async {
+ const FlutterSecureStorage secureStorage = FlutterSecureStorage();
+ final bool hasEncryptionKey = await secureStorage.containsKey(key: encKey);
if (!hasEncryptionKey) {
- var key = Hive.generateSecureKey();
+ final List key = Hive.generateSecureKey();
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!);
}
}
+/// Mappings for the different boxes and their keys
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 isOnbordingShowing = 'isOnbordingShowing';
- static String users = 'users';
+
+ /// A boolean field of [appSettingsBox] box.
+ static String isOnboardingShowing = 'isOnboardingShowing';
+
+ /// Encryption key to decrypt [serverInstallationBox] and [usersBox] box.
+ static String serverInstallationEncryptionKey = 'key';
+
+ /// Server installation box. Contains server details and provider tokens.
+ static String serverInstallationBox = 'appConfig';
+
+ /// A List field of [serverInstallationBox] box.
static String rootKeys = 'rootKeys';
- static String appSettings = 'appSettings';
- static String servicesState = 'servicesState';
-
- static String key = 'key';
- static String sshEnckey = 'sshEngkey';
-
- static String cloudFlareDomain = 'cloudFlareDomain';
- static String hetznerKey = 'hetznerKey';
- static String cloudFlareKey = 'cloudFlareKey';
- static String rootUser = 'rootUser';
- static String hetznerServer = 'hetznerServer';
+ /// A boolean field of [serverInstallationBox] box.
static String hasFinalChecked = 'hasFinalChecked';
+
+ /// A boolean field of [serverInstallationBox] box.
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';
+
+ /// A boolean field of [serverInstallationBox] box.
static String isServerResetedFirstTime = 'isServerResetedFirstTime';
+
+ /// A boolean field of [serverInstallationBox] box.
static String isServerResetedSecondTime = 'isServerResetedSecondTime';
- static String sshConfig = 'sshConfig';
- static String sshPrivateKey = "sshPrivateKey";
- static String sshPublicKey = "sshPublicKey";
+
+ /// A boolean field of [serverInstallationBox] box.
+ static String isRecoveringServer = 'isRecoveringServer';
+
+ /// Deprecated users box as it is unencrypted
+ static String usersDeprecated = 'users';
+
+ /// Box with users
+ static String usersBox = 'usersEncrypted';
}
diff --git a/lib/config/localization.dart b/lib/config/localization.dart
index 09d5ac07..b8356950 100644
--- a/lib/config/localization.dart
+++ b/lib/config/localization.dart
@@ -3,20 +3,18 @@ import 'package:flutter/material.dart';
class Localization extends StatelessWidget {
const Localization({
- Key? key,
+ final super.key,
this.child,
- }) : super(key: key);
+ });
final Widget? child;
@override
- Widget build(BuildContext context) {
- return EasyLocalization(
- supportedLocales: [Locale('ru'), Locale('en')],
- path: 'assets/translations',
- fallbackLocale: Locale('en'),
- saveLocale: false,
- useOnlyLangCode: true,
- child: child!,
- );
- }
+ Widget build(final BuildContext context) => EasyLocalization(
+ supportedLocales: const [Locale('ru'), Locale('en')],
+ path: 'assets/translations',
+ fallbackLocale: const Locale('en'),
+ saveLocale: false,
+ useOnlyLangCode: true,
+ child: child!,
+ );
}
diff --git a/lib/config/text_themes.dart b/lib/config/text_themes.dart
index f14d54e1..63b4b99c 100644
--- a/lib/config/text_themes.dart
+++ b/lib/config/text_themes.dart
@@ -1,80 +1,80 @@
import 'package:flutter/material.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,
color: BrandColors.textColor1,
);
-final headline1Style = defaultTextStyle.copyWith(
+final TextStyle headline1Style = defaultTextStyle.copyWith(
fontSize: 40,
fontWeight: NamedFontWeight.extraBold,
color: BrandColors.headlineColor,
);
-final headline2Style = defaultTextStyle.copyWith(
+final TextStyle headline2Style = defaultTextStyle.copyWith(
fontSize: 24,
fontWeight: NamedFontWeight.extraBold,
color: BrandColors.headlineColor,
);
-final onboardingTitle = defaultTextStyle.copyWith(
+final TextStyle onboardingTitle = defaultTextStyle.copyWith(
fontSize: 30,
fontWeight: NamedFontWeight.extraBold,
color: BrandColors.headlineColor,
);
-final headline3Style = defaultTextStyle.copyWith(
+final TextStyle headline3Style = defaultTextStyle.copyWith(
fontSize: 20,
fontWeight: NamedFontWeight.extraBold,
color: BrandColors.headlineColor,
);
-final headline4Style = defaultTextStyle.copyWith(
+final TextStyle headline4Style = defaultTextStyle.copyWith(
fontSize: 18,
fontWeight: NamedFontWeight.medium,
color: BrandColors.headlineColor,
);
-final headline4UnderlinedStyle = defaultTextStyle.copyWith(
+final TextStyle headline4UnderlinedStyle = defaultTextStyle.copyWith(
fontSize: 18,
fontWeight: NamedFontWeight.medium,
color: BrandColors.headlineColor,
decoration: TextDecoration.underline,
);
-final headline5Style = defaultTextStyle.copyWith(
+final TextStyle headline5Style = defaultTextStyle.copyWith(
fontSize: 15,
fontWeight: NamedFontWeight.medium,
color: BrandColors.headlineColor.withOpacity(0.8),
);
-final body1Style = defaultTextStyle;
-final body2Style = defaultTextStyle.copyWith(
+const TextStyle body1Style = defaultTextStyle;
+final TextStyle body2Style = defaultTextStyle.copyWith(
color: BrandColors.textColor2,
);
-final buttonTitleText = defaultTextStyle.copyWith(
+final TextStyle buttonTitleText = defaultTextStyle.copyWith(
color: BrandColors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
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);
-
-final progressTextStyleLight = TextStyle(
+const TextStyle progressTextStyleLight = TextStyle(
fontSize: 11,
color: BrandColors.textColor1,
height: 1.7,
);
-final progressTextStyleDark = progressTextStyleLight.copyWith(
+final TextStyle progressTextStyleDark = progressTextStyleLight.copyWith(
color: BrandColors.white,
);
diff --git a/lib/logic/api_maps/api_map.dart b/lib/logic/api_maps/api_map.dart
index 5e07d838..007bfd98 100644
--- a/lib/logic/api_maps/api_map.dart
+++ b/lib/logic/api_maps/api_map.dart
@@ -10,27 +10,32 @@ import 'package:selfprivacy/logic/models/message.dart';
abstract class ApiMap {
Future getClient() async {
- var dio = Dio(await options);
+ final Dio dio = Dio(await options);
if (hasLogger) {
dio.interceptors.add(PrettyDioLogger());
}
dio.interceptors.add(ConsoleInterceptor());
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
- (HttpClient client) {
+ (final HttpClient client) {
client.badCertificateCallback =
- (X509Certificate cert, String host, int port) => true;
+ (final X509Certificate cert, final String host, final int port) =>
+ true;
return client;
};
- dio.interceptors.add(InterceptorsWrapper(onError: (DioError e, handler) {
- print(e.requestOptions.path);
- print(e.requestOptions.data);
+ dio.interceptors.add(
+ InterceptorsWrapper(
+ onError: (final DioError e, final ErrorInterceptorHandler handler) {
+ print(e.requestOptions.path);
+ print(e.requestOptions.data);
- print(e.message);
- print(e.response);
+ print(e.message);
+ print(e.response);
- return handler.next(e);
- }));
+ return handler.next(e);
+ },
+ ),
+ );
return dio;
}
@@ -42,21 +47,21 @@ abstract class ApiMap {
ValidateStatus? validateStatus;
- void close(Dio client) {
+ void close(final Dio client) {
client.close();
validateStatus = null;
}
}
class ConsoleInterceptor extends InterceptorsWrapper {
- void addMessage(Message message) {
+ void addMessage(final Message message) {
getIt.get().addMessage(message);
}
@override
- Future onRequest(
- RequestOptions options,
- RequestInterceptorHandler requestInterceptorHandler,
+ Future onRequest(
+ final RequestOptions options,
+ final RequestInterceptorHandler handler,
) async {
addMessage(
Message(
@@ -64,13 +69,13 @@ class ConsoleInterceptor extends InterceptorsWrapper {
'request-uri: ${options.uri}\nheaders: ${options.headers}\ndata: ${options.data}',
),
);
- return super.onRequest(options, requestInterceptorHandler);
+ return super.onRequest(options, handler);
}
@override
- Future onResponse(
- Response response,
- ResponseInterceptorHandler requestInterceptorHandler,
+ Future onResponse(
+ final Response response,
+ final ResponseInterceptorHandler handler,
) async {
addMessage(
Message(
@@ -80,13 +85,16 @@ class ConsoleInterceptor extends InterceptorsWrapper {
);
return super.onResponse(
response,
- requestInterceptorHandler,
+ handler,
);
}
@override
- Future onError(DioError err, ErrorInterceptorHandler handler) async {
- var response = err.response;
+ Future onError(
+ final DioError err,
+ final ErrorInterceptorHandler handler,
+ ) async {
+ final Response? response = err.response;
log(err.toString());
addMessage(
Message.warn(
diff --git a/lib/logic/api_maps/backblaze.dart b/lib/logic/api_maps/backblaze.dart
index 6a56788c..8d827e78 100644
--- a/lib/logic/api_maps/backblaze.dart
+++ b/lib/logic/api_maps/backblaze.dart
@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/api_map.dart';
-import 'package:selfprivacy/logic/models/backblaze_credential.dart';
+import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart';
class BackblazeApiAuth {
BackblazeApiAuth({required this.authorizationToken, required this.apiUrl});
@@ -13,8 +13,10 @@ class BackblazeApiAuth {
}
class BackblazeApplicationKey {
- BackblazeApplicationKey(
- {required this.applicationKeyId, required this.applicationKey});
+ BackblazeApplicationKey({
+ required this.applicationKeyId,
+ required this.applicationKey,
+ });
final String applicationKeyId;
final String applicationKey;
@@ -23,11 +25,13 @@ class BackblazeApplicationKey {
class BackblazeApi extends ApiMap {
BackblazeApi({this.hasLogger = false, this.isWithToken = true});
+ @override
BaseOptions get options {
- var options = BaseOptions(baseUrl: rootAddress);
+ final BaseOptions options = BaseOptions(baseUrl: rootAddress);
if (isWithToken) {
- var backblazeCredential = getIt().backblazeCredential;
- var token = backblazeCredential!.applicationKey;
+ final BackblazeCredential? backblazeCredential =
+ getIt().backblazeCredential;
+ final String token = backblazeCredential!.applicationKey;
options.headers = {'Authorization': 'Basic $token'};
}
@@ -44,14 +48,17 @@ class BackblazeApi extends ApiMap {
String apiPrefix = '/b2api/v2';
Future getAuthorizationToken() async {
- var client = await getClient();
- var backblazeCredential = getIt().backblazeCredential;
+ final Dio client = await getClient();
+ final BackblazeCredential? backblazeCredential =
+ getIt().backblazeCredential;
if (backblazeCredential == null) {
throw Exception('Backblaze credential is null');
}
final String encodedApiKey = encodedBackblazeKey(
- backblazeCredential.keyId, backblazeCredential.applicationKey);
- var response = await client.get(
+ backblazeCredential.keyId,
+ backblazeCredential.applicationKey,
+ );
+ final Response response = await client.get(
'b2_authorize_account',
options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}),
);
@@ -64,32 +71,38 @@ class BackblazeApi extends ApiMap {
);
}
- Future isValid(String encodedApiKey) async {
- var client = await getClient();
- Response response = await client.get(
- 'b2_authorize_account',
- options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}),
- );
- close(client);
- if (response.statusCode == HttpStatus.ok) {
- if (response.data['allowed']['capabilities'].contains('listBuckets')) {
- return true;
+ Future 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;
- } else if (response.statusCode == HttpStatus.unauthorized) {
- return false;
- } else {
- throw Exception('code: ${response.statusCode}');
+ } finally {
+ close(client);
}
}
// Create bucket
- Future createBucket(String bucketName) async {
- final auth = await getAuthorizationToken();
- var backblazeCredential = getIt().backblazeCredential;
- var client = await getClient();
+ Future createBucket(final String bucketName) async {
+ final BackblazeApiAuth auth = await getAuthorizationToken();
+ final BackblazeCredential? backblazeCredential =
+ getIt().backblazeCredential;
+ final Dio client = await getClient();
client.options.baseUrl = auth.apiUrl;
- var response = await client.post(
+ final Response response = await client.post(
'$apiPrefix/b2_create_bucket',
data: {
'accountId': backblazeCredential!.keyId,
@@ -97,9 +110,9 @@ class BackblazeApi extends ApiMap {
'bucketType': 'allPrivate',
'lifecycleRules': [
{
- "daysFromHidingToDeleting": 30,
- "daysFromUploadingToHiding": null,
- "fileNamePrefix": ""
+ 'daysFromHidingToDeleting': 30,
+ 'daysFromUploadingToHiding': null,
+ 'fileNamePrefix': ''
}
],
},
@@ -116,11 +129,11 @@ class BackblazeApi extends ApiMap {
}
// Create a limited capability key with access to the given bucket
- Future createKey(String bucketId) async {
- final auth = await getAuthorizationToken();
- var client = await getClient();
+ Future createKey(final String bucketId) async {
+ final BackblazeApiAuth auth = await getAuthorizationToken();
+ final Dio client = await getClient();
client.options.baseUrl = auth.apiUrl;
- var response = await client.post(
+ final Response response = await client.post(
'$apiPrefix/b2_create_key',
data: {
'accountId': getIt().backblazeCredential!.keyId,
@@ -135,8 +148,9 @@ class BackblazeApi extends ApiMap {
close(client);
if (response.statusCode == HttpStatus.ok) {
return BackblazeApplicationKey(
- applicationKeyId: response.data['applicationKeyId'],
- applicationKey: response.data['applicationKey']);
+ applicationKeyId: response.data['applicationKeyId'],
+ applicationKey: response.data['applicationKey'],
+ );
} else {
throw Exception('code: ${response.statusCode}');
}
diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart
index 5aaf9dc7..9141d5fe 100644
--- a/lib/logic/api_maps/cloudflare.dart
+++ b/lib/logic/api_maps/cloudflare.dart
@@ -3,20 +3,40 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/api_map.dart';
-import 'package:selfprivacy/logic/models/cloudflare_domain.dart';
-import 'package:selfprivacy/logic/models/dns_records.dart';
+import 'package:selfprivacy/logic/models/hive/server_domain.dart';
+import 'package:selfprivacy/logic/models/json/dns_records.dart';
+
+class DomainNotFoundException implements Exception {
+ DomainNotFoundException(this.message);
+ final String message;
+}
class CloudflareApi extends ApiMap {
- CloudflareApi({this.hasLogger = 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 {
- var options = BaseOptions(baseUrl: rootAddress);
+ final BaseOptions options = BaseOptions(baseUrl: rootAddress);
if (isWithToken) {
- var token = getIt().cloudFlareKey;
+ final String? token = getIt().cloudFlareKey;
assert(token != null);
options.headers = {'Authorization': 'Bearer $token'};
}
+ if (customToken != null) {
+ options.headers = {'Authorization': 'Bearer $customToken'};
+ }
+
if (validateStatus != null) {
options.validateStatus = validateStatus!;
}
@@ -26,14 +46,15 @@ class CloudflareApi extends ApiMap {
@override
String rootAddress = 'https://api.cloudflare.com/client/v4';
- Future isValid(String token) async {
- validateStatus = (status) {
- return status == HttpStatus.ok || status == HttpStatus.unauthorized;
- };
+ Future isValid(final String token) async {
+ validateStatus = (final status) =>
+ status == HttpStatus.ok || status == HttpStatus.unauthorized;
- var client = await getClient();
- Response response = await client.get('/user/tokens/verify',
- options: Options(headers: {'Authorization': 'Bearer $token'}));
+ final Dio client = await getClient();
+ final Response response = await client.get(
+ '/user/tokens/verify',
+ options: Options(headers: {'Authorization': 'Bearer $token'}),
+ );
close(client);
@@ -46,37 +67,40 @@ class CloudflareApi extends ApiMap {
}
}
- Future getZoneId(String domain) async {
- validateStatus = (status) {
- return status == HttpStatus.ok || status == HttpStatus.forbidden;
- };
- var client = await getClient();
- Response response = await client.get(
+ Future getZoneId(final String domain) async {
+ validateStatus = (final status) =>
+ status == HttpStatus.ok || status == HttpStatus.forbidden;
+ final Dio client = await getClient();
+ final Response response = await client.get(
'/zones',
queryParameters: {'name': domain},
);
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 removeSimilarRecords({
- String? ip4,
- required CloudFlareDomain cloudFlareDomain,
+ required final ServerDomain cloudFlareDomain,
+ final String? ip4,
}) async {
- var domainName = cloudFlareDomain.domainName;
- var domainZoneId = cloudFlareDomain.zoneId;
+ final String domainName = cloudFlareDomain.domainName;
+ final String domainZoneId = cloudFlareDomain.zoneId;
- var url = '/zones/$domainZoneId/dns_records';
+ final String url = '/zones/$domainZoneId/dns_records';
- var client = await getClient();
- Response response = await client.get(url);
+ final Dio client = await getClient();
+ final Response response = await client.get(url);
- List records = response.data['result'] ?? [];
- var allDeleteFutures = [];
+ final List records = response.data['result'] ?? [];
+ final List allDeleteFutures = [];
- for (var record in records) {
+ for (final record in records) {
if (record['zone_name'] == domainName) {
allDeleteFutures.add(
client.delete('$url/${record["id"]}'),
@@ -89,28 +113,30 @@ class CloudflareApi extends ApiMap {
}
Future> getDnsRecords({
- required CloudFlareDomain cloudFlareDomain,
+ required final ServerDomain cloudFlareDomain,
}) async {
- var domainName = cloudFlareDomain.domainName;
- var domainZoneId = cloudFlareDomain.zoneId;
+ final String domainName = cloudFlareDomain.domainName;
+ final String domainZoneId = cloudFlareDomain.zoneId;
- var url = '/zones/$domainZoneId/dns_records';
+ final String url = '/zones/$domainZoneId/dns_records';
- var client = await getClient();
- Response response = await client.get(url);
+ final Dio client = await getClient();
+ final Response response = await client.get(url);
- List records = response.data['result'] ?? [];
- var allRecords = [];
+ final List records = response.data['result'] ?? [];
+ final List allRecords = [];
- for (var record in records) {
+ for (final record in records) {
if (record['zone_name'] == domainName) {
- allRecords.add(DnsRecord(
- name: record['name'],
- type: record['type'],
- content: record['content'],
- ttl: record['ttl'],
- proxied: record['proxied'],
- ));
+ allRecords.add(
+ DnsRecord(
+ name: record['name'],
+ type: record['type'],
+ content: record['content'],
+ ttl: record['ttl'],
+ proxied: record['proxied'],
+ ),
+ );
}
}
@@ -119,51 +145,59 @@ class CloudflareApi extends ApiMap {
}
Future createMultipleDnsRecords({
- String? ip4,
- required CloudFlareDomain cloudFlareDomain,
+ required final ServerDomain cloudFlareDomain,
+ final String? ip4,
}) async {
- var domainName = cloudFlareDomain.domainName;
- var domainZoneId = cloudFlareDomain.zoneId;
- var listDnsRecords = projectDnsRecords(domainName, ip4);
+ final String domainName = cloudFlareDomain.domainName;
+ final String domainZoneId = cloudFlareDomain.zoneId;
+ final List listDnsRecords = projectDnsRecords(domainName, ip4);
+ final List allCreateFutures = [];
- var url = '$rootAddress/zones/$domainZoneId/dns_records';
-
- var allCreateFutures = [];
- var client = await getClient();
-
- for (var record in listDnsRecords) {
- allCreateFutures.add(
- client.post(
- url,
- data: record.toJson(),
- ),
- );
+ final Dio client = await getClient();
+ try {
+ for (final DnsRecord record in listDnsRecords) {
+ allCreateFutures.add(
+ client.post(
+ '/zones/$domainZoneId/dns_records',
+ data: record.toJson(),
+ ),
+ );
+ }
+ await Future.wait(allCreateFutures);
+ } on DioError catch (e) {
+ print(e.message);
+ rethrow;
+ } finally {
+ close(client);
}
-
- await Future.wait(allCreateFutures);
- close(client);
}
- List projectDnsRecords(String? domainName, String? ip4) {
- var domainA = DnsRecord(type: 'A', name: domainName, content: ip4);
+ List projectDnsRecords(
+ final String? domainName,
+ final String? ip4,
+ ) {
+ final DnsRecord domainA =
+ DnsRecord(type: 'A', name: domainName, content: ip4);
- var mx = DnsRecord(type: 'MX', name: '@', content: domainName);
- var apiA = DnsRecord(type: 'A', name: 'api', content: ip4);
- var cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4);
- var gitA = DnsRecord(type: 'A', name: 'git', content: ip4);
- var meetA = DnsRecord(type: 'A', name: 'meet', content: ip4);
- var passwordA = DnsRecord(type: 'A', name: 'password', content: ip4);
- var socialA = DnsRecord(type: 'A', name: 'social', content: ip4);
- var vpn = DnsRecord(type: 'A', name: 'vpn', 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);
- var txt1 = DnsRecord(
+ final DnsRecord txt1 = DnsRecord(
type: 'TXT',
name: '_dmarc',
content: 'v=DMARC1; p=none',
ttl: 18000,
);
- var txt2 = DnsRecord(
+ final DnsRecord txt2 = DnsRecord(
type: 'TXT',
name: domainName,
content: 'v=spf1 a mx ip4:$ip4 -all',
@@ -186,18 +220,20 @@ class CloudflareApi extends ApiMap {
}
Future setDkim(
- String dkimRecordString, CloudFlareDomain cloudFlareDomain) async {
- final domainZoneId = cloudFlareDomain.zoneId;
- final url = '$rootAddress/zones/$domainZoneId/dns_records';
+ final String dkimRecordString,
+ final ServerDomain cloudFlareDomain,
+ ) async {
+ final String domainZoneId = cloudFlareDomain.zoneId;
+ final String url = '$rootAddress/zones/$domainZoneId/dns_records';
- final dkimRecord = DnsRecord(
+ final DnsRecord dkimRecord = DnsRecord(
type: 'TXT',
name: 'selector._domainkey',
content: dkimRecordString,
ttl: 18000,
);
- var client = await getClient();
+ final Dio client = await getClient();
await client.post(
url,
data: dkimRecord.toJson(),
@@ -207,23 +243,17 @@ class CloudflareApi extends ApiMap {
}
Future> domainList() async {
- var url = '$rootAddress/zones?per_page=50';
- var client = await getClient();
+ final String url = '$rootAddress/zones';
+ final Dio client = await getClient();
- var response = await client.get(
+ final Response response = await client.get(
url,
queryParameters: {'per_page': 50},
);
close(client);
return response.data['result']
- .map((el) => el['name'] as String)
+ .map((final el) => el['name'] as String)
.toList();
}
-
- @override
- final bool hasLogger;
-
- @override
- final bool isWithToken;
}
diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart
index 77afa5d8..4de4f36f 100644
--- a/lib/logic/api_maps/hetzner.dart
+++ b/lib/logic/api_maps/hetzner.dart
@@ -4,21 +4,23 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/api_map.dart';
-import 'package:selfprivacy/logic/models/hetzner_server_info.dart';
-import 'package:selfprivacy/logic/models/server_details.dart';
-import 'package:selfprivacy/logic/models/user.dart';
+import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart';
+import 'package:selfprivacy/logic/models/hive/server_details.dart';
+import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class HetznerApi extends ApiMap {
+ HetznerApi({this.hasLogger = false, this.isWithToken = true});
+ @override
bool hasLogger;
+ @override
bool isWithToken;
- HetznerApi({this.hasLogger = false, this.isWithToken = true});
-
+ @override
BaseOptions get options {
- var options = BaseOptions(baseUrl: rootAddress);
+ final BaseOptions options = BaseOptions(baseUrl: rootAddress);
if (isWithToken) {
- var token = getIt().hetznerKey;
+ final String? token = getIt().hetznerKey;
assert(token != null);
options.headers = {'Authorization': 'Bearer $token'};
}
@@ -33,12 +35,11 @@ class HetznerApi extends ApiMap {
@override
String rootAddress = 'https://api.hetzner.cloud/v1';
- Future isValid(String token) async {
- validateStatus = (status) {
- return status == HttpStatus.ok || status == HttpStatus.unauthorized;
- };
- var client = await getClient();
- Response response = await client.get(
+ Future isValid(final String token) async {
+ validateStatus = (final int? status) =>
+ status == HttpStatus.ok || status == HttpStatus.unauthorized;
+ final Dio client = await getClient();
+ final Response response = await client.get(
'/servers',
options: Options(
headers: {'Authorization': 'Bearer $token'},
@@ -55,99 +56,98 @@ class HetznerApi extends ApiMap {
}
}
- Future isFreeToCreate() async {
- var client = await getClient();
-
- 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 createVolume() async {
- var client = await getClient();
- Response dbCreateResponse = await client.post(
+ Future createVolume() async {
+ final Dio client = await getClient();
+ final Response dbCreateResponse = await client.post(
'/volumes',
data: {
- "size": 10,
- "name": StringGenerators.dbStorageName(),
- "labels": {"labelkey": "value"},
- "location": "fsn1",
- "automount": false,
- "format": "ext4"
+ 'size': 10,
+ 'name': StringGenerators.dbStorageName(),
+ 'labels': {'labelkey': 'value'},
+ 'location': 'fsn1',
+ 'automount': false,
+ 'format': 'ext4'
},
);
- var dbId = dbCreateResponse.data['volume']['id'];
- return HetznerDataBase(
+ final dbId = dbCreateResponse.data['volume']['id'];
+ return ServerVolume(
id: dbId,
name: dbCreateResponse.data['volume']['name'],
);
}
- Future createServer({
- required String cloudFlareKey,
- required User rootUser,
- required String domainName,
- required HetznerDataBase dataBase,
+ Future createServer({
+ required final String cloudFlareKey,
+ required final User rootUser,
+ required final String domainName,
+ required final ServerVolume dataBase,
}) async {
- var client = await getClient();
+ final Dio client = await getClient();
- var dbPassword = StringGenerators.dbPassword();
- var dbId = dataBase.id;
+ final String dbPassword = StringGenerators.dbPassword();
+ final int dbId = dataBase.id;
- final apiToken = StringGenerators.apiToken();
+ final String apiToken = StringGenerators.apiToken();
- final hostname = getHostnameFromDomain(domainName);
+ final String hostname = getHostnameFromDomain(domainName);
- final base64Password =
+ final String base64Password =
base64.encode(utf8.encode(rootUser.password ?? 'PASS'));
- print("hostname: $hostname");
+ print('hostname: $hostname');
/// add ssh key when you need it: e.g. "ssh_keys":["kherel"]
/// check the branch name, it could be "development" or "master".
///
- final userdataString =
+ 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);
- final data = {
- "name": hostname,
- "server_type": "cx11",
- "start_after_create": false,
- "image": "ubuntu-20.04",
- "volumes": [dbId],
- "networks": [],
- "user_data": userdataString,
- "labels": {},
- "automount": true,
- "location": "fsn1"
+ final Map data = {
+ '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");
+ print('Decoded data: $data');
- Response serverCreateResponse = await client.post(
- '/servers',
- data: data,
- );
+ ServerHostingDetails? serverDetails;
- print(serverCreateResponse.data);
- client.close();
- return HetznerServerDetails(
- id: serverCreateResponse.data['server']['id'],
- ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'],
- createTime: DateTime.now(),
- dataBase: dataBase,
- apiToken: apiToken,
- );
+ try {
+ final Response serverCreateResponse = await client.post(
+ '/servers',
+ data: data,
+ );
+ 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(String domain) {
+ static String getHostnameFromDomain(final String domain) {
// Replace all non-alphanumeric characters with an underscore
- var hostname =
+ String hostname =
domain.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
if (hostname.endsWith('-')) {
hostname = hostname.substring(0, hostname.length - 1);
@@ -163,24 +163,24 @@ class HetznerApi extends ApiMap {
}
Future deleteSelfprivacyServerAndAllVolumes({
- required String domainName,
+ required final String domainName,
}) async {
- var client = await getClient();
+ final Dio client = await getClient();
- final hostname = getHostnameFromDomain(domainName);
+ final String hostname = getHostnameFromDomain(domainName);
- Response serversReponse = await client.get('/servers');
- List servers = serversReponse.data['servers'];
- Map server = servers.firstWhere((el) => el['name'] == hostname);
- List volumes = server['volumes'];
- var laterFutures = [];
+ 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 laterFutures = [];
- for (var volumeId in volumes) {
+ for (final volumeId in volumes) {
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('/servers/${server['id']}'));
@@ -189,20 +189,20 @@ class HetznerApi extends ApiMap {
close(client);
}
- Future reset() async {
- var server = getIt().hetznerServer!;
+ Future reset() async {
+ final ServerHostingDetails server = getIt().serverDetails!;
- var client = await getClient();
+ final Dio client = await getClient();
await client.post('/servers/${server.id}/actions/reset');
close(client);
return server.copyWith(startTime: DateTime.now());
}
- Future powerOn() async {
- var server = getIt().hetznerServer!;
+ Future powerOn() async {
+ final ServerHostingDetails server = getIt().serverDetails!;
- var client = await getClient();
+ final Dio client = await getClient();
await client.post('/servers/${server.id}/actions/poweron');
close(client);
@@ -210,16 +210,20 @@ class HetznerApi extends ApiMap {
}
Future