diff --git a/appimage.yml b/appimage.yml index 40bcc3c3..9914346f 100644 --- a/appimage.yml +++ b/appimage.yml @@ -10,7 +10,7 @@ AppDir: id: org.selfprivacy.app name: SelfPrivacy icon: org.selfprivacy.app - version: 0.9.1 + version: 0.10.0 exec: selfprivacy exec_args: $@ apt: diff --git a/assets/markdown/how_hetzner-be.md b/assets/markdown/how_hetzner-be.md index 758f6ca0..cf6b2984 100644 --- a/assets/markdown/how_hetzner-be.md +++ b/assets/markdown/how_hetzner-be.md @@ -9,7 +9,7 @@ наступнае: **SSH Keys, API Tokens, Certificates, Members.** Вы патрэбныя **API Tokens**. Націсніце на яго. 5. У правай частцы інтэрфейсу павінна быць **Generate API token** button. Калі вы карыстаецеся мабільнай версіяй вэб-старонкі, у - у правым ніжнім куце вы ўбачыце **red cross**. Націсніце гэтую кнопку. + у правым ніжнім куце вы ўбачыце **чырвоны плюс**. Націсніце гэтую кнопку. 6. У полі **Description** дайце нашаму токену імя (гэта можа быць любое імя, якое вам падабаецца. На сутнасць гэта не ўплывае. 7. Пад полем **permissions** мы бачым магчымасць выбару @@ -18,4 +18,4 @@ 9. Пасля гэтага будзе паказаны наш ключ. Захоўвайце яго ў надзейным месцы, або ў менеджэры пароляў, што лепш. -![Наладжванне маркера Hetzner](рэсурс:assets/images/gifs/Hetzner.gif) +![Наладжванне маркера Hetzner](resource:assets/images/gifs/Hetzner.gif) diff --git a/assets/markdown/how_hetzner-ru.md b/assets/markdown/how_hetzner-ru.md index 94d19c37..82bef302 100644 --- a/assets/markdown/how_hetzner-ru.md +++ b/assets/markdown/how_hetzner-ru.md @@ -1,9 +1,12 @@ -### Как получить Hetzner API Token -1. Переходим по ссылке https://hetzner.com -2. Заходим в созданный нами проект. Если такового - нет, значит создаём. -3. Наводим мышкой на боковую панель. Она должна раскрыться, показав нам пункты меню. Нас интересует последний — Security (с иконкой ключика). -4. Далее, в верхней части интерфейса видим примерно такой список: SSH Keys, API Tokens, Certificates, Members. Нам нужен API Tokens. Переходим по нему. -5. В правой части интерфейса, нас будет ожидать кнопка Generate API token. Если же Вы используете мобильную версию сайта, в нижнем правом углу Вы увидите красный плюсик. Нажимаем на эту кнопку. -6. В поле Description, даём нашему токену название (это может быть любое название, которые Вам нравиться. Сути оно не меняет. +### Как получить токен API от Hetzner +1. Посетите следующую [ссылку](https://console.hetzner.cloud/) и войдите в свой новый аккаунт. +2. Войдите в ранее созданный проект. Если вы еще не создали его, пожалуйста, сделайте это. +3. Наведите курсор мыши на боковую панель. Панель должна расшириться и показать меню. Нас интересует последний пункт — **Security** (иконка ключа). +4. Далее, в верхней части интерфейса, видим примерно следующее: **SSH Keys, API Tokens, Certificates, Members.** Нужно нажать **API Tokens**. +5. В правой части интерфейса должна быть кнопка **Generate API token**. Если вы используете мобильную версию веб-страницы, в нижнем правом углу вы увидите **красный плюс**. Нажмите на эту кнопку. +6. В поле **Description** дайте вашему токену имя (это может быть любое имя, которое вам нравится). +7. Под полем **Description** мы видим возможность выбрать **permissions**. Выберите **Read & Write**. +8. Нажмите **Generate API Token**. +9. После этого вам будет показан ключ. Храните его в надежном месте, или в менеджере паролей, что еще лучше. -![Hetzner token setup](resource:assets/images/gifs/Hetzner.gif) +![Получение токена Hetzner](resource:assets/images/gifs/Hetzner.gif) diff --git a/assets/translations/es.json b/assets/translations/es.json index ce007fb5..54d11a45 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -31,7 +31,7 @@ "remove": "Eliminar", "apply": "Solicitar", "done": "Hecho", - "connect_to_existing": "Conectarse a un servidor de SelfPrivacy existente", + "connect_to_existing": "¡Ya tengo un servidor de SelfPrivacy!", "app_name": "SelfPrivacy", "please_connect": "¡Conecta tu servidor y dominio para sumergirte!", "copied_to_clipboard": "¡Copiado al portapapeles!" @@ -45,7 +45,10 @@ "delete_server_title": "Eliminar servidor", "delete_server_description": "Esto elimina su servidor. Ya no será accesible.", "title": "Ajustes de la aplicación", - "dark_theme_title": "Tema oscuro" + "dark_theme_title": "Tema oscuro", + "system_dark_theme_title": "Tema del sistema", + "system_dark_theme_description": "Utiliza un tema claro u oscuro de la configuración del sistema", + "dangerous_settings": "Configuraciones peligrosas" }, "ssh": { "delete_confirm_question": "¿Está seguro de que desea eliminar la clave SSH?", @@ -57,7 +60,7 @@ "subtitle_without_keys": "Sin llaves", "no_key_name": "Clave sin nombre", "root_title": "Estas son las claves de superusuario", - "input_label": "Clave pública ED25519 o RSA" + "input_label": "Clave pública ED25519, ECDSA o RSA" }, "about_application_page": { "application_version_text": "Versión de la aplicación {}", @@ -92,5 +95,26 @@ }, "about_us_page": { "title": "Sobre nosotros" + }, + "server": { + "reboot_after_upgrade_hint": "Reinicio sin aviso después de aplicar cambios en el servidor", + "card_title": "Servidor", + "description": "Todos sus servicios funcionan aquí", + "general_information": "Información general", + "resource_usage": "Uso de recursos", + "server_timezone": "Zona horaria del servidor", + "select_timezone": "Selecciona zona horaria", + "reboot_after_upgrade": "Reinicia después de actualizar", + "allow_autoupgrade": "Permite la autoactualización", + "allow_autoupgrade_hint": "Ppermite actualizaciones automáticas de paquetes en el servidor" + }, + "resource_chart": { + "month": "Mes", + "out": "Fuera", + "day": "Día", + "hour": "Hora", + "cpu_title": "Uso de CPU", + "network_title": "Uso de la red", + "in": "En" } } diff --git a/assets/translations/et.json b/assets/translations/et.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/assets/translations/et.json @@ -0,0 +1 @@ +{} diff --git a/assets/translations/he.json b/assets/translations/he.json index 43b6e3a2..90e4b419 100644 --- a/assets/translations/he.json +++ b/assets/translations/he.json @@ -34,7 +34,7 @@ "username": "שם משתמש", "loading": "בטעינה…", "later": "דילוג כדי להגדיר אחר כך", - "connect_to_existing": "התחברות לשרת SelfPrivacy קיים", + "connect_to_existing": "כבר יש לי שרת SelfPrivacy!", "reset": "איפוס", "details": "פרטים", "no_data": "אין נתונים", @@ -461,7 +461,8 @@ "enter_username_and_password": "נא למלא שם משתמש וסיסמה חזקה", "finish": "הכול מאותחל", "create_master_account": "יצירת חשבון ראשי", - "checks": "בדיקות הושלמו \n{} מתוך {}" + "checks": "בדיקות הושלמו \n{} מתוך {}", + "domain_critical_error": "לא הצלחנו להגיע לשם התחום הזה! נגיעה למידע נוסף…" }, "recovering": { "method_select_other_device": "יש לי גישה דרך מכשיר אחר", @@ -480,7 +481,7 @@ "modal_confirmation_ip_valid": "ה־IP זהה לזה שברשומת ה־DNS", "modal_confirmation_ip_invalid": "ה־IP שונה מזה שברשומת ה־DNS", "generic_error": "הפעולה בוטלה, נא לנסות שוב.", - "recovery_main_header": "התחברות לשרת SelfPrivacy קיים", + "recovery_main_header": "התחברות לשרת קיים", "domain_recover_placeholder": "שם התחום שלך", "domain_recover_error": "לא נמצא שרת עם שם תחום כזה", "method_select_description": "נא לבחור שיטת שחזור:", @@ -610,7 +611,11 @@ "use_staging_acme_description": "חל על הקמת שרתים חדשים.", "use_staging_acme": "להשתמש בשרת ACME לבדיקות", "ignore_tls": "לא לאמת אישורי TLS", - "ignore_tls_description": "היישום לא יאמת אישורי RLS בעת התחברות לשרת." + "ignore_tls_description": "היישום לא יאמת אישורי RLS בעת התחברות לשרת.", + "add_root_ssh_key": "הוספת מפתח SSH למשתמש העל (root)", + "allow_ssh_key_at_setup": "לאפשר הגדרת מפתח SSH למשתמש העל (root) במהלך ההתקנה", + "allow_ssh_key_at_setup_description": "כפתור להוספת מפתח יופיע במסך האישור.", + "root_ssh_key_added": "מפתח SSH למשתמש העל (root) הוגדר והוחל" }, "cloud": { "title": "אחסון בענן", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 89770fff..0b413bdb 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -24,7 +24,7 @@ "username": "Имя пользователя", "loading": "Загрузка…", "later": "Пропустить и настроить потом", - "connect_to_existing": "Подключиться к существующему серверу SelfPrivacy", + "connect_to_existing": "У меня уже есть SelfPrivacy сервер!", "reset": "Сбросить", "details": "Детальная информация", "no_data": "Нет данных", @@ -471,11 +471,12 @@ }, "server_provider_description": "Место, где будут находиться ваши данные и сервисы SelfPrivacy:", "multiple_domains_found": "Найдено несколько доменов", - "multiple_domains_found_text": "Предоставленный токен дает доступ к следующим доменам. Пожалуйста, выберите тот, который вы хотите использовать. Для обеспечения безопасности других доменов следует ограничить доступ этого токена только тем доменом, который вы хотите использовать с SelfPrivacy." + "multiple_domains_found_text": "Предоставленный токен дает доступ к следующим доменам. Пожалуйста, выберите тот, который вы хотите использовать. Для обеспечения безопасности других доменов следует ограничить доступ этого токена только тем доменом, который вы хотите использовать с SelfPrivacy.", + "domain_critical_error": "Не получается подключиться к домену! Нажмите для подробностей…" }, "recovering": { "generic_error": "Ошибка проведения операции, попробуйте ещё раз.", - "recovery_main_header": "Подключение к существующему серверу SelfPrivacy", + "recovery_main_header": "Подключение к существующему серверу", "domain_recovery_description": "Введите домен, по которому вы хотите получить доступ к серверу:", "domain_recover_placeholder": "Домен", "domain_recover_error": "Не удалось найти сервер с таким доменом", @@ -626,6 +627,10 @@ "cubit_statuses": "Текущий статут кубитов загрузки", "reset_onboarding_description": "Принудить показ приветственного экрана", "ignore_tls_description": "Приложение не будет проверять сертификаты TLS при подключении к серверу.", - "ignore_tls": "Не проверять сертификаты TLS" + "ignore_tls": "Не проверять сертификаты TLS", + "add_root_ssh_key": "Добавить ключ суперпользователя", + "root_ssh_key_added": "SSH ключ суперпользователя задан и будет применён", + "allow_ssh_key_at_setup": "Разрешить задавать SSH ключи суперпользователя во время установки", + "allow_ssh_key_at_setup_description": "Кнопка для добавления ключа появится на экране подтверждения." } } diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 00000000..7e7e7f67 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/fastlane/metadata/android/en-US/changelogs/0.10.0.txt b/fastlane/metadata/android/en-US/changelogs/0.10.0.txt new file mode 100644 index 00000000..0de2f8df --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0.10.0.txt @@ -0,0 +1,47 @@ +### Features + +- **Server installation**: New NixOS version is used during server setup ([#415](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/415)) + - It is also possible to set a root SSH key during server setup. This feature can be activated in developer settings. +- **DNS management**: DNS records creation dynamically gets desired records from the server now ([#424](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/424), resolves [#265](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/265)) +- **UI**: Add the button to copy password on the new user creation screen ([#409](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/409), resolves [#299](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/299)) +- **UI**: Add animation to the recovery key screen ([#410](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/410), resolves [#164](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/164)) +- **Backups**: Bucket name now includes the date of creation ([#403](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/403), resolves [#263](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/263)) +- **UI**: Snapshots List page now shows the button to open the Jobs sheet ([#396](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/396), resolves [#290](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/290)) +- **Server installation**: Implement better domain ownership check during installation ([#394](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/394), resolves [#389](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/389)) +- **UI**: Implement flexible precision formatting for prices ([#387](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/387)) + +### Bug Fixes + +- **UI**: Domain name no longer overflows the screen ([#422](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/422), resolves [#408](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/408)) +- **UI**: Fix overflow of the filled buttons +- **Hetzner**: Filter away ARM architecture from available servers ([#404](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/404), resolves [#402](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/402)) +- **UI**: Add refresh indicator on the 'Devices' screen ([#398](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/398), resolves [#258](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/258) and [#163](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/163)) +- **GraphQL API**: Force DateTime to UTC when timezone naive ([#386](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/386), resolves [#385](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/385)) + +### Other + +- Updated the copyright year ([#417](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/417)) + - Happy new year! +- Upgrade to Flutter 3.16.1 +- Rename the Recovery flow button to prevent user confusion ([#399](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/399), resolves [#346](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/346)) +- **GraphQL API**: Remove and replace deprecated mutations ([#423](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/423), resolves [#418](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/418)) + +### Translation contributions + +* French + * smtg (12) + +* Spanish + * NaiJi ✨ (35) + +* German + * Marvin F (23) + * User 1234 (30) + +* Hebrew + * Yaron (578) + +* Russian + * def (4) + * NaiJi ✨ (9) + * Inex Code (10) diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index 7dff72f6..539f36a1 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -168,25 +168,19 @@ class DnsRecordsCubit Future fix() async { emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing)); + final List records = await api.getDnsRecords(); + + /// TODO: Error handling? final ServerDomain? domain = serverInstallationCubit.state.serverDomain; - final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4; await ProvidersController.currentDnsProvider!.removeDomainRecords( + records: records, domain: domain!, ); await ProvidersController.currentDnsProvider!.createDomainRecords( + records: records, domain: domain, - ip4: ipAddress, ); - final List records = await api.getDnsRecords(); - final DnsRecord? dkimRecord = extractDkimRecord(records); - if (dkimRecord != null) { - await ProvidersController.currentDnsProvider!.setDnsRecord( - dkimRecord, - domain, - ); - } - await load(); } } diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 0b5b6eed..95726fb4 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -264,12 +264,22 @@ class ServerInstallationCubit extends Cubit { final ServerHostingDetails serverDetails, ) async { await repository.saveServerDetails(serverDetails); + + /// TODO: Error handling? await ProvidersController.currentDnsProvider!.removeDomainRecords( - ip4: serverDetails.ip4, + records: getProjectDnsRecords( + state.serverDomain!.domainName, + serverDetails.ip4, + false, + ), domain: state.serverDomain!, ); await ProvidersController.currentDnsProvider!.createDomainRecords( - ip4: serverDetails.ip4, + records: getProjectDnsRecords( + state.serverDomain!.domainName, + serverDetails.ip4, + true, + ), domain: state.serverDomain!, ); diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index d69f5b89..36cb8743 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -558,14 +558,30 @@ class ServerInstallationRepository { } Future deleteServer(final ServerDomain serverDomain) async { + final ServerApi api = ServerApi(); + final dnsRecords = await api.getDnsRecords(); + final GenericResult removalResult = + await ProvidersController.currentDnsProvider!.removeDomainRecords( + domain: serverDomain, + records: dnsRecords, + ); + + if (!removalResult.success) { + getIt().showSnackBar( + 'modals.dns_removal_error'.tr(), + ); + return false; + } + final deletionResult = await ProvidersController.currentServerProvider!.deleteServer( serverDomain.domainName, ); if (!deletionResult.success) { - getIt() - .showSnackBar('modals.server_validators_error'.tr()); + getIt().showSnackBar( + 'modals.server_validators_error'.tr(), + ); return false; } @@ -576,13 +592,6 @@ class ServerInstallationRepository { await box.put(BNames.isLoading, false); await box.put(BNames.serverDetails, null); - final GenericResult removalResult = await ProvidersController - .currentDnsProvider! - .removeDomainRecords(domain: serverDomain); - - if (!removalResult.success) { - getIt().showSnackBar('modals.dns_removal_error'.tr()); - } return true; } diff --git a/lib/logic/models/json/dns_providers/cloudflare_dns_adapter.dart b/lib/logic/models/json/dns_providers/cloudflare_dns_adapter.dart index 3a22c89e..8046555c 100644 --- a/lib/logic/models/json/dns_providers/cloudflare_dns_adapter.dart +++ b/lib/logic/models/json/dns_providers/cloudflare_dns_adapter.dart @@ -4,14 +4,18 @@ CloudflareDnsRecord _fromDnsRecord( final DnsRecord dnsRecord, final String rootDomain, ) { + final String type = dnsRecord.type; String name = dnsRecord.name ?? ''; if (name != rootDomain && name != '@') { name = '$name.$rootDomain'; } + if (type == 'MX' && name == '@') { + name = rootDomain; + } return CloudflareDnsRecord( content: dnsRecord.content, name: name, - type: dnsRecord.type, + type: type, zoneName: rootDomain, id: null, ttl: dnsRecord.ttl, diff --git a/lib/logic/providers/dns_providers/cloudflare.dart b/lib/logic/providers/dns_providers/cloudflare.dart index d655cfbb..e40292a9 100644 --- a/lib/logic/providers/dns_providers/cloudflare.dart +++ b/lib/logic/providers/dns_providers/cloudflare.dart @@ -3,7 +3,6 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_providers/cloudflare_dns_info.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; -import 'package:selfprivacy/utils/network_utils.dart'; class ApiAdapter { ApiAdapter({ @@ -80,15 +79,14 @@ class CloudflareDnsProvider extends DnsProvider { @override Future> createDomainRecords({ + required final List records, required final ServerDomain domain, - final String? ip4, }) async { final syncZoneIdResult = await syncZoneId(domain.domainName); if (!syncZoneIdResult.success) { return syncZoneIdResult; } - final records = getProjectDnsRecords(domain.domainName, ip4); return _adapter.api().createMultipleDnsRecords( zoneId: _adapter.cachedZoneId, records: records @@ -102,16 +100,17 @@ class CloudflareDnsProvider extends DnsProvider { @override Future> removeDomainRecords({ + required final List records, required final ServerDomain domain, - final String? ip4, }) async { final syncZoneIdResult = await syncZoneId(domain.domainName); if (!syncZoneIdResult.success) { return syncZoneIdResult; } - final result = - await _adapter.api().getDnsRecords(zoneId: _adapter.cachedZoneId); + final result = await _adapter.api().getDnsRecords( + zoneId: _adapter.cachedZoneId, + ); if (result.data.isEmpty || !result.success) { return GenericResult( success: result.success, @@ -121,9 +120,29 @@ class CloudflareDnsProvider extends DnsProvider { ); } + final List selfprivacyRecords = records + .map( + (final record) => CloudflareDnsRecord.fromDnsRecord( + record, + domain.domainName, + ), + ) + .toList(); + + final List cloudflareRecords = result.data; + + /// Remove all records that do not match with SelfPrivacy + cloudflareRecords.removeWhere( + (final cloudflareRecord) => !selfprivacyRecords.any( + (final selfprivacyRecord) => + selfprivacyRecord.type == cloudflareRecord.type && + selfprivacyRecord.name == cloudflareRecord.name, + ), + ); + return _adapter.api().removeSimilarRecords( zoneId: _adapter.cachedZoneId, - records: result.data, + records: cloudflareRecords, ); } diff --git a/lib/logic/providers/dns_providers/desec.dart b/lib/logic/providers/dns_providers/desec.dart index ea139039..188045bf 100644 --- a/lib/logic/providers/dns_providers/desec.dart +++ b/lib/logic/providers/dns_providers/desec.dart @@ -3,7 +3,6 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_providers/desec_dns_info.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; -import 'package:selfprivacy/utils/network_utils.dart'; class ApiAdapter { ApiAdapter({final bool isWithToken = true}) @@ -75,16 +74,11 @@ class DesecDnsProvider extends DnsProvider { @override Future> createDomainRecords({ + required final List records, required final ServerDomain domain, - final String? ip4, }) async { - final List listDnsRecords = getProjectDnsRecords( - domain.domainName, - ip4, - ); - final List bulkRecords = []; - for (final DnsRecord record in listDnsRecords) { + for (final DnsRecord record in records) { bulkRecords.add(DesecDnsRecord.fromDnsRecord(record, domain.domainName)); } @@ -96,21 +90,19 @@ class DesecDnsProvider extends DnsProvider { @override Future> removeDomainRecords({ + required final List records, required final ServerDomain domain, - final String? ip4, }) async { - final List listDnsRecords = getProjectDnsRecords( - domain.domainName, - ip4, - ); - final List bulkRecords = []; - for (final DnsRecord record in listDnsRecords) { + for (final DnsRecord record in records) { final desecRecord = DesecDnsRecord.fromDnsRecord( record, domain.domainName, ); bulkRecords.add( + /// Yes, it looks weird, but exactly forcing 'records' field + /// to empty array signals deSEC to remove the DNS record completely + /// https://desec.readthedocs.io/en/latest/dns/rrsets.html#deleting-an-rrset DesecDnsRecord( subname: desecRecord.subname, type: desecRecord.type, @@ -119,14 +111,6 @@ class DesecDnsProvider extends DnsProvider { ), ); } - bulkRecords.add( - DesecDnsRecord( - subname: 'selector._domainkey', - type: 'TXT', - ttl: 18000, - records: [], - ), - ); return _adapter.api().removeSimilarRecords( domainName: domain.domainName, diff --git a/lib/logic/providers/dns_providers/digital_ocean_dns.dart b/lib/logic/providers/dns_providers/digital_ocean_dns.dart index e07b63b1..f111c5f3 100644 --- a/lib/logic/providers/dns_providers/digital_ocean_dns.dart +++ b/lib/logic/providers/dns_providers/digital_ocean_dns.dart @@ -3,7 +3,6 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_providers/digital_ocean_dns_info.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; -import 'package:selfprivacy/utils/network_utils.dart'; class ApiAdapter { ApiAdapter({final bool isWithToken = true}) @@ -75,15 +74,12 @@ class DigitalOceanDnsProvider extends DnsProvider { @override Future> createDomainRecords({ + required final List records, required final ServerDomain domain, - final String? ip4, }) async => _adapter.api().createMultipleDnsRecords( domainName: domain.domainName, - records: getProjectDnsRecords( - domain.domainName, - ip4, - ) + records: records .map( (final e) => DigitalOceanDnsRecord.fromDnsRecord(e, domain.domainName), @@ -93,8 +89,8 @@ class DigitalOceanDnsProvider extends DnsProvider { @override Future> removeDomainRecords({ + required final List records, required final ServerDomain domain, - final String? ip4, }) async { final result = await _adapter.api().getDnsRecords(domain.domainName); if (result.data.isEmpty || !result.success) { @@ -106,17 +102,29 @@ class DigitalOceanDnsProvider extends DnsProvider { ); } - const ignoreType = 'SOA'; - final List filteredRecords = []; - for (final record in result.data) { - if (record.type != ignoreType) { - filteredRecords.add(record); - } - } + final List selfprivacyRecords = records + .map( + (final record) => DigitalOceanDnsRecord.fromDnsRecord( + record, + domain.domainName, + ), + ) + .toList(); + + final List oceanRecords = result.data; + + /// Remove all records that do not match with SelfPrivacy + oceanRecords.removeWhere( + (final oceanRecord) => !selfprivacyRecords.any( + (final selfprivacyRecord) => + selfprivacyRecord.type == oceanRecord.type && + selfprivacyRecord.name == oceanRecord.name, + ), + ); return _adapter.api().removeSimilarRecords( domainName: domain.domainName, - records: filteredRecords, + records: oceanRecords, ); } diff --git a/lib/logic/providers/dns_providers/dns_provider.dart b/lib/logic/providers/dns_providers/dns_provider.dart index 98bd2199..406368b0 100644 --- a/lib/logic/providers/dns_providers/dns_provider.dart +++ b/lib/logic/providers/dns_providers/dns_provider.dart @@ -23,23 +23,21 @@ abstract class DnsProvider { /// Returns list of all available domain entries assigned to the account. Future>> domainList(); - /// Tries to create all main domain records needed - /// for SelfPrivacy to launch on requested domain by ip4. + /// Tries to create domain records + /// by our records list. /// /// Doesn't check for duplication, cleaning has /// to be done beforehand by [removeDomainRecords] Future> createDomainRecords({ + required final List records, required final ServerDomain domain, - final String? ip4, }); - /// Tries to remove all domain records of requested domain by ip4. - /// - /// Will remove all entries, including the ones - /// that weren't created by SelfPrivacy. + /// Tries to remove all records of requested + /// domain that match our records list. Future> removeDomainRecords({ + required final List records, required final ServerDomain domain, - final String? ip4, }); /// Returns list of all [DnsRecord] entries assigned to requested domain. diff --git a/lib/ui/pages/setup/initializing/domain_picker.dart b/lib/ui/pages/setup/initializing/domain_picker.dart index 1d64349c..69e11a8d 100644 --- a/lib/ui/pages/setup/initializing/domain_picker.dart +++ b/lib/ui/pages/setup/initializing/domain_picker.dart @@ -87,9 +87,13 @@ class _DomainPickerState extends State { }); }, ), - Text( - domain, - style: Theme.of(context).textTheme.bodyLarge, + Expanded( + child: Text( + domain, + style: Theme.of(context).textTheme.bodyLarge, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), ), ], ), @@ -118,12 +122,16 @@ class _DomainPickerState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - state.domain, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - textAlign: TextAlign.center, + Expanded( + child: Text( + state.domain, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), ), ], ), diff --git a/lib/utils/network_utils.dart b/lib/utils/network_utils.dart index bfd8a82d..c155d6a0 100644 --- a/lib/utils/network_utils.dart +++ b/lib/utils/network_utils.dart @@ -93,6 +93,7 @@ void launchURL(final url) async { List getProjectDnsRecords( final String? domainName, final String? ip4, + final bool isCreating, ) { final DnsRecord domainA = DnsRecord(type: 'A', name: domainName, content: ip4); @@ -121,6 +122,16 @@ List getProjectDnsRecords( ttl: 18000, ); + /// We never create this record! + /// This declaration is only for removal + /// as we need to compare by 'type' and 'name' + final DnsRecord txt3 = DnsRecord( + type: 'TXT', + name: 'selector._domainkey', + content: 'v=DKIM1; k=rsa; p=none', + ttl: 18000, + ); + return [ domainA, apiA, @@ -132,6 +143,7 @@ List getProjectDnsRecords( mx, txt1, txt2, + if (!isCreating) txt3, vpn, ]; } diff --git a/pubspec.yaml b/pubspec.yaml index 2ae559b0..12a071d1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: selfprivacy description: selfprivacy.org publish_to: 'none' -version: 0.9.1+19 +version: 0.10.0+20 environment: sdk: '>=3.2.1 <4.0.0'