chore: Merge desec into refactoring

pull/213/head
NaiJi ✨ 2023-05-17 13:58:15 -03:00
commit 4260152081
27 changed files with 966 additions and 219 deletions

View File

@ -1,10 +1 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256px" height="116px" viewBox="0 0 256 116" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<g transform="translate(0.000000, -1.000000)">
<path d="M202.3569,50.394 L197.0459,48.27 C172.0849,104.434 72.7859,70.289 66.8109,86.997 C65.8149,98.283 121.0379,89.143 160.5169,91.056 C172.5559,91.639 178.5929,100.727 173.4809,115.54 L183.5499,115.571 C195.1649,79.362 232.2329,97.841 233.7819,85.891 C231.2369,78.034 191.1809,85.891 202.3569,50.394 Z" fill="#FFFFFF"></path>
<path d="M176.332,109.3483 C177.925,104.0373 177.394,98.7263 174.739,95.5393 C172.083,92.3523 168.365,90.2283 163.585,89.6973 L71.17,88.6343 C70.639,88.6343 70.108,88.1033 69.577,88.1033 C69.046,87.5723 69.046,87.0413 69.577,86.5103 C70.108,85.4483 70.639,84.9163 71.701,84.9163 L164.647,83.8543 C175.801,83.3233 187.486,74.2943 191.734,63.6723 L197.046,49.8633 C197.046,49.3313 197.577,48.8003 197.046,48.2693 C191.203,21.1823 166.772,0.9993 138.091,0.9993 C111.535,0.9993 88.697,17.9953 80.73,41.8963 C75.419,38.1783 69.046,36.0533 61.61,36.5853 C48.863,37.6473 38.772,48.2693 37.178,61.0163 C36.647,64.2033 37.178,67.3903 37.71,70.5763 C16.996,71.1073 0,88.1033 0,109.3483 C0,111.4723 0,113.0663 0.531,115.1903 C0.531,116.2533 1.593,116.7843 2.125,116.7843 L172.614,116.7843 C173.676,116.7843 174.739,116.2533 174.739,115.1903 L176.332,109.3483 Z" fill="#F4811F"></path>
<path d="M205.5436,49.8628 L202.8876,49.8628 C202.3566,49.8628 201.8256,50.3938 201.2946,50.9248 L197.5766,63.6718 C195.9836,68.9828 196.5146,74.2948 199.1706,77.4808 C201.8256,80.6678 205.5436,82.7918 210.3236,83.3238 L229.9756,84.3858 C230.5066,84.3858 231.0376,84.9168 231.5686,84.9168 C232.0996,85.4478 232.0996,85.9788 231.5686,86.5098 C231.0376,87.5728 230.5066,88.1038 229.4436,88.1038 L209.2616,89.1658 C198.1076,89.6968 186.4236,98.7258 182.1746,109.3478 L181.1116,114.1288 C180.5806,114.6598 181.1116,115.7218 182.1746,115.7218 L252.2826,115.7218 C253.3446,115.7218 253.8756,115.1908 253.8756,114.1288 C254.9376,109.8798 255.9996,105.0998 255.9996,100.3188 C255.9996,72.7008 233.1616,49.8628 205.5436,49.8628" fill="#FAAD3F"></path>
</g>
</g>
</svg>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 209.51 94.74"><defs><style>.cls-1{fill:#fff;}</style></defs><path class="cls-1" d="M143.05,93.42l1.07-3.71c1.27-4.41.8-8.48-1.34-11.48-2-2.76-5.26-4.38-9.25-4.57L58,72.7a1.47,1.47,0,0,1-1.35-2,2,2,0,0,1,1.75-1.34l76.26-1c9-.41,18.84-7.75,22.27-16.71l4.34-11.36a2.68,2.68,0,0,0,.18-1,3.31,3.31,0,0,0-.06-.54,49.67,49.67,0,0,0-95.49-5.14,22.35,22.35,0,0,0-35,23.42A31.73,31.73,0,0,0,.34,93.45a1.47,1.47,0,0,0,1.45,1.27l139.49,0h0A1.83,1.83,0,0,0,143.05,93.42Z"/><path class="cls-1" d="M168.22,41.15q-1,0-2.1.06a.88.88,0,0,0-.32.07,1.17,1.17,0,0,0-.76.8l-3,10.26c-1.28,4.41-.81,8.48,1.34,11.48a11.65,11.65,0,0,0,9.24,4.57l16.11,1a1.44,1.44,0,0,1,1.14.62,1.5,1.5,0,0,1,.17,1.37,2,2,0,0,1-1.75,1.34l-16.73,1c-9.09.42-18.88,7.75-22.31,16.7l-1.21,3.16a.9.9,0,0,0,.79,1.22h57.63A1.55,1.55,0,0,0,208,93.63a41.34,41.34,0,0,0-39.76-52.48Z"/></svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 923 B

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="7.4053912mm"
height="7.5173831mm"
viewBox="0 0 7.4053913 7.5173831"
version="1.1"
id="svg1262"
sodipodi:docname="logo.notext.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<defs
id="defs1256" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="101.86078"
inkscape:cy="8.9271745"
inkscape:document-units="mm"
inkscape:current-layer="g3885"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="2560"
inkscape:window-height="1365"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="1" />
<metadata
id="metadata1259">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-254.94057,-266.78298)">
<g
id="g3885"
transform="matrix(0.26519825,0,0,0.26519825,228.89366,215.69135)"
style="fill:#000000">
<g
style="fill:#000000;stroke:#ffffff;stroke-opacity:1"
id="layer1-9"
transform="matrix(0.22901929,0,0,0.22901929,26.296508,84.906304)"
inkscape:export-filename="/home/nils/git/desec-stack/webapp/src/assets/logo.png"
inkscape:export-xdpi="567.52002"
inkscape:export-ydpi="567.52002">
<g
style="fill:#000000;stroke:#ffffff;stroke-opacity:1"
transform="translate(-194.13584,150.8067)"
id="g3933">
<path
inkscape:connector-curvature="0"
d="m 509.13584,366.2239 c 8.87906,-33.13708 42.93987,-52.8021 76.07695,-43.92304 21.43594,5.74374 38.17931,22.48711 43.92305,43.92304 0,0 -6.09923,-6.07815 -10,-6.07815 -3.90077,0 -10,6.07815 -10,6.07815 0,0 -6.09923,-6.07815 -10,-6.07815 -3.90077,0 -10,6.07815 -10,6.07815 0,0 -6.09923,-6.07815 -10,-6.07815 -3.90077,0 -10,6.07815 -10,6.07815 0,0 -6.09923,-6.07815 -10,-6.07815 -3.90077,0 -10,6.07815 -10,6.07815 0,0 -6.09923,-6.07815 -10,-6.07815 -3.90077,0 -10,6.07815 -10,6.07815 0,0 -6.09923,-6.07815 -10,-6.07815 -3.90077,0 -10,6.07815 -10,6.07815 z"
id="path2985-6-3"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.99999994;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker:none;enable-background:accumulate" />
<path
inkscape:connector-curvature="0"
d="m 567.42674,364.89583 v 61.87321 c 0,9.34738 5.48085,16.17306 12.23879,16.17306 6.75795,0 12.23635,-6.83606 12.23635,-16.18344 0,0 -1.07806,-1.02674 -1.75904,-1.03964 -0.64261,-0.0122 -1.69589,0.91753 -1.69589,0.91753 0,6.70817 -3.93157,13.01592 -8.78142,13.01592 -4.84984,0 -8.78142,-6.30775 -8.78142,-13.01592 l -7.6e-4,-61.74072 z"
id="path3775-7-4-6"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:117.14173126;stroke-opacity:1;marker:none;enable-background:accumulate" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -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.

View File

@ -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**.

View File

@ -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:"
},

View File

@ -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": "Длина строки [], должна быть меньше либо равна {}"
}
}
}

View File

@ -56,7 +56,7 @@ class ResponseLoggingParser extends ResponseParser {
abstract class ApiMap {
Future<GraphQLClient> getClient() async {
IOClient? ioClient;
if (StagingOptions.stagingAcme) {
if (StagingOptions.stagingAcme || !StagingOptions.verifyCertificate) {
final HttpClient httpClient = HttpClient();
httpClient.badCertificateCallback = (
final cert,

View File

@ -76,7 +76,8 @@ type DeviceApiTokenMutationReturn implements MutationReturnInterface {
enum DnsProvider {
CLOUDFLARE,
DIGITALOCEAN
DIGITALOCEAN,
DESEC
}
type DnsRecord {

View File

@ -1096,7 +1096,7 @@ class _CopyWithStubImpl$Input$UserMutationInput<TRes>
_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;
}

View File

@ -189,88 +189,6 @@ class CloudflareApi extends DnsProviderApi {
return allRecords;
}
@override
List<DesiredDnsRecord> 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<GenericResult<void>> createMultipleDnsRecords({
required final ServerDomain domain,
@ -353,4 +271,147 @@ class CloudflareApi extends DnsProviderApi {
return domains;
}
@override
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
) async {
final List<DnsRecord> records = await getDnsRecords(domain: domain);
final List<DesiredDnsRecord> foundRecords = [];
try {
final List<DesiredDnsRecord> 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<DesiredDnsRecord> 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,
),
];
}
}

View File

@ -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<ApiConfigModel>().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<GenericResult<bool>> 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<String?> getZoneId(final String domain) async => domain;
@override
Future<GenericResult<void>> removeSimilarRecords({
required final ServerDomain domain,
final String? ip4,
}) async {
final String domainName = domain.domainName;
final String url = '/$domainName/rrsets/';
final List<DnsRecord> listDnsRecords = projectDnsRecords(domainName, ip4);
final Dio client = await getClient();
try {
final List<dynamic> 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<List<DnsRecord>> getDnsRecords({
required final ServerDomain domain,
}) async {
Response response;
final String domainName = domain.domainName;
final List<DnsRecord> allRecords = <DnsRecord>[];
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<dynamic>)
? 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<GenericResult<void>> createMultipleDnsRecords({
required final ServerDomain domain,
final String? ip4,
}) async {
final String domainName = domain.domainName;
final List<DnsRecord> listDnsRecords = projectDnsRecords(domainName, ip4);
final Dio client = await getClient();
try {
final List<dynamic> 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<DnsRecord> 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 <DnsRecord>[
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<void> 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<List<String>> domainList() async {
List<String> domains = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
'',
);
await Future.delayed(const Duration(seconds: 1));
domains = response.data
.map<String>((final el) => el['name'] as String)
.toList();
} catch (e) {
print(e);
} finally {
close(client);
}
return domains;
}
@override
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
) async {
final List<DnsRecord> records = await getDnsRecords(domain: domain);
final List<DesiredDnsRecord> foundRecords = [];
try {
final List<DesiredDnsRecord> 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<DesiredDnsRecord> 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,
),
];
}
}

View File

@ -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,
);
}

View File

@ -171,61 +171,67 @@ class DigitalOceanDnsApi extends DnsProviderApi {
return allRecords;
}
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
);
@override
List<DesiredDnsRecord> getDesiredDnsRecords({
List<DesiredDnsRecord> 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,

View File

@ -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<List<DnsRecord>> getDnsRecords({
required final ServerDomain domain,
});
List<DesiredDnsRecord> getDesiredDnsRecords({
final String? domainName,
final String? ipAddress,
final String? dkimPublicKey,
});
Future<GenericResult<void>> removeSimilarRecords({
required final ServerDomain domain,
final String? ip4,
@ -33,6 +30,16 @@ abstract class DnsProviderApi extends ApiMap {
final DnsRecord record,
final ServerDomain domain,
);
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
);
List<DesiredDnsRecord> getDesiredDnsRecords(
final String? domainName,
final String? ip4,
final String? dkimPublicKey,
);
Future<String?> getZoneId(final String domain);
Future<List<String>> domainList();

View File

@ -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;
}

View File

@ -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<DnsRecord> records = await ProvidersController
.currentDnsProvider!
.getDnsRecords(domain: domain);
final String? dkimPublicKey =
extractDkimRecord(await api.getDnsRecords())?.content;
final List<DesiredDnsRecord> desiredRecords =
ProvidersController.currentDnsProvider!.getDesiredDnsRecords(
domainName: domain.domainName,
ipAddress: ipAddress,
dkimPublicKey: dkimPublicKey,
);
final List<DesiredDnsRecord> 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,
),
);
}
}

View File

@ -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<ServerInstallationState> {
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<ServerInstallationState> {
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<ServerInstallationState> {
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<NavigationService>()
.showSnackBar('recovering.generic_error'.tr());
return;
}
if (dnsProvider == DnsProviderType.unknown) {
if (serverProvider == ServerProviderType.unknown ||
dnsProvider == DnsProviderType.unknown) {
getIt<NavigationService>()
.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<ServerInstallationState> {
);
}
Future<void> setAndValidateCloudflareToken(final String token) async {
Future<void> setAndValidateDnsApiToken(final String token) async {
final ServerInstallationRecovery dataState =
state as ServerInstallationRecovery;
final ServerDomain? serverDomain = dataState.serverDomain;
@ -703,11 +704,15 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
.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<ServerInstallationState> {
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<ServerInstallationState> {
void clearAppConfig() {
closeTimer();
ProvidersController.clearProviders();
StagingOptions.verifyCertificate = false;
repository.clearAppConfig();
emit(const ServerInstallationEmpty());
}

View File

@ -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<ServerInstallationState> load() async {
final String? providerApiToken = getIt<ApiConfigModel>().serverProviderKey;
final String? location = getIt<ApiConfigModel>().serverLocation;
final String? cloudflareToken = getIt<ApiConfigModel>().dnsProviderKey;
final String? dnsApiToken = getIt<ApiConfigModel>().dnsProviderKey;
final String? serverTypeIdentificator = getIt<ApiConfigModel>().serverType;
final ServerDomain? serverDomain = getIt<ApiConfigModel>().serverDomain;
final DnsProviderType? dnsProvider = getIt<ApiConfigModel>().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<ApiConfigModel>().init();
}
Future<void> saveDnsProviderType(final DnsProvider type) async {
await getIt<ApiConfigModel>().storeDnsProviderType(type);
}
Future<void> saveBackblazeKey(
final BackblazeCredential backblazeCredential,
) async {
@ -618,7 +624,7 @@ class ServerInstallationRepository {
await getIt<ApiConfigModel>().storeDnsProviderKey(key);
}
Future<void> deleteCloudFlareKey() async {
Future<void> deleteDnsProviderKey() async {
await box.delete(BNames.cloudFlareKey);
getIt<ApiConfigModel>().init();
}

View File

@ -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:

View File

@ -60,6 +60,8 @@ class DnsProviderTypeAdapter extends TypeAdapter<DnsProviderType> {
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<DnsProviderType> {
case DnsProviderType.cloudflare:
writer.writeByte(1);
break;
case DnsProviderType.digitalOcean:
case DnsProviderType.desec:
writer.writeByte(2);
break;
case DnsProviderType.digitalOcean:
writer.writeByte(3);
break;
}
}

View File

@ -0,0 +1,3 @@
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
class DesecDnsProvider extends DnsProvider {}

View File

@ -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');
}

View File

@ -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()),

View File

@ -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<DnsProviderPicker> {
),
),
);
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(),
),
],

View File

@ -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) {

View File

@ -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();

View File

@ -46,9 +46,8 @@ class RecoveryServerProviderConnected extends StatelessWidget {
),
const SizedBox(height: 16),
BrandButton.filled(
onPressed: formCubitState.isSubmitting
? null
: () => context.read<ServerProviderFormCubit>().trySubmit(),
onPressed: () =>
context.read<ServerProviderFormCubit>().trySubmit(),
child: Text('basis.continue'.tr()),
),
const SizedBox(height: 16),