diff --git a/assets/markdown/how_fix_domain_cloudflare-en.md b/assets/markdown/how_fix_domain_cloudflare-en.md new file mode 100644 index 00000000..7751cea0 --- /dev/null +++ b/assets/markdown/how_fix_domain_cloudflare-en.md @@ -0,0 +1,6 @@ +### How to point Name Servers for Cloudflare DNS +1. Visit the following [link](https://dash.cloudflare.com) and sign + into your Cloudflare account. +2. Enter DNS settings of your domain. +3. Copy your NS records and paste them into a Nameservers section of your domain registar settings. +4. For more specific instructions for each of commonly used registars, follow the Cloudflare [guide](https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/). diff --git a/assets/markdown/how_fix_domain_desec-en.md b/assets/markdown/how_fix_domain_desec-en.md new file mode 100644 index 00000000..9e12bd68 --- /dev/null +++ b/assets/markdown/how_fix_domain_desec-en.md @@ -0,0 +1,6 @@ +### How to point Name Servers for deSEC DNS +1. Visit the following [link](https://desec.io/domains) and sign + into your deSEC account. +2. Click on the "Setup instructions" icon on the right side of your domain card, in the "Actions" section. +3. Copy your NS records and paste them into a Nameservers section of your domain registar settings. +4. For more specific instructions follow the official deSEC guide listed on the page. diff --git a/assets/markdown/how_fix_domain_digital_ocean-en.md b/assets/markdown/how_fix_domain_digital_ocean-en.md new file mode 100644 index 00000000..6069a67e --- /dev/null +++ b/assets/markdown/how_fix_domain_digital_ocean-en.md @@ -0,0 +1,8 @@ +### How to point Name Servers for Digital Ocean DNS +1. Visit the following [link](https://cloud.digitalocean.com/) and sign + into your Digital Ocean account. +2. Enter the [Networking](https://cloud.digitalocean.com/networking/domains) tab from the menu bar on the left. +3. Make sure you are on the [Domain](https://cloud.digitalocean.com/networking/domains) section of the tab, which is the very first one. +4. Click on your domain card, the one you have selected for SelfPrivacy. +5. Copy your NS records and paste them into a Nameservers section of your domain registar settings. +6. For more specific instructions for each of commonly used registars, follow the Digital Ocean [guide](https://docs.digitalocean.com/products/networking/dns/getting-started/dns-registrars/). diff --git a/assets/translations/en.json b/assets/translations/en.json index 4dc053d6..7453c5dc 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -452,6 +452,7 @@ "server_rebooted": "Server rebooted. Waiting for the last verification…", "server_started": "Server started. It will be validated and rebooted now…", "server_created": "Server created. DNS checks and server boot in progress…", + "domain_critical_error": "We can't reach this domain! Tap to read more…", "until_the_next_check": "Until the next check: ", "check": "Check", "one_more_restart": "One more restart to apply your security certificates.", diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg index c9a747f5..c3224ac6 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg index 292aee31..decbeb1b 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg index bffdf10a..0d62ec8b 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg index 11477ad3..8ba6d1c6 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg index 11477ad3..15d43105 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.jpg new file mode 100644 index 00000000..b063caed Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.jpg new file mode 100644 index 00000000..cba85334 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.jpg differ diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index ca7e0a50..0c3bd893 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -63,8 +63,8 @@ class BackupsCubit extends ServerInstallationDependendCubit { .state.serverDomain!.domainName .replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); final int serverId = serverInstallationCubit.state.serverDetails!.id; - String bucketName = 'selfprivacy-$domain-$serverId'; - // If bucket name is too long, shorten it + String bucketName = + '${DateTime.now().millisecondsSinceEpoch}-$serverId-$domain'; if (bucketName.length > 49) { bucketName = bucketName.substring(0, 49); } diff --git a/lib/logic/cubit/devices/devices_cubit.dart b/lib/logic/cubit/devices/devices_cubit.dart index 5debf20e..7713254b 100644 --- a/lib/logic/cubit/devices/devices_cubit.dart +++ b/lib/logic/cubit/devices/devices_cubit.dart @@ -21,7 +21,7 @@ class ApiDevicesCubit } Future refresh() async { - emit(const ApiDevicesState([], LoadingStatus.refreshing)); + emit(ApiDevicesState([state.thisDevice], LoadingStatus.refreshing)); _refetch(); } diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 8884b472..f4cd8ae1 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -23,6 +23,7 @@ import 'package:selfprivacy/logic/models/server_type.dart'; import 'package:selfprivacy/logic/providers/provider_settings.dart'; import 'package:selfprivacy/logic/providers/providers_controller.dart'; import 'package:selfprivacy/ui/helpers/modals.dart'; +import 'package:selfprivacy/utils/network_utils.dart'; export 'package:provider/provider.dart'; @@ -321,13 +322,16 @@ class ServerInstallationCubit extends Cubit { final String ip4 = dataState.serverDetails!.ip4; final String domainName = dataState.serverDomain!.domainName; - final Map matches = await repository.isDnsAddressesMatch( + final Map matches = await validateDnsMatch( domainName, + ['api'], ip4, - dataState.dnsMatches ?? {}, ); - if (matches.values.every((final bool value) => value)) { + if (matches.values.every( + (final DnsRecordStatus value) => value == DnsRecordStatus.ok, + ) && + matches.values.isNotEmpty) { final ServerHostingDetails server = await repository.startServer( dataState.serverDetails!, ); diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 30bccfd5..d8f7050a 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -212,33 +212,6 @@ class ServerInstallationRepository { return domainResult.data.contains(domain); } - Future> isDnsAddressesMatch( - final String? domainName, - final String? ip4, - final Map skippedMatches, - ) async { - final Map matches = {}; - try { - await InternetAddress.lookup(domainName!).then( - (final records) { - for (final record in records) { - if (skippedMatches[record.host] ?? false) { - matches[record.host] = true; - continue; - } - if (record.address == ip4!) { - matches[record.host] = true; - } - } - }, - ); - } catch (e) { - print(e); - } - - return matches; - } - Future createDkimRecord(final ServerDomain domain) async { final ServerApi api = ServerApi(); diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart index 9abd5a21..e06f87c3 100644 --- a/lib/logic/cubit/server_installation/server_installation_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -145,7 +145,7 @@ class ServerInstallationNotFinished extends ServerInstallationState { super.installationDialoguePopUp, }); final bool isLoading; - final Map? dnsMatches; + final Map? dnsMatches; @override List get props => [ @@ -175,7 +175,7 @@ class ServerInstallationNotFinished extends ServerInstallationState { final bool? isServerResetedFirstTime, final bool? isServerResetedSecondTime, final bool? isLoading, - final Map? dnsMatches, + final Map? dnsMatches, final CallbackDialogueBranching? installationDialoguePopUp, }) => ServerInstallationNotFinished( diff --git a/lib/logic/models/json/hetzner_server_info.dart b/lib/logic/models/json/hetzner_server_info.dart index 6d51f2b8..122d0288 100644 --- a/lib/logic/models/json/hetzner_server_info.dart +++ b/lib/logic/models/json/hetzner_server_info.dart @@ -79,6 +79,7 @@ class HetznerServerTypeInfo { this.prices, this.name, this.description, + this.architecture, ); final int cores; final num memory; @@ -86,6 +87,7 @@ class HetznerServerTypeInfo { final String name; final String description; + final String architecture; final List prices; diff --git a/lib/logic/models/json/hetzner_server_info.g.dart b/lib/logic/models/json/hetzner_server_info.g.dart index b73a0a9d..27b94deb 100644 --- a/lib/logic/models/json/hetzner_server_info.g.dart +++ b/lib/logic/models/json/hetzner_server_info.g.dart @@ -83,6 +83,7 @@ HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson( .toList(), json['name'] as String, json['description'] as String, + json['architecture'] as String, ); Map _$HetznerServerTypeInfoToJson( @@ -93,6 +94,7 @@ Map _$HetznerServerTypeInfoToJson( 'disk': instance.disk, 'name': instance.name, 'description': instance.description, + 'architecture': instance.architecture, 'prices': instance.prices, }; diff --git a/lib/logic/providers/dns_providers/cloudflare.dart b/lib/logic/providers/dns_providers/cloudflare.dart index b1f37804..4ef662eb 100644 --- a/lib/logic/providers/dns_providers/cloudflare.dart +++ b/lib/logic/providers/dns_providers/cloudflare.dart @@ -39,6 +39,9 @@ class CloudflareDnsProvider extends DnsProvider { @override DnsProviderType get type => DnsProviderType.cloudflare; + @override + String get howToRegistar => 'how_fix_domain_cloudflare'; + @override Future> tryInitApiByToken(final String token) async { final api = _adapter.api(getInitialized: false); diff --git a/lib/logic/providers/dns_providers/desec.dart b/lib/logic/providers/dns_providers/desec.dart index 727508f6..82dca964 100644 --- a/lib/logic/providers/dns_providers/desec.dart +++ b/lib/logic/providers/dns_providers/desec.dart @@ -34,6 +34,9 @@ class DesecDnsProvider extends DnsProvider { @override DnsProviderType get type => DnsProviderType.desec; + @override + String get howToRegistar => 'how_fix_domain_desec'; + @override Future> tryInitApiByToken(final String token) async { final api = _adapter.api(getInitialized: false); diff --git a/lib/logic/providers/dns_providers/digital_ocean_dns.dart b/lib/logic/providers/dns_providers/digital_ocean_dns.dart index 776c37e5..f0ef28b6 100644 --- a/lib/logic/providers/dns_providers/digital_ocean_dns.dart +++ b/lib/logic/providers/dns_providers/digital_ocean_dns.dart @@ -34,6 +34,9 @@ class DigitalOceanDnsProvider extends DnsProvider { @override DnsProviderType get type => DnsProviderType.digitalOcean; + @override + String get howToRegistar => 'how_fix_domain_digital_ocean'; + @override Future> tryInitApiByToken(final String token) async { final api = _adapter.api(getInitialized: false); diff --git a/lib/logic/providers/dns_providers/dns_provider.dart b/lib/logic/providers/dns_providers/dns_provider.dart index 9ff6f611..a3ecdd51 100644 --- a/lib/logic/providers/dns_providers/dns_provider.dart +++ b/lib/logic/providers/dns_providers/dns_provider.dart @@ -9,6 +9,10 @@ abstract class DnsProvider { /// provider implements [DnsProvider] interface. DnsProviderType get type; + /// Returns a full url to a guide on how to setup + /// DNS provider nameservers + String get howToRegistar; + /// Tries to access an account linked to the provided token. /// /// To generate a token for your account follow instructions of your diff --git a/lib/logic/providers/server_providers/hetzner.dart b/lib/logic/providers/server_providers/hetzner.dart index ff8e04c8..560a0342 100644 --- a/lib/logic/providers/server_providers/hetzner.dart +++ b/lib/logic/providers/server_providers/hetzner.dart @@ -482,6 +482,9 @@ class HetznerServerProvider extends ServerProvider { final rawTypes = result.data; for (final rawType in rawTypes) { + if (rawType.architecture == 'arm') { + continue; + } for (final rawPrice in rawType.prices) { if (rawPrice.location == location.identifier) { types.add( diff --git a/lib/ui/pages/devices/devices.dart b/lib/ui/pages/devices/devices.dart index 52fffdbe..91fa0d38 100644 --- a/lib/ui/pages/devices/devices.dart +++ b/lib/ui/pages/devices/devices.dart @@ -91,6 +91,12 @@ class _DevicesInfo extends StatelessWidget { color: Theme.of(context).colorScheme.secondary, ), ), + if (devicesStatus.status == LoadingStatus.refreshing) ...[ + const Center( + heightFactor: 4, + child: CircularProgressIndicator(), + ), + ], ...devicesStatus.otherDevices .map((final device) => _DeviceTile(device: device)), ], diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 0e96d340..99821373 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -44,10 +44,8 @@ class MorePage extends StatelessWidget { title: 'storage.start_migration_button'.tr(), iconData: Icons.drive_file_move_outline, goTo: () => ServicesMigrationRoute( - diskStatus: context - .watch() - .state - .diskStatus, + diskStatus: + context.read().state.diskStatus, services: context .read() .state diff --git a/lib/ui/pages/setup/initializing/broken_domain_outlined_card.dart b/lib/ui/pages/setup/initializing/broken_domain_outlined_card.dart new file mode 100644 index 00000000..97e18bfe --- /dev/null +++ b/lib/ui/pages/setup/initializing/broken_domain_outlined_card.dart @@ -0,0 +1,61 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart'; +import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart'; +import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; +import 'package:selfprivacy/ui/components/cards/filled_card.dart'; + +class BrokenDomainOutlinedCard extends StatelessWidget { + const BrokenDomainOutlinedCard({ + required this.domain, + required this.dnsProvider, + super.key, + }); + + final String domain; + final DnsProvider dnsProvider; + + @override + Widget build(final BuildContext context) => SizedBox( + width: double.infinity, + child: FilledCard( + error: true, + child: InkResponse( + highlightShape: BoxShape.rectangle, + onTap: () => context.read().showArticle( + article: dnsProvider.howToRegistar, + context: context, + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + size: 24.0, + ), + const SizedBox(width: 12.0), + Flexible( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + domain, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge, + ), + Text('initializing.domain_critical_error'.tr()), + ], + ), + ), + ], + ), + ), + ), + ), + ); +} diff --git a/lib/ui/pages/setup/initializing/initializing.dart b/lib/ui/pages/setup/initializing/initializing.dart index 841c1955..02c29269 100644 --- a/lib/ui/pages/setup/initializing/initializing.dart +++ b/lib/ui/pages/setup/initializing/initializing.dart @@ -10,6 +10,7 @@ import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_fo import 'package:selfprivacy/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart'; +import 'package:selfprivacy/logic/providers/providers_controller.dart'; import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; import 'package:selfprivacy/ui/components/buttons/outlined_button.dart'; @@ -17,6 +18,7 @@ import 'package:selfprivacy/ui/components/drawers/progress_drawer.dart'; import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; import 'package:selfprivacy/ui/components/drawers/support_drawer.dart'; import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart'; +import 'package:selfprivacy/ui/pages/setup/initializing/broken_domain_outlined_card.dart'; import 'package:selfprivacy/ui/pages/setup/initializing/dns_provider_picker.dart'; import 'package:selfprivacy/ui/pages/setup/initializing/domain_picker.dart'; import 'package:selfprivacy/ui/pages/setup/initializing/server_provider_picker.dart'; @@ -24,6 +26,7 @@ import 'package:selfprivacy/ui/pages/setup/initializing/server_type_picker.dart' import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart'; import 'package:selfprivacy/ui/router/router.dart'; import 'package:selfprivacy/utils/breakpoints.dart'; +import 'package:selfprivacy/utils/network_utils.dart'; @RoutePage() class InitializingPage extends StatelessWidget { @@ -493,12 +496,17 @@ class InitializingPage extends StatelessWidget { Column( children: state.dnsMatches!.entries.map((final entry) { final String domain = entry.key; - final bool isCorrect = entry.value; + if (entry.value == DnsRecordStatus.nonexistent) { + return BrokenDomainOutlinedCard( + domain: domain, + dnsProvider: ProvidersController.currentDnsProvider!, + ); + } return Row( children: [ - if (isCorrect) + if (entry.value == DnsRecordStatus.ok) const Icon(Icons.check, color: Colors.green), - if (!isCorrect) + if (entry.value == DnsRecordStatus.waiting) const Icon(Icons.schedule, color: Colors.amber), const SizedBox(width: 10), Text(domain), diff --git a/lib/utils/network_utils.dart b/lib/utils/network_utils.dart index be041fb1..bfd8a82d 100644 --- a/lib/utils/network_utils.dart +++ b/lib/utils/network_utils.dart @@ -1,6 +1,54 @@ +import 'dart:io'; + import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:url_launcher/url_launcher.dart'; +enum DnsRecordStatus { ok, waiting, nonexistent } + +/// Check if DNS records were recognized. +/// +/// Return pairs of full record name matched to its status. +/// +/// If no record found, return just one pair of [domain] matched to critical non-existent status. +/// +/// - [domain] - full domain delegated to SelfPrivacy (e.g. reimu.love) +/// - [subdomains] - list of all subdomains we want to validate recods of (e.g. api, cloud...) +/// - [ip4] - IP address of our server we want to validate DNS records by (e.g. 127.0.0.1) +Future> validateDnsMatch( + final String domain, + final List subdomains, + final String ip4, +) async { + final Map matches = {}; + + Future lookup(final String address) async { + await InternetAddress.lookup(address).then( + (final records) { + for (final record in records) { + final bool isIpCorrect = record.address == ip4; + matches[record.host] = + isIpCorrect ? DnsRecordStatus.ok : DnsRecordStatus.waiting; + } + }, + ); + } + + try { + await lookup(domain); + for (final subdomain in subdomains) { + await lookup('$subdomain.$domain'); + } + } catch (e) { + print(e); + } + + if (matches.isEmpty) { + matches[domain] = DnsRecordStatus.nonexistent; + } + + return matches; +} + DnsRecord? extractDkimRecord(final List records) { DnsRecord? dkimRecord;