diff --git a/assets/images/logos/cloudflare.svg b/assets/images/logos/cloudflare.svg index a8cbf3bc..03a60465 100644 --- a/assets/images/logos/cloudflare.svg +++ b/assets/images/logos/cloudflare.svg @@ -1,10 +1 @@ - - - - - - - - - - + diff --git a/assets/images/logos/desec.svg b/assets/images/logos/desec.svg new file mode 100644 index 00000000..cb54b268 --- /dev/null +++ b/assets/images/logos/desec.svg @@ -0,0 +1,89 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/assets/markdown/how_desec-en.md b/assets/markdown/how_desec-en.md new file mode 100644 index 00000000..c86d3855 --- /dev/null +++ b/assets/markdown/how_desec-en.md @@ -0,0 +1,9 @@ +### How to get deSEC API Token +1. Log in at: https://desec.io/login +2. Go to **Domains** page at: https://desec.io/domains +3. Go to **Token management** tab. +4. Click on the round "plus" button in the upper right corner. +5. **"Generate New Token"** dialogue must be displayed. Enter any **Token name** you wish. *Advanced settings* are not required, so do not touch anything there. +6. Click on **Save**. +7. Make sure you **save** the token's **secret value** as it will only be displayed once. +8. Now you can safely **close** the dialogue. \ No newline at end of file diff --git a/assets/markdown/how_desec-ru.md b/assets/markdown/how_desec-ru.md new file mode 100644 index 00000000..a93acc77 --- /dev/null +++ b/assets/markdown/how_desec-ru.md @@ -0,0 +1,9 @@ +### Как получить deSEC API Токен +1. Авторизуемся в deSEC: https://desec.io/login +2. Переходим на страницу **Domains** по ссылке: https://desec.io/domains +3. Переходим на вкладку **Token management**. +4. Нажимаем на большую кнопку с плюсом в правом верхнем углу страницы. +5. Должен был появиться **"Generate New Token"** диалог. Вводим любое имя токена в **Token name**. *Advanced settings* необязательны, так что ничего там не трогаем. +6. Кликаем **Save**. +7. Обязательно сохраняем "**secret value**" ключ токена, потому что он отображается исключительно один раз. +8. Теперь спокойно закрываем диалог, нажав **close**. \ No newline at end of file diff --git a/assets/translations/en.json b/assets/translations/en.json index 327229bb..0ac7562e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -323,7 +323,7 @@ "manage_domain_dns": "To manage your domain's DNS", "use_this_domain": "Use this domain?", "use_this_domain_text": "The token you provided gives access to the following domain", - "cloudflare_api_token": "CloudFlare API Token", + "cloudflare_api_token": "DNS Provider API Token", "connect_backblaze_storage": "Connect Backblaze storage", "no_connected_domains": "No connected domains at the moment", "loading_domain_list": "Loading domain list", @@ -394,8 +394,8 @@ "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_cloudflare": "Connect to your DNS Provider", + "confirm_cloudflare_description": "Enter a token of your DNS Provider with access to {}:", "confirm_backblaze": "Connect to Backblaze", "confirm_backblaze_description": "Enter a Backblaze token with access to backup storage:" }, diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 450be193..8fdf0054 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -286,8 +286,8 @@ "select_provider_price_text_do": "$17 в месяц за небольшой сервер и 50GB места на диске", "select_provider_payment_title": "Методы оплаты", "select_provider_payment_text_hetzner": "Банковские карты, SWIFT, SEPA, PayPal", - "select_provider_payment_text_do": "Банковские карты, Google Pay, PayPal", "select_provider_payment_text_cloudflare": "Банковские карты", + "select_provider_payment_text_do": "Банковские карты, Google Pay, PayPal", "select_provider_email_notice": "Хостинг электронной почты недоступен для новых клиентов. Разблокировать можно будет после первой оплаты.", "select_provider_site_button": "Посетить сайт", "connect_to_server_provider": "Авторизоваться в ", @@ -315,7 +315,7 @@ "manage_domain_dns": "Для управления DNS вашего домена", "use_this_domain": "Используем этот домен?", "use_this_domain_text": "Указанный вами токен даёт контроль над этим доменом", - "cloudflare_api_token": "CloudFlare API ключ", + "cloudflare_api_token": "API ключ DNS провайдера", "connect_backblaze_storage": "Подключите облачное хранилище Backblaze", "no_connected_domains": "На данный момент подлюченных доменов нет", "loading_domain_list": "Загружаем список доменов", @@ -371,8 +371,8 @@ "modal_confirmation_dns_invalid": "Обратный DNS указывает на другой домен", "modal_confirmation_ip_valid": "IP совпадает с указанным в DNS записи", "modal_confirmation_ip_invalid": "IP не совпадает с указанным в DNS записи", - "confirm_cloudflare": "Подключение к Cloudflare", - "confirm_cloudflare_description": "Введите токен Cloudflare, который имеет права на {}:", + "confirm_cloudflare": "Подключение к DNS Провайдеру", + "confirm_cloudflare_description": "Введите токен DNS Провайдера, который имеет права на {}:", "confirm_backblaze_description": "Введите токен Backblaze, который имеет права на хранилище резервных копий:", "confirm_backblaze": "Подключение к Backblaze", "server_provider_connected": "Подключение к вашему серверному провайдеру", @@ -478,4 +478,4 @@ "length_not_equal": "Длина строки [], должна быть равна {}", "length_longer": "Длина строки [], должна быть меньше либо равна {}" } -} +} \ No newline at end of file diff --git a/lib/logic/api_maps/graphql_maps/api_map.dart b/lib/logic/api_maps/graphql_maps/api_map.dart index a633866e..34e39b7a 100644 --- a/lib/logic/api_maps/graphql_maps/api_map.dart +++ b/lib/logic/api_maps/graphql_maps/api_map.dart @@ -56,7 +56,7 @@ class ResponseLoggingParser extends ResponseParser { abstract class ApiMap { Future getClient() async { IOClient? ioClient; - if (StagingOptions.stagingAcme) { + if (StagingOptions.stagingAcme || !StagingOptions.verifyCertificate) { final HttpClient httpClient = HttpClient(); httpClient.badCertificateCallback = ( final cert, diff --git a/lib/logic/api_maps/graphql_maps/schema/schema.graphql b/lib/logic/api_maps/graphql_maps/schema/schema.graphql index 81c703d1..bd05dfb5 100644 --- a/lib/logic/api_maps/graphql_maps/schema/schema.graphql +++ b/lib/logic/api_maps/graphql_maps/schema/schema.graphql @@ -76,7 +76,8 @@ type DeviceApiTokenMutationReturn implements MutationReturnInterface { enum DnsProvider { CLOUDFLARE, - DIGITALOCEAN + DIGITALOCEAN, + DESEC } type DnsRecord { diff --git a/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart index a8318d9f..b0d0341d 100644 --- a/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart @@ -1096,7 +1096,7 @@ class _CopyWithStubImpl$Input$UserMutationInput _res; } -enum Enum$DnsProvider { CLOUDFLARE, DIGITALOCEAN, $unknown } +enum Enum$DnsProvider { CLOUDFLARE, DIGITALOCEAN, DESEC, $unknown } String toJson$Enum$DnsProvider(Enum$DnsProvider e) { switch (e) { @@ -1104,6 +1104,8 @@ String toJson$Enum$DnsProvider(Enum$DnsProvider e) { return r'CLOUDFLARE'; case Enum$DnsProvider.DIGITALOCEAN: return r'DIGITALOCEAN'; + case Enum$DnsProvider.DESEC: + return r'DESEC'; case Enum$DnsProvider.$unknown: return r'$unknown'; } @@ -1115,6 +1117,8 @@ Enum$DnsProvider fromJson$Enum$DnsProvider(String value) { return Enum$DnsProvider.CLOUDFLARE; case r'DIGITALOCEAN': return Enum$DnsProvider.DIGITALOCEAN; + case r'DESEC': + return Enum$DnsProvider.DESEC; default: return Enum$DnsProvider.$unknown; } diff --git a/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart b/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart index d9ac5c6c..5df05a48 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart @@ -189,88 +189,6 @@ class CloudflareApi extends DnsProviderApi { return allRecords; } - @override - List getDesiredDnsRecords({ - final String? domainName, - final String? ipAddress, - final String? dkimPublicKey, - }) { - if (domainName == null || ipAddress == null) { - return []; - } - return [ - DesiredDnsRecord( - name: domainName, - content: ipAddress, - description: 'record.root', - ), - DesiredDnsRecord( - name: 'api.$domainName', - content: ipAddress, - description: 'record.api', - ), - DesiredDnsRecord( - name: 'cloud.$domainName', - content: ipAddress, - description: 'record.cloud', - ), - DesiredDnsRecord( - name: 'git.$domainName', - content: ipAddress, - description: 'record.git', - ), - DesiredDnsRecord( - name: 'meet.$domainName', - content: ipAddress, - description: 'record.meet', - ), - DesiredDnsRecord( - name: 'social.$domainName', - content: ipAddress, - description: 'record.social', - ), - DesiredDnsRecord( - name: 'password.$domainName', - content: ipAddress, - description: 'record.password', - ), - DesiredDnsRecord( - name: 'vpn.$domainName', - content: ipAddress, - description: 'record.vpn', - ), - DesiredDnsRecord( - name: domainName, - content: domainName, - description: 'record.mx', - type: 'MX', - category: DnsRecordsCategory.email, - ), - DesiredDnsRecord( - name: '_dmarc.$domainName', - content: 'v=DMARC1; p=none', - description: 'record.dmarc', - type: 'TXT', - category: DnsRecordsCategory.email, - ), - DesiredDnsRecord( - name: domainName, - content: 'v=spf1 a mx ip4:$ipAddress -all', - description: 'record.spf', - type: 'TXT', - category: DnsRecordsCategory.email, - ), - if (dkimPublicKey != null) - DesiredDnsRecord( - name: 'selector._domainkey.$domainName', - content: dkimPublicKey, - description: 'record.dkim', - type: 'TXT', - category: DnsRecordsCategory.email, - ), - ]; - } - @override Future> createMultipleDnsRecords({ required final ServerDomain domain, @@ -353,4 +271,147 @@ class CloudflareApi extends DnsProviderApi { return domains; } + + @override + Future>> validateDnsRecords( + final ServerDomain domain, + final String ip4, + final String dkimPublicKey, + ) async { + final List records = await getDnsRecords(domain: domain); + final List foundRecords = []; + try { + final List desiredRecords = + getDesiredDnsRecords(domain.domainName, ip4, dkimPublicKey); + for (final DesiredDnsRecord record in desiredRecords) { + if (record.description == 'record.dkim') { + final DnsRecord foundRecord = records.firstWhere( + (final r) => (r.name == record.name) && r.type == record.type, + orElse: () => DnsRecord( + name: record.name, + type: record.type, + content: '', + ttl: 800, + proxied: false, + ), + ); + // remove all spaces and tabulators from + // the foundRecord.content and the record.content + // to compare them + final String? foundContent = + foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); + final String content = record.content.replaceAll(RegExp(r'\s+'), ''); + if (foundContent == content) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } else { + if (records.any( + (final r) => + (r.name == record.name) && + r.type == record.type && + r.content == record.content, + )) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } + } + } catch (e) { + print(e); + return GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } + return GenericResult( + data: foundRecords, + success: true, + ); + } + + @override + List getDesiredDnsRecords( + final String? domainName, + final String? ip4, + final String? dkimPublicKey, + ) { + if (domainName == null || ip4 == null) { + return []; + } + return [ + DesiredDnsRecord( + name: domainName, + content: ip4, + description: 'record.root', + ), + DesiredDnsRecord( + name: 'api.$domainName', + content: ip4, + description: 'record.api', + ), + DesiredDnsRecord( + name: 'cloud.$domainName', + content: ip4, + description: 'record.cloud', + ), + DesiredDnsRecord( + name: 'git.$domainName', + content: ip4, + description: 'record.git', + ), + DesiredDnsRecord( + name: 'meet.$domainName', + content: ip4, + description: 'record.meet', + ), + DesiredDnsRecord( + name: 'social.$domainName', + content: ip4, + description: 'record.social', + ), + DesiredDnsRecord( + name: 'password.$domainName', + content: ip4, + description: 'record.password', + ), + DesiredDnsRecord( + name: 'vpn.$domainName', + content: ip4, + description: 'record.vpn', + ), + DesiredDnsRecord( + name: domainName, + content: domainName, + description: 'record.mx', + type: 'MX', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: '_dmarc.$domainName', + content: 'v=DMARC1; p=none', + description: 'record.dmarc', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: domainName, + content: 'v=spf1 a mx ip4:$ip4 -all', + description: 'record.spf', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + if (dkimPublicKey != null) + DesiredDnsRecord( + name: 'selector._domainkey.$domainName', + content: dkimPublicKey, + description: 'record.dkim', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + ]; + } } diff --git a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart new file mode 100644 index 00000000..8298b08d --- /dev/null +++ b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart @@ -0,0 +1,475 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/json/dns_records.dart'; + +class DesecApi extends DnsProviderApi { + DesecApi({ + this.hasLogger = false, + this.isWithToken = true, + this.customToken, + }); + @override + final bool hasLogger; + @override + final bool isWithToken; + + final String? customToken; + + @override + RegExp getApiTokenValidation() => + RegExp(r'\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); + + @override + BaseOptions get options { + final BaseOptions options = BaseOptions(baseUrl: rootAddress); + if (isWithToken) { + final String? token = getIt().dnsProviderKey; + assert(token != null); + options.headers = {'Authorization': 'Token $token'}; + } + + if (customToken != null) { + options.headers = {'Authorization': 'Token $customToken'}; + } + + if (validateStatus != null) { + options.validateStatus = validateStatus!; + } + return options; + } + + @override + String rootAddress = 'https://desec.io/api/v1/domains/'; + + @override + Future> isApiTokenValid(final String token) async { + bool isValid = false; + Response? response; + String message = ''; + final Dio client = await getClient(); + try { + response = await client.get( + '', + options: Options( + followRedirects: false, + validateStatus: (final status) => + status != null && (status >= 200 || status == 401), + headers: {'Authorization': 'Token $token'}, + ), + ); + await Future.delayed(const Duration(seconds: 1)); + } catch (e) { + print(e); + isValid = false; + message = e.toString(); + } finally { + close(client); + } + + if (response == null) { + return GenericResult( + data: isValid, + success: false, + message: message, + ); + } + + if (response.statusCode == HttpStatus.ok) { + isValid = true; + } else if (response.statusCode == HttpStatus.unauthorized) { + isValid = false; + } else { + throw Exception('code: ${response.statusCode}'); + } + + return GenericResult( + data: isValid, + success: true, + message: response.statusMessage, + ); + } + + @override + Future getZoneId(final String domain) async => domain; + + @override + Future> removeSimilarRecords({ + required final ServerDomain domain, + final String? ip4, + }) async { + final String domainName = domain.domainName; + final String url = '/$domainName/rrsets/'; + final List listDnsRecords = projectDnsRecords(domainName, ip4); + + final Dio client = await getClient(); + try { + final List bulkRecords = []; + for (final DnsRecord record in listDnsRecords) { + bulkRecords.add( + { + 'subname': record.name, + 'type': record.type, + 'ttl': record.ttl, + 'records': [], + }, + ); + } + bulkRecords.add( + { + 'subname': 'selector._domainkey', + 'type': 'TXT', + 'ttl': 18000, + 'records': [], + }, + ); + await client.put(url, data: bulkRecords); + await Future.delayed(const Duration(seconds: 1)); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + @override + Future> getDnsRecords({ + required final ServerDomain domain, + }) async { + Response response; + final String domainName = domain.domainName; + final List allRecords = []; + + final String url = '/$domainName/rrsets/'; + + final Dio client = await getClient(); + try { + response = await client.get(url); + await Future.delayed(const Duration(seconds: 1)); + final List records = response.data; + + for (final record in records) { + final String? content = (record['records'] is List) + ? record['records'][0] + : record['records']; + allRecords.add( + DnsRecord( + name: record['subname'], + type: record['type'], + content: content, + ttl: record['ttl'], + ), + ); + } + } catch (e) { + print(e); + } finally { + close(client); + } + + return allRecords; + } + + @override + Future> createMultipleDnsRecords({ + required final ServerDomain domain, + final String? ip4, + }) async { + final String domainName = domain.domainName; + final List listDnsRecords = projectDnsRecords(domainName, ip4); + + final Dio client = await getClient(); + try { + final List bulkRecords = []; + for (final DnsRecord record in listDnsRecords) { + bulkRecords.add( + { + 'subname': record.name, + 'type': record.type, + 'ttl': record.ttl, + 'records': [extractContent(record)], + }, + ); + } + await client.post( + '/$domainName/rrsets/', + data: bulkRecords, + ); + await Future.delayed(const Duration(seconds: 1)); + } on DioError catch (e) { + print(e.message); + rethrow; + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + List projectDnsRecords( + final String? domainName, + final String? ip4, + ) { + final DnsRecord domainA = DnsRecord(type: 'A', name: '', content: ip4); + + final DnsRecord mx = + DnsRecord(type: 'MX', name: '', content: '10 $domainName.'); + final DnsRecord apiA = DnsRecord(type: 'A', name: 'api', content: ip4); + final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); + final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4); + final DnsRecord meetA = DnsRecord(type: 'A', name: 'meet', content: ip4); + final DnsRecord passwordA = + DnsRecord(type: 'A', name: 'password', content: ip4); + final DnsRecord socialA = + DnsRecord(type: 'A', name: 'social', content: ip4); + final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); + + final DnsRecord txt1 = DnsRecord( + type: 'TXT', + name: '_dmarc', + content: '"v=DMARC1; p=none"', + ttl: 18000, + ); + + final DnsRecord txt2 = DnsRecord( + type: 'TXT', + name: '', + content: '"v=spf1 a mx ip4:$ip4 -all"', + ttl: 18000, + ); + + return [ + domainA, + apiA, + cloudA, + gitA, + meetA, + passwordA, + socialA, + mx, + txt1, + txt2, + vpn + ]; + } + + String? extractContent(final DnsRecord record) { + String? content = record.content; + if (record.type == 'TXT' && content != null && !content.startsWith('"')) { + content = '"$content"'; + } + + return content; + } + + @override + Future setDnsRecord( + final DnsRecord record, + final ServerDomain domain, + ) async { + final String url = '/${domain.domainName}/rrsets/'; + + final Dio client = await getClient(); + try { + await client.post( + url, + data: { + 'subname': record.name, + 'type': record.type, + 'ttl': record.ttl, + 'records': [extractContent(record)], + }, + ); + await Future.delayed(const Duration(seconds: 1)); + } catch (e) { + print(e); + } finally { + close(client); + } + } + + @override + Future> domainList() async { + List domains = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get( + '', + ); + await Future.delayed(const Duration(seconds: 1)); + domains = response.data + .map((final el) => el['name'] as String) + .toList(); + } catch (e) { + print(e); + } finally { + close(client); + } + + return domains; + } + + @override + Future>> validateDnsRecords( + final ServerDomain domain, + final String ip4, + final String dkimPublicKey, + ) async { + final List records = await getDnsRecords(domain: domain); + final List foundRecords = []; + try { + final List desiredRecords = + getDesiredDnsRecords(domain.domainName, ip4, dkimPublicKey); + for (final DesiredDnsRecord record in desiredRecords) { + if (record.description == 'record.dkim') { + final DnsRecord foundRecord = records.firstWhere( + (final r) => + ('${r.name}.${domain.domainName}' == record.name) && + r.type == record.type, + orElse: () => DnsRecord( + name: record.name, + type: record.type, + content: '', + ttl: 800, + proxied: false, + ), + ); + // remove all spaces and tabulators from + // the foundRecord.content and the record.content + // to compare them + final String? foundContent = + foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); + final String content = record.content.replaceAll(RegExp(r'\s+'), ''); + if (foundContent == content) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } else { + if (records.any( + (final r) => + ('${r.name}.${domain.domainName}' == record.name || + record.name == '') && + r.type == record.type && + r.content == record.content, + )) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } + } + } catch (e) { + print(e); + return GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } + return GenericResult( + data: foundRecords, + success: true, + ); + } + + @override + List getDesiredDnsRecords( + final String? domainName, + final String? ip4, + final String? dkimPublicKey, + ) { + if (domainName == null || ip4 == null) { + return []; + } + return [ + DesiredDnsRecord( + name: '', + content: ip4, + description: 'record.root', + ), + DesiredDnsRecord( + name: 'api.$domainName', + content: ip4, + description: 'record.api', + ), + DesiredDnsRecord( + name: 'cloud.$domainName', + content: ip4, + description: 'record.cloud', + ), + DesiredDnsRecord( + name: 'git.$domainName', + content: ip4, + description: 'record.git', + ), + DesiredDnsRecord( + name: 'meet.$domainName', + content: ip4, + description: 'record.meet', + ), + DesiredDnsRecord( + name: 'social.$domainName', + content: ip4, + description: 'record.social', + ), + DesiredDnsRecord( + name: 'password.$domainName', + content: ip4, + description: 'record.password', + ), + DesiredDnsRecord( + name: 'vpn.$domainName', + content: ip4, + description: 'record.vpn', + ), + DesiredDnsRecord( + name: '', + content: '10 $domainName.', + description: 'record.mx', + type: 'MX', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: '_dmarc.$domainName', + content: '"v=DMARC1; p=none"', + description: 'record.dmarc', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: '', + content: '"v=spf1 a mx ip4:$ip4 -all"', + description: 'record.spf', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + if (dkimPublicKey != null) + DesiredDnsRecord( + name: 'selector._domainkey.$domainName', + content: '"$dkimPublicKey"', + description: 'record.dkim', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + ]; + } +} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_factory.dart b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_factory.dart new file mode 100644 index 00000000..6c10259b --- /dev/null +++ b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_factory.dart @@ -0,0 +1,16 @@ +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desec/desec.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart'; + +class DesecApiFactory extends DnsProviderApiFactory { + @override + DnsProviderApi getDnsProvider({ + final DnsProviderApiSettings settings = const DnsProviderApiSettings(), + }) => + DesecApi( + hasLogger: settings.hasLogger, + isWithToken: settings.isWithToken, + customToken: settings.customToken, + ); +} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns.dart b/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns.dart index fd2b2e02..9a33bdfb 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns.dart @@ -171,61 +171,67 @@ class DigitalOceanDnsApi extends DnsProviderApi { return allRecords; } + Future>> validateDnsRecords( + final ServerDomain domain, + final String ip4, + final String dkimPublicKey, + ); + @override - List getDesiredDnsRecords({ + List getDesiredDnsRecords( final String? domainName, - final String? ipAddress, + final String? ip4, final String? dkimPublicKey, - }) { - if (domainName == null || ipAddress == null) { + ) { + if (domainName == null || ip4 == null) { return []; } return [ DesiredDnsRecord( name: '@', - content: ipAddress, + content: ip4, description: 'record.root', displayName: domainName, ), DesiredDnsRecord( name: 'api', - content: ipAddress, + content: ip4, description: 'record.api', displayName: 'api.$domainName', ), DesiredDnsRecord( name: 'cloud', - content: ipAddress, + content: ip4, description: 'record.cloud', displayName: 'cloud.$domainName', ), DesiredDnsRecord( name: 'git', - content: ipAddress, + content: ip4, description: 'record.git', displayName: 'git.$domainName', ), DesiredDnsRecord( name: 'meet', - content: ipAddress, + content: ip4, description: 'record.meet', displayName: 'meet.$domainName', ), DesiredDnsRecord( name: 'social', - content: ipAddress, + content: ip4, description: 'record.social', displayName: 'social.$domainName', ), DesiredDnsRecord( name: 'password', - content: ipAddress, + content: ip4, description: 'record.password', displayName: 'password.$domainName', ), DesiredDnsRecord( name: 'vpn', - content: ipAddress, + content: ip4, description: 'record.vpn', displayName: 'vpn.$domainName', ), @@ -245,7 +251,7 @@ class DigitalOceanDnsApi extends DnsProviderApi { ), DesiredDnsRecord( name: '@', - content: 'v=spf1 a mx ip4:$ipAddress -all', + content: 'v=spf1 a mx ip4:$ip4 -all', description: 'record.spf', type: 'TXT', category: DnsRecordsCategory.email, diff --git a/lib/logic/api_maps/rest_maps/dns_providers/dns_provider.dart b/lib/logic/api_maps/rest_maps/dns_providers/dns_provider.dart index 299928b0..af9d3e6b 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/dns_provider.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/dns_provider.dart @@ -3,6 +3,7 @@ import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; +import 'package:selfprivacy/utils/network_utils.dart'; export 'package:selfprivacy/logic/api_maps/generic_result.dart'; export 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart'; @@ -16,11 +17,7 @@ abstract class DnsProviderApi extends ApiMap { Future> getDnsRecords({ required final ServerDomain domain, }); - List getDesiredDnsRecords({ - final String? domainName, - final String? ipAddress, - final String? dkimPublicKey, - }); + Future> removeSimilarRecords({ required final ServerDomain domain, final String? ip4, @@ -33,6 +30,16 @@ abstract class DnsProviderApi extends ApiMap { final DnsRecord record, final ServerDomain domain, ); + Future>> validateDnsRecords( + final ServerDomain domain, + final String ip4, + final String dkimPublicKey, + ); + List getDesiredDnsRecords( + final String? domainName, + final String? ip4, + final String? dkimPublicKey, + ); Future getZoneId(final String domain); Future> domainList(); diff --git a/lib/logic/api_maps/staging_options.dart b/lib/logic/api_maps/staging_options.dart index 7d3084b7..a4e98fe8 100644 --- a/lib/logic/api_maps/staging_options.dart +++ b/lib/logic/api_maps/staging_options.dart @@ -1,8 +1,16 @@ -/// Controls staging environment for network, is used during manual -/// integration testing and such +/// Controls staging environment for network class StagingOptions { /// Whether we request for staging temprorary certificates. /// Hardcode to 'true' in the middle of testing to not /// get your domain banned by constant certificate renewal + /// + /// If set to 'true', the 'verifyCertificate' becomes useless static bool get stagingAcme => false; + + /// Should we consider CERTIFICATE_VERIFY_FAILED code an error + /// For now it's just a global variable and DNS API + /// classes can change it at will + /// + /// Doesn't matter if 'statingAcme' is set to 'true' + static bool verifyCertificate = false; } diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index 3905ff33..4a2deea4 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -25,12 +25,14 @@ class DnsRecordsCubit emit( DnsRecordsState( dnsState: DnsRecordsStatus.refreshing, - dnsRecords: - ProvidersController.currentDnsProvider!.getDesiredDnsRecords( - domainName: serverInstallationCubit.state.serverDomain?.domainName, - dkimPublicKey: '', - ipAddress: '', - ), + dnsRecords: ApiController.currentDnsProviderApiFactory + ?.getDnsProvider() + .getDesiredDnsRecords( + serverInstallationCubit.state.serverDomain?.domainName, + '', + '', + ) ?? + [], ), ); @@ -38,68 +40,32 @@ class DnsRecordsCubit final ServerDomain? domain = serverInstallationCubit.state.serverDomain; final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4; - if (domain != null && ipAddress != null) { - final List records = await ProvidersController - .currentDnsProvider! - .getDnsRecords(domain: domain); - final String? dkimPublicKey = - extractDkimRecord(await api.getDnsRecords())?.content; - final List desiredRecords = - ProvidersController.currentDnsProvider!.getDesiredDnsRecords( - domainName: domain.domainName, - ipAddress: ipAddress, - dkimPublicKey: dkimPublicKey, - ); - final List foundRecords = []; - for (final DesiredDnsRecord desiredRecord in desiredRecords) { - if (desiredRecord.description == 'record.dkim') { - final DnsRecord foundRecord = records.firstWhere( - (final r) => - r.name == desiredRecord.name && r.type == desiredRecord.type, - orElse: () => DnsRecord( - name: desiredRecord.name, - type: desiredRecord.type, - content: '', - ttl: 800, - proxied: false, - ), - ); - // remove all spaces and tabulators from - // the foundRecord.content and the record.content - // to compare them - final String? foundContent = - foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); - final String content = - desiredRecord.content.replaceAll(RegExp(r'\s+'), ''); - if (foundContent == content) { - foundRecords.add(desiredRecord.copyWith(isSatisfied: true)); - } else { - foundRecords.add(desiredRecord.copyWith(isSatisfied: false)); - } - } else { - if (records.any( - (final r) => - r.name == desiredRecord.name && - r.type == desiredRecord.type && - r.content == desiredRecord.content, - )) { - foundRecords.add(desiredRecord.copyWith(isSatisfied: true)); - } else { - foundRecords.add(desiredRecord.copyWith(isSatisfied: false)); - } - } - } - emit( - DnsRecordsState( - dnsRecords: foundRecords, - dnsState: foundRecords.any((final r) => r.isSatisfied == false) - ? DnsRecordsStatus.error - : DnsRecordsStatus.good, - ), - ); - } else { + if (domain == null && ipAddress == null) { emit(const DnsRecordsState()); + return; } + + final foundRecords = await ApiController.currentDnsProviderApiFactory! + .getDnsProvider() + .validateDnsRecords( + domain!, + ipAddress!, + extractDkimRecord(await api.getDnsRecords())?.content ?? '', + ); + + if (!foundRecords.success || foundRecords.data.isEmpty) { + emit(const DnsRecordsState()); + return; + } + + emit( + DnsRecordsState( + dnsRecords: foundRecords.data, + dnsState: foundRecords.data.any((final r) => r.isSatisfied == false) + ? DnsRecordsStatus.error + : DnsRecordsStatus.good, + ), + ); } } diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index ca8b0958..52740b64 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -9,6 +9,9 @@ import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart'; import 'package:selfprivacy/logic/models/launch_installation_data.dart'; import 'package:selfprivacy/logic/providers/provider_settings.dart'; import 'package:selfprivacy/logic/providers/providers_controller.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart'; +import 'package:selfprivacy/logic/api_maps/staging_options.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'; @@ -182,7 +185,7 @@ class ServerInstallationCubit extends Cubit { void setDnsApiToken(final String dnsApiToken) async { if (state is ServerInstallationRecovery) { - await setAndValidateCloudflareToken(dnsApiToken); + await setAndValidateDnsApiToken(dnsApiToken); return; } await repository.setDnsApiToken(dnsApiToken); @@ -429,6 +432,7 @@ class ServerInstallationCubit extends Cubit { emit(TimerState(dataState: dataState, isLoading: true)); final bool isServerWorking = await repository.isHttpServerWorking(); + StagingOptions.verifyCertificate = true; if (isServerWorking) { bool dkimCreated = true; @@ -534,21 +538,18 @@ class ServerInstallationCubit extends Cubit { customToken: serverDetails.apiToken, isWithToken: true, ).getServerProviderType(); - final DnsProviderType dnsProvider = await ServerApi( + final dnsProvider = await ServerApi( customToken: serverDetails.apiToken, isWithToken: true, ).getDnsProviderType(); - if (serverProvider == ServerProviderType.unknown) { - getIt() - .showSnackBar('recovering.generic_error'.tr()); - return; - } - if (dnsProvider == DnsProviderType.unknown) { + if (serverProvider == ServerProviderType.unknown || + dnsProvider == DnsProviderType.unknown) { getIt() .showSnackBar('recovering.generic_error'.tr()); return; } await repository.saveServerDetails(serverDetails); + await repository.saveDnsProviderType(dnsProvider); setServerProviderType(serverProvider); setDnsProviderType(dnsProvider); emit( @@ -689,7 +690,7 @@ class ServerInstallationCubit extends Cubit { ); } - Future setAndValidateCloudflareToken(final String token) async { + Future setAndValidateDnsApiToken(final String token) async { final ServerInstallationRecovery dataState = state as ServerInstallationRecovery; final ServerDomain? serverDomain = dataState.serverDomain; @@ -703,11 +704,15 @@ class ServerInstallationCubit extends Cubit { .showSnackBar('recovering.domain_not_available_on_token'.tr()); return; } + final dnsProviderType = await ServerApi( + customToken: dataState.serverDetails!.apiToken, + isWithToken: true, + ).getDnsProviderType(); await repository.saveDomain( ServerDomain( domainName: serverDomain.domainName, zoneId: zoneId, - provider: DnsProviderType.cloudflare, + provider: dnsProviderType, ), ); await repository.setDnsApiToken(token); @@ -716,7 +721,7 @@ class ServerInstallationCubit extends Cubit { serverDomain: ServerDomain( domainName: serverDomain.domainName, zoneId: zoneId, - provider: DnsProviderType.cloudflare, + provider: dnsProviderType, ), dnsApiToken: token, currentStep: RecoveryStep.backblazeToken, @@ -750,6 +755,7 @@ class ServerInstallationCubit extends Cubit { void clearAppConfig() { closeTimer(); ProvidersController.clearProviders(); + StagingOptions.verifyCertificate = false; repository.clearAppConfig(); emit(const ServerInstallationEmpty()); } diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 6c352a88..fb3470ed 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -10,17 +10,18 @@ import 'package:hive/hive.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/hive_config.dart'; +import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/providers/provider_settings.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; +import 'package:selfprivacy/logic/api_maps/staging_options.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/json/device_token.dart'; -import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/models/message.dart'; import 'package:selfprivacy/logic/models/server_basic_info.dart'; import 'package:selfprivacy/logic/models/server_type.dart'; @@ -45,7 +46,7 @@ class ServerInstallationRepository { Future load() async { final String? providerApiToken = getIt().serverProviderKey; final String? location = getIt().serverLocation; - final String? cloudflareToken = getIt().dnsProviderKey; + final String? dnsApiToken = getIt().dnsProviderKey; final String? serverTypeIdentificator = getIt().serverType; final ServerDomain? serverDomain = getIt().serverDomain; final DnsProviderType? dnsProvider = getIt().dnsProvider; @@ -78,10 +79,11 @@ class ServerInstallationRepository { } if (box.get(BNames.hasFinalChecked, defaultValue: false)) { + StagingOptions.verifyCertificate = true; return ServerInstallationFinished( providerApiToken: providerApiToken!, serverTypeIdentificator: serverTypeIdentificator ?? '', - dnsApiToken: cloudflareToken!, + dnsApiToken: dnsApiToken!, serverDomain: serverDomain!, backblazeCredential: backblazeCredential!, serverDetails: serverDetails!, @@ -98,14 +100,14 @@ class ServerInstallationRepository { serverDomain != null) { return ServerInstallationRecovery( providerApiToken: providerApiToken, - dnsApiToken: cloudflareToken, + dnsApiToken: dnsApiToken, serverDomain: serverDomain, backblazeCredential: backblazeCredential, serverDetails: serverDetails, rootUser: box.get(BNames.rootUser), currentStep: _getCurrentRecoveryStep( providerApiToken, - cloudflareToken, + dnsApiToken, serverDomain, serverDetails, ), @@ -115,7 +117,7 @@ class ServerInstallationRepository { return ServerInstallationNotFinished( providerApiToken: providerApiToken, - dnsApiToken: cloudflareToken, + dnsApiToken: dnsApiToken, serverDomain: serverDomain, backblazeCredential: backblazeCredential, serverDetails: serverDetails, @@ -603,6 +605,10 @@ class ServerInstallationRepository { getIt().init(); } + Future saveDnsProviderType(final DnsProvider type) async { + await getIt().storeDnsProviderType(type); + } + Future saveBackblazeKey( final BackblazeCredential backblazeCredential, ) async { @@ -618,7 +624,7 @@ class ServerInstallationRepository { await getIt().storeDnsProviderKey(key); } - Future deleteCloudFlareKey() async { + Future deleteDnsProviderKey() async { await box.delete(BNames.cloudFlareKey); getIt().init(); } diff --git a/lib/logic/models/hive/server_domain.dart b/lib/logic/models/hive/server_domain.dart index 2d9a554b..1649be2a 100644 --- a/lib/logic/models/hive/server_domain.dart +++ b/lib/logic/models/hive/server_domain.dart @@ -31,12 +31,16 @@ enum DnsProviderType { @HiveField(1) cloudflare, @HiveField(2) + desec, + @HiveField(3) digitalOcean; factory DnsProviderType.fromGraphQL(final Enum$DnsProvider provider) { switch (provider) { case Enum$DnsProvider.CLOUDFLARE: return cloudflare; + case Enum$DnsProvider.DESEC: + return desec; case Enum$DnsProvider.DIGITALOCEAN: return digitalOcean; default: diff --git a/lib/logic/models/hive/server_domain.g.dart b/lib/logic/models/hive/server_domain.g.dart index 19f1ef6f..303407bc 100644 --- a/lib/logic/models/hive/server_domain.g.dart +++ b/lib/logic/models/hive/server_domain.g.dart @@ -60,6 +60,8 @@ class DnsProviderTypeAdapter extends TypeAdapter { case 1: return DnsProviderType.cloudflare; case 2: + return DnsProviderType.desec; + case 3: return DnsProviderType.digitalOcean; default: return DnsProviderType.unknown; @@ -75,9 +77,12 @@ class DnsProviderTypeAdapter extends TypeAdapter { case DnsProviderType.cloudflare: writer.writeByte(1); break; - case DnsProviderType.digitalOcean: + case DnsProviderType.desec: writer.writeByte(2); break; + case DnsProviderType.digitalOcean: + writer.writeByte(3); + break; } } diff --git a/lib/logic/providers/dns_providers/desec.dart b/lib/logic/providers/dns_providers/desec.dart new file mode 100644 index 00000000..c7a5bab8 --- /dev/null +++ b/lib/logic/providers/dns_providers/desec.dart @@ -0,0 +1,3 @@ +import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; + +class DesecDnsProvider extends DnsProvider {} diff --git a/lib/logic/providers/dns_providers/dns_provider_factory.dart b/lib/logic/providers/dns_providers/dns_provider_factory.dart index a5adebc9..b42854b7 100644 --- a/lib/logic/providers/dns_providers/dns_provider_factory.dart +++ b/lib/logic/providers/dns_providers/dns_provider_factory.dart @@ -1,5 +1,6 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/providers/dns_providers/cloudflare.dart'; +import 'package:selfprivacy/logic/providers/dns_providers/desec.dart'; import 'package:selfprivacy/logic/providers/dns_providers/digital_ocean.dart'; import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; import 'package:selfprivacy/logic/providers/provider_settings.dart'; @@ -18,6 +19,8 @@ class DnsProviderFactory { return CloudflareDnsProvider(); case DnsProviderType.digitalOcean: return DigitalOceanDnsProvider(); + case DnsProviderType.desec: + return DesecDnsProvider(); case DnsProviderType.unknown: throw UnknownProviderException('Unknown server provider'); } diff --git a/lib/ui/pages/more/about_application.dart b/lib/ui/pages/more/about_application.dart index 54e493de..1fa5c932 100644 --- a/lib/ui/pages/more/about_application.dart +++ b/lib/ui/pages/more/about_application.dart @@ -50,7 +50,7 @@ class AboutApplicationPage extends StatelessWidget { children: [ TextButton( onPressed: () => launchUrl( - Uri.parse('https://selfprivacy.ru/privacy-policy'), + Uri.parse('https://selfprivacy.org/privacy-policy/'), mode: LaunchMode.externalApplication, ), child: Text('about_application_page.privacy_policy'.tr()), diff --git a/lib/ui/pages/setup/initializing/dns_provider_picker.dart b/lib/ui/pages/setup/initializing/dns_provider_picker.dart index 92f1a36d..fd839092 100644 --- a/lib/ui/pages/setup/initializing/dns_provider_picker.dart +++ b/lib/ui/pages/setup/initializing/dns_provider_picker.dart @@ -11,6 +11,8 @@ import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; import 'package:selfprivacy/ui/components/buttons/outlined_button.dart'; import 'package:selfprivacy/ui/components/cards/outlined_card.dart'; import 'package:selfprivacy/utils/network_utils.dart'; +import 'package:selfprivacy/utils/launch_url.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class DnsProviderPicker extends StatefulWidget { const DnsProviderPicker({ @@ -69,6 +71,19 @@ class _DnsProviderPickerState extends State { ), ), ); + + case DnsProviderType.desec: + return ProviderInputDataPage( + providerCubit: widget.formCubit, + providerInfo: ProviderPageInfo( + providerType: DnsProviderType.desec, + pathToHow: 'how_desec', + image: Image.asset( + 'assets/images/logos/desec.svg', + width: 150, + ), + ), + ); } } } @@ -186,7 +201,7 @@ class ProviderSelectionPage extends StatelessWidget { padding: const EdgeInsets.all(10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(40), - color: const Color.fromARGB(255, 241, 215, 166), + color: const Color.fromARGB(255, 244, 128, 31), ), child: SvgPicture.asset( 'assets/images/logos/cloudflare.svg', @@ -230,7 +245,7 @@ class ProviderSelectionPage extends StatelessWidget { // Outlined button that will open website BrandOutlinedButton( onPressed: () => - launchURL('https://dash.cloudflare.com/'), + launchUrlString('https://dash.cloudflare.com/'), title: 'initializing.select_provider_site_button'.tr(), ), ], @@ -295,7 +310,71 @@ class ProviderSelectionPage extends StatelessWidget { // Outlined button that will open website BrandOutlinedButton( onPressed: () => - launchURL('https://www.digitalocean.com'), + launchUrlString('https://www.digitalocean.com'), + title: 'initializing.select_provider_site_button'.tr(), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + OutlinedCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 40, + height: 40, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(40), + color: const Color.fromARGB(255, 245, 229, 82), + ), + child: SvgPicture.asset( + 'assets/images/logos/desec.svg', + ), + ), + const SizedBox(width: 16), + Text( + 'deSEC', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_price_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'initializing.select_provider_price_free'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_payment_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'initializing.select_provider_payment_text_do'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + BrandButton.rised( + text: 'basis.select'.tr(), + onPressed: () { + serverInstallationCubit + .setDnsProviderType(DnsProviderType.desec); + callback(DnsProviderType.desec); + }, + ), + // Outlined button that will open website + BrandOutlinedButton( + onPressed: () => launchUrlString('https://desec.io/'), title: 'initializing.select_provider_site_button'.tr(), ), ], diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart b/lib/ui/pages/setup/recovering/recovery_confirm_dns.dart similarity index 96% rename from lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart rename to lib/ui/pages/setup/recovering/recovery_confirm_dns.dart index 93c889a5..9dcad056 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_dns.dart @@ -7,8 +7,8 @@ import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; -class RecoveryConfirmCloudflare extends StatelessWidget { - const RecoveryConfirmCloudflare({super.key}); +class RecoveryConfirmDns extends StatelessWidget { + const RecoveryConfirmDns({super.key}); @override Widget build(final BuildContext context) { diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index 0d11c4d8..be5eb2ea 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -12,7 +12,7 @@ import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart' import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_recovery_key.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_new_device_key.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_backblaze.dart'; -import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_dns.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_server.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_server_provider_connected.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; @@ -56,7 +56,7 @@ class RecoveryRouting extends StatelessWidget { currentPage = const RecoveryConfirmServer(); break; case RecoveryStep.dnsProviderToken: - currentPage = const RecoveryConfirmCloudflare(); + currentPage = const RecoveryConfirmDns(); break; case RecoveryStep.backblazeToken: currentPage = const RecoveryConfirmBackblaze(); diff --git a/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart b/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart index 9b6cb09e..d8a9e8cb 100644 --- a/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart +++ b/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart @@ -46,9 +46,8 @@ class RecoveryServerProviderConnected extends StatelessWidget { ), const SizedBox(height: 16), BrandButton.filled( - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), + onPressed: () => + context.read().trySubmit(), child: Text('basis.continue'.tr()), ), const SizedBox(height: 16),