DNS records cubit and screen
parent
83a2d19e37
commit
914d56ff87
|
@ -91,6 +91,28 @@
|
||||||
"status": "Status — Good",
|
"status": "Status — Good",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "It's your personal internet address that will point to the server and other services of yours."
|
"1": "It's your personal internet address that will point to the server and other services of yours."
|
||||||
|
},
|
||||||
|
"screen_title": "Domain and DNS",
|
||||||
|
"states": {
|
||||||
|
"ok": "Records are OK",
|
||||||
|
"error": "Problems found",
|
||||||
|
"error_subtitle": "Tap here to fix them",
|
||||||
|
"refreshing": "Refreshing status...",
|
||||||
|
"uninitialized": "Data is not retrieved yet"
|
||||||
|
},
|
||||||
|
"record_description": {
|
||||||
|
"root": "Root domain",
|
||||||
|
"api": "SelfPrivacy API",
|
||||||
|
"cloud": "File cloud",
|
||||||
|
"git": "Git server",
|
||||||
|
"meet": "Video conference",
|
||||||
|
"social": "Social network",
|
||||||
|
"password": "Password manager",
|
||||||
|
"vpn": "VPN",
|
||||||
|
"mx": "MX record",
|
||||||
|
"dmarc": "DMARC record",
|
||||||
|
"spf": "SPF record",
|
||||||
|
"dkim": "DKIM key"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
|
@ -120,7 +142,8 @@
|
||||||
"_comment": "Card shown when user skips initial setup",
|
"_comment": "Card shown when user skips initial setup",
|
||||||
"1": "Please finish application setup using ",
|
"1": "Please finish application setup using ",
|
||||||
"2": "@:more.configuration_wizard",
|
"2": "@:more.configuration_wizard",
|
||||||
"3": " for further work"
|
"3": " for further work",
|
||||||
|
"in_menu": "Server is not set up yet. Please finish setup using setup wizard for further work."
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"_comment": "Вкладка сервисы",
|
"_comment": "Вкладка сервисы",
|
||||||
|
|
|
@ -91,6 +91,38 @@
|
||||||
"status": "Статус — в норме",
|
"status": "Статус — в норме",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "Это ваш личный адрес в интернете, который будет указывать на сервер и другие ваши сервисы."
|
"1": "Это ваш личный адрес в интернете, который будет указывать на сервер и другие ваши сервисы."
|
||||||
|
},
|
||||||
|
"screen_title": "Домен и DNS",
|
||||||
|
"states": {
|
||||||
|
"ok": "Записи в норме",
|
||||||
|
"error": "Обнаружены проблемы",
|
||||||
|
"error_subtitle": "Нажмите здесь, чтобы исправить",
|
||||||
|
"refreshing": "Обновление данных...",
|
||||||
|
"uninitialized": "Данные ещё не получены"
|
||||||
|
},
|
||||||
|
"record_description": {
|
||||||
|
"root": "Корневой домен",
|
||||||
|
"api": "SelfPrivacy API",
|
||||||
|
"cloud": "Файловое облако",
|
||||||
|
"git": "Git сервер",
|
||||||
|
"meet": "Видеоконференции",
|
||||||
|
"social": "Социальная сеть",
|
||||||
|
"password": "Менеджер паролей",
|
||||||
|
"vpn": "VPN",
|
||||||
|
"mx": "MX запись",
|
||||||
|
"dmarc": "DMARC запись",
|
||||||
|
"spf": "SPF запись",
|
||||||
|
"dkim": "DKIM ключ"
|
||||||
|
},
|
||||||
|
"cards": {
|
||||||
|
"services": {
|
||||||
|
"title": "Сервисы",
|
||||||
|
"subtitle": "Записи типа “A” необходимые для работы сервисов."
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"title": "Электронная почта",
|
||||||
|
"subtitle": "Записи необходимые для безопасного обмена электронной почтой."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
|
@ -98,7 +130,7 @@
|
||||||
"status": "Статус — в норме",
|
"status": "Статус — в норме",
|
||||||
"bottom_sheet": {
|
"bottom_sheet": {
|
||||||
"1": "Выручит Вас в любой ситуации: хакерская атака, удаление сервера и т.д.",
|
"1": "Выручит Вас в любой ситуации: хакерская атака, удаление сервера и т.д.",
|
||||||
"2": "Использовано 3Gb из бесплатых 10Gb. Последнее копирование была сделано вчера в {}."
|
"2": "Использовано 3Gb из бесплатых 10Gb. Последнее копирование была сделано вчера в {}."
|
||||||
},
|
},
|
||||||
"reuploadKey": "Принудительно обновить ключ",
|
"reuploadKey": "Принудительно обновить ключ",
|
||||||
"reuploadedKey": "Ключ на сервере обновлён",
|
"reuploadedKey": "Ключ на сервере обновлён",
|
||||||
|
@ -121,7 +153,8 @@
|
||||||
"_comment": "Карточка показывающая когда человек скипнул настройку, на карте текст из 3 блоков, средний содержит ссыку на мастер подключения",
|
"_comment": "Карточка показывающая когда человек скипнул настройку, на карте текст из 3 блоков, средний содержит ссыку на мастер подключения",
|
||||||
"1": "Завершите настройку приложения используя ",
|
"1": "Завершите настройку приложения используя ",
|
||||||
"2": "@:more.configuration_wizard",
|
"2": "@:more.configuration_wizard",
|
||||||
"3": " для продолжения работы"
|
"3": " для продолжения работы",
|
||||||
|
"in_menu": "Сервер ещё не настроен, воспользуйтесь мастером подключения."
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"_comment": "Вкладка сервисы",
|
"_comment": "Вкладка сервисы",
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
|
|
||||||
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
|
||||||
|
@ -20,6 +21,7 @@ class BlocAndProviderConfig extends StatelessWidget {
|
||||||
var appConfigCubit = AppConfigCubit()..load();
|
var appConfigCubit = AppConfigCubit()..load();
|
||||||
var servicesCubit = ServicesCubit(appConfigCubit);
|
var servicesCubit = ServicesCubit(appConfigCubit);
|
||||||
var backupsCubit = BackupsCubit(appConfigCubit);
|
var backupsCubit = BackupsCubit(appConfigCubit);
|
||||||
|
var dnsRecordsCubit = DnsRecordsCubit(appConfigCubit);
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
|
@ -33,6 +35,7 @@ class BlocAndProviderConfig extends StatelessWidget {
|
||||||
BlocProvider(create: (_) => usersCubit..load(), lazy: false),
|
BlocProvider(create: (_) => usersCubit..load(), lazy: false),
|
||||||
BlocProvider(create: (_) => servicesCubit..load(), lazy: false),
|
BlocProvider(create: (_) => servicesCubit..load(), lazy: false),
|
||||||
BlocProvider(create: (_) => backupsCubit..load(), lazy: false),
|
BlocProvider(create: (_) => backupsCubit..load(), lazy: false),
|
||||||
|
BlocProvider(create: (_) => dnsRecordsCubit..load()),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (_) =>
|
create: (_) =>
|
||||||
JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit),
|
JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/api_map.dart';
|
import 'package:selfprivacy/logic/api_maps/api_map.dart';
|
||||||
|
@ -87,6 +88,36 @@ class CloudflareApi extends ApiMap {
|
||||||
close(client);
|
close(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<DnsRecord>> getDnsRecords({
|
||||||
|
required CloudFlareDomain cloudFlareDomain,
|
||||||
|
}) async {
|
||||||
|
var domainName = cloudFlareDomain.domainName;
|
||||||
|
var domainZoneId = cloudFlareDomain.zoneId;
|
||||||
|
|
||||||
|
var url = '/zones/$domainZoneId/dns_records';
|
||||||
|
|
||||||
|
var client = await getClient();
|
||||||
|
Response response = await client.get(url);
|
||||||
|
|
||||||
|
List records = response.data['result'] ?? [];
|
||||||
|
var allRecords = <DnsRecord>[];
|
||||||
|
|
||||||
|
for (var record in records) {
|
||||||
|
if (record['zone_name'] == domainName) {
|
||||||
|
allRecords.add(DnsRecord(
|
||||||
|
name: record['name'],
|
||||||
|
type: record['type'],
|
||||||
|
content: record['content'],
|
||||||
|
ttl: record['ttl'],
|
||||||
|
proxied: record['proxied'],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(client);
|
||||||
|
return allRecords;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> createMultipleDnsRecords({
|
Future<void> createMultipleDnsRecords({
|
||||||
String? ip4,
|
String? ip4,
|
||||||
required CloudFlareDomain cloudFlareDomain,
|
required CloudFlareDomain cloudFlareDomain,
|
||||||
|
@ -113,33 +144,33 @@ class CloudflareApi extends ApiMap {
|
||||||
close(client);
|
close(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DnsRecords> projectDnsRecords(String? domainName, String? ip4) {
|
List<DnsRecord> projectDnsRecords(String? domainName, String? ip4) {
|
||||||
var domainA = DnsRecords(type: 'A', name: domainName, content: ip4);
|
var domainA = DnsRecord(type: 'A', name: domainName, content: ip4);
|
||||||
|
|
||||||
var mx = DnsRecords(type: 'MX', name: '@', content: domainName);
|
var mx = DnsRecord(type: 'MX', name: '@', content: domainName);
|
||||||
var apiA = DnsRecords(type: 'A', name: 'api', content: ip4);
|
var apiA = DnsRecord(type: 'A', name: 'api', content: ip4);
|
||||||
var cloudA = DnsRecords(type: 'A', name: 'cloud', content: ip4);
|
var cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4);
|
||||||
var gitA = DnsRecords(type: 'A', name: 'git', content: ip4);
|
var gitA = DnsRecord(type: 'A', name: 'git', content: ip4);
|
||||||
var meetA = DnsRecords(type: 'A', name: 'meet', content: ip4);
|
var meetA = DnsRecord(type: 'A', name: 'meet', content: ip4);
|
||||||
var passwordA = DnsRecords(type: 'A', name: 'password', content: ip4);
|
var passwordA = DnsRecord(type: 'A', name: 'password', content: ip4);
|
||||||
var socialA = DnsRecords(type: 'A', name: 'social', content: ip4);
|
var socialA = DnsRecord(type: 'A', name: 'social', content: ip4);
|
||||||
var vpn = DnsRecords(type: 'A', name: 'vpn', content: ip4);
|
var vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4);
|
||||||
|
|
||||||
var txt1 = DnsRecords(
|
var txt1 = DnsRecord(
|
||||||
type: 'TXT',
|
type: 'TXT',
|
||||||
name: '_dmarc',
|
name: '_dmarc',
|
||||||
content: 'v=DMARC1; p=none',
|
content: 'v=DMARC1; p=none',
|
||||||
ttl: 18000,
|
ttl: 18000,
|
||||||
);
|
);
|
||||||
|
|
||||||
var txt2 = DnsRecords(
|
var txt2 = DnsRecord(
|
||||||
type: 'TXT',
|
type: 'TXT',
|
||||||
name: domainName,
|
name: domainName,
|
||||||
content: 'v=spf1 a mx ip4:$ip4 -all',
|
content: 'v=spf1 a mx ip4:$ip4 -all',
|
||||||
ttl: 18000,
|
ttl: 18000,
|
||||||
);
|
);
|
||||||
|
|
||||||
return <DnsRecords>[
|
return <DnsRecord>[
|
||||||
domainA,
|
domainA,
|
||||||
apiA,
|
apiA,
|
||||||
cloudA,
|
cloudA,
|
||||||
|
@ -159,7 +190,7 @@ class CloudflareApi extends ApiMap {
|
||||||
final domainZoneId = cloudFlareDomain.zoneId;
|
final domainZoneId = cloudFlareDomain.zoneId;
|
||||||
final url = '$rootAddress/zones/$domainZoneId/dns_records';
|
final url = '$rootAddress/zones/$domainZoneId/dns_records';
|
||||||
|
|
||||||
final dkimRecord = DnsRecords(
|
final dkimRecord = DnsRecord(
|
||||||
type: 'TXT',
|
type: 'TXT',
|
||||||
name: 'selector._domainkey',
|
name: 'selector._domainkey',
|
||||||
content: dkimRecordString,
|
content: dkimRecordString,
|
||||||
|
|
|
@ -0,0 +1,184 @@
|
||||||
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/cloudflare_domain.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/dns_records.dart';
|
||||||
|
|
||||||
|
import '../../api_maps/cloudflare.dart';
|
||||||
|
import '../../api_maps/server.dart';
|
||||||
|
|
||||||
|
part 'dns_records_state.dart';
|
||||||
|
|
||||||
|
class DnsRecordsCubit extends AppConfigDependendCubit<DnsRecordsState> {
|
||||||
|
DnsRecordsCubit(AppConfigCubit appConfigCubit)
|
||||||
|
: super(appConfigCubit,
|
||||||
|
DnsRecordsState(dnsState: DnsRecordsStatus.refreshing));
|
||||||
|
|
||||||
|
final api = ServerApi();
|
||||||
|
final cloudflare = CloudflareApi();
|
||||||
|
|
||||||
|
Future<void> load() async {
|
||||||
|
emit(DnsRecordsState(
|
||||||
|
dnsState: DnsRecordsStatus.refreshing,
|
||||||
|
dnsRecords: _getDesiredDnsRecords(
|
||||||
|
appConfigCubit.state.cloudFlareDomain?.domainName, "", "")));
|
||||||
|
print('Loading DNS status');
|
||||||
|
if (appConfigCubit.state is AppConfigFinished) {
|
||||||
|
final CloudFlareDomain? domain = appConfigCubit.state.cloudFlareDomain;
|
||||||
|
final String? ipAddress = appConfigCubit.state.hetznerServer?.ip4;
|
||||||
|
if (domain != null && ipAddress != null) {
|
||||||
|
final List<DnsRecord> records =
|
||||||
|
await cloudflare.getDnsRecords(cloudFlareDomain: domain);
|
||||||
|
final dkimPublicKey = await api.getDkim();
|
||||||
|
final desiredRecords =
|
||||||
|
_getDesiredDnsRecords(domain.domainName, ipAddress, dkimPublicKey);
|
||||||
|
List<DesiredDnsRecord> foundRecords = [];
|
||||||
|
for (final record in desiredRecords) {
|
||||||
|
if (record.description ==
|
||||||
|
'providers.domain.record_description.dkim') {
|
||||||
|
final foundRecord = records.firstWhere(
|
||||||
|
(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 foundContent =
|
||||||
|
foundRecord.content?.replaceAll(RegExp(r'\s+'), '');
|
||||||
|
final 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((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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit(DnsRecordsState(
|
||||||
|
dnsRecords: foundRecords,
|
||||||
|
dnsState: foundRecords.any((r) => r.isSatisfied == false)
|
||||||
|
? DnsRecordsStatus.error
|
||||||
|
: DnsRecordsStatus.good,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
emit(DnsRecordsState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onChange(Change<DnsRecordsState> change) {
|
||||||
|
print(change);
|
||||||
|
super.onChange(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clear() async {
|
||||||
|
emit(DnsRecordsState(dnsState: DnsRecordsStatus.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing));
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fix() async {
|
||||||
|
emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing));
|
||||||
|
final CloudFlareDomain? domain = appConfigCubit.state.cloudFlareDomain;
|
||||||
|
final String? ipAddress = appConfigCubit.state.hetznerServer?.ip4;
|
||||||
|
final dkimPublicKey = await api.getDkim();
|
||||||
|
await cloudflare.removeSimilarRecords(cloudFlareDomain: domain!);
|
||||||
|
await cloudflare.createMultipleDnsRecords(
|
||||||
|
cloudFlareDomain: domain, ip4: ipAddress);
|
||||||
|
await cloudflare.setDkim(dkimPublicKey, domain);
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DesiredDnsRecord> _getDesiredDnsRecords(
|
||||||
|
String? domainName, String? ipAddress, String? dkimPublicKey) {
|
||||||
|
if (domainName == null || ipAddress == null || dkimPublicKey == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: domainName,
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.root',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'api.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.api',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'cloud.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.cloud',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'git.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.git',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'meet.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.meet',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'social.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.social',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'password.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.password',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'vpn.$domainName',
|
||||||
|
content: ipAddress,
|
||||||
|
description: 'providers.domain.record_description.vpn',
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: domainName,
|
||||||
|
content: domainName,
|
||||||
|
description: 'providers.domain.record_description.mx',
|
||||||
|
type: 'MX',
|
||||||
|
category: DnsRecordsCategory.email,
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: '_dmarc.$domainName',
|
||||||
|
content: 'v=DMARC1; p=none',
|
||||||
|
description: 'providers.domain.record_description.dmarc',
|
||||||
|
type: 'TXT',
|
||||||
|
category: DnsRecordsCategory.email,
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: domainName,
|
||||||
|
content: 'v=spf1 a mx ip4:$ipAddress -all',
|
||||||
|
description: 'providers.domain.record_description.spf',
|
||||||
|
type: 'TXT',
|
||||||
|
category: DnsRecordsCategory.email,
|
||||||
|
),
|
||||||
|
DesiredDnsRecord(
|
||||||
|
name: 'selector._domainkey.$domainName',
|
||||||
|
content: dkimPublicKey,
|
||||||
|
description: 'providers.domain.record_description.dkim',
|
||||||
|
type: 'TXT',
|
||||||
|
category: DnsRecordsCategory.email,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
part of 'dns_records_cubit.dart';
|
||||||
|
|
||||||
|
enum DnsRecordsStatus {
|
||||||
|
uninitialized,
|
||||||
|
refreshing,
|
||||||
|
good,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DnsRecordsCategory {
|
||||||
|
services,
|
||||||
|
email,
|
||||||
|
other,
|
||||||
|
}
|
||||||
|
|
||||||
|
class DnsRecordsState extends AppConfigDependendState {
|
||||||
|
const DnsRecordsState({
|
||||||
|
this.dnsState = DnsRecordsStatus.uninitialized,
|
||||||
|
this.dnsRecords = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
final DnsRecordsStatus dnsState;
|
||||||
|
final List<DesiredDnsRecord> dnsRecords;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [
|
||||||
|
dnsState,
|
||||||
|
dnsRecords,
|
||||||
|
];
|
||||||
|
|
||||||
|
DnsRecordsState copyWith({
|
||||||
|
DnsRecordsStatus? dnsState,
|
||||||
|
List<DesiredDnsRecord>? dnsRecords,
|
||||||
|
}) {
|
||||||
|
return DnsRecordsState(
|
||||||
|
dnsState: dnsState ?? this.dnsState,
|
||||||
|
dnsRecords: dnsRecords ?? this.dnsRecords,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DesiredDnsRecord {
|
||||||
|
const DesiredDnsRecord({
|
||||||
|
required this.name,
|
||||||
|
this.type = "A",
|
||||||
|
required this.content,
|
||||||
|
this.description = '',
|
||||||
|
this.category = DnsRecordsCategory.services,
|
||||||
|
this.isSatisfied = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final String type;
|
||||||
|
final String content;
|
||||||
|
final String description;
|
||||||
|
final DnsRecordsCategory category;
|
||||||
|
final bool isSatisfied;
|
||||||
|
|
||||||
|
DesiredDnsRecord copyWith({
|
||||||
|
String? name,
|
||||||
|
String? type,
|
||||||
|
String? content,
|
||||||
|
String? description,
|
||||||
|
DnsRecordsCategory? category,
|
||||||
|
bool? isSatisfied,
|
||||||
|
}) {
|
||||||
|
return DesiredDnsRecord(
|
||||||
|
name: name ?? this.name,
|
||||||
|
type: type ?? this.type,
|
||||||
|
content: content ?? this.content,
|
||||||
|
description: description ?? this.description,
|
||||||
|
category: category ?? this.category,
|
||||||
|
isSatisfied: isSatisfied ?? this.isSatisfied,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,8 +3,8 @@ import 'package:json_annotation/json_annotation.dart';
|
||||||
part 'dns_records.g.dart';
|
part 'dns_records.g.dart';
|
||||||
|
|
||||||
@JsonSerializable(createToJson: true, createFactory: false)
|
@JsonSerializable(createToJson: true, createFactory: false)
|
||||||
class DnsRecords {
|
class DnsRecord {
|
||||||
DnsRecords({
|
DnsRecord({
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.content,
|
required this.content,
|
||||||
|
|
|
@ -6,7 +6,7 @@ part of 'dns_records.dart';
|
||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
Map<String, dynamic> _$DnsRecordsToJson(DnsRecords instance) =>
|
Map<String, dynamic> _$DnsRecordsToJson(DnsRecord instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'type': instance.type,
|
'type': instance.type,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
|
|
|
@ -0,0 +1,219 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
||||||
|
|
||||||
|
class DnsDetailsPage extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_DnsDetailsPageState createState() => _DnsDetailsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DnsDetailsPageState extends State<DnsDetailsPage> {
|
||||||
|
Widget _getStateCard(DnsRecordsStatus dnsState, Function fixCallback) {
|
||||||
|
var description = '';
|
||||||
|
var subtitle = '';
|
||||||
|
var icon = Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: Colors.green,
|
||||||
|
);
|
||||||
|
switch (dnsState) {
|
||||||
|
case DnsRecordsStatus.uninitialized:
|
||||||
|
description = 'providers.domain.states.uninitialized'.tr();
|
||||||
|
icon = Icon(
|
||||||
|
Icons.refresh,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case DnsRecordsStatus.refreshing:
|
||||||
|
description = 'providers.domain.states.refreshing'.tr();
|
||||||
|
icon = Icon(
|
||||||
|
Icons.refresh,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case DnsRecordsStatus.good:
|
||||||
|
description = 'providers.domain.states.ok'.tr();
|
||||||
|
icon = Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: Colors.green,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case DnsRecordsStatus.error:
|
||||||
|
description = 'providers.domain.states.error'.tr();
|
||||||
|
subtitle = 'providers.domain.states.error_subtitle'.tr();
|
||||||
|
icon = Icon(
|
||||||
|
Icons.error,
|
||||||
|
color: Colors.red,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return ListTile(
|
||||||
|
onTap: dnsState == DnsRecordsStatus.error ? () => fixCallback() : null,
|
||||||
|
title: Text(
|
||||||
|
description,
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
subtitle: subtitle != '' ? Text(subtitle) : null,
|
||||||
|
leading: icon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
|
||||||
|
final domain = getIt<ApiConfigModel>().cloudFlareDomain?.domainName ?? '';
|
||||||
|
var dnsCubit = context.watch<DnsRecordsCubit>().state;
|
||||||
|
|
||||||
|
print(dnsCubit.dnsState);
|
||||||
|
|
||||||
|
if (!isReady) {
|
||||||
|
return BrandHeroScreen(
|
||||||
|
hasBackButton: true,
|
||||||
|
headerTitle: '',
|
||||||
|
heroIcon: BrandIcons.globe,
|
||||||
|
heroTitle: 'providers.domain.screen_title'.tr(),
|
||||||
|
children: <Widget>[
|
||||||
|
BrandCards.outlined(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
'not_ready_card.in_menu'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BrandHeroScreen(
|
||||||
|
hasBackButton: true,
|
||||||
|
heroSubtitle: domain,
|
||||||
|
heroIcon: BrandIcons.globe,
|
||||||
|
heroTitle: 'providers.domain.screen_title'.tr(),
|
||||||
|
children: <Widget>[
|
||||||
|
BrandCards.outlined(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_getStateCard(dnsCubit.dnsState, () {
|
||||||
|
context.read<DnsRecordsCubit>().fix();
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 16.0),
|
||||||
|
// Outlined card with a list of A records and their
|
||||||
|
// status.
|
||||||
|
BrandCards.outlined(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
'providers.domain.cards.services.title'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'providers.domain.cards.services.subtitle'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.caption,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...dnsCubit.dnsRecords
|
||||||
|
.where(
|
||||||
|
(dnsRecord) =>
|
||||||
|
dnsRecord.category == DnsRecordsCategory.services,
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(dnsRecord) => Column(
|
||||||
|
children: [
|
||||||
|
Divider(
|
||||||
|
height: 1.0,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
dnsRecord.isSatisfied
|
||||||
|
? Icons.check
|
||||||
|
: dnsCubit.dnsState ==
|
||||||
|
DnsRecordsStatus.refreshing
|
||||||
|
? Icons.refresh
|
||||||
|
: Icons.error,
|
||||||
|
color: dnsRecord.isSatisfied
|
||||||
|
? Colors.green
|
||||||
|
: dnsCubit.dnsState ==
|
||||||
|
DnsRecordsStatus.refreshing
|
||||||
|
? Colors.grey
|
||||||
|
: Colors.red,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
dnsRecord.description.tr(),
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
dnsRecord.name,
|
||||||
|
style: Theme.of(context).textTheme.caption,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16.0),
|
||||||
|
BrandCards.outlined(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
'providers.domain.cards.email.title'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'providers.domain.cards.email.subtitle'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.caption,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...dnsCubit.dnsRecords
|
||||||
|
.where(
|
||||||
|
(dnsRecord) =>
|
||||||
|
dnsRecord.category == DnsRecordsCategory.email,
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(dnsRecord) => Column(
|
||||||
|
children: [
|
||||||
|
Divider(
|
||||||
|
height: 1.0,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
dnsRecord.isSatisfied
|
||||||
|
? Icons.check
|
||||||
|
: dnsCubit.dnsState ==
|
||||||
|
DnsRecordsStatus.refreshing
|
||||||
|
? Icons.refresh
|
||||||
|
: Icons.error,
|
||||||
|
color: dnsRecord.isSatisfied
|
||||||
|
? Colors.green
|
||||||
|
: dnsCubit.dnsState ==
|
||||||
|
DnsRecordsStatus.refreshing
|
||||||
|
? Colors.grey
|
||||||
|
: Colors.red,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
dnsRecord.description.tr(),
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,10 +3,9 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:selfprivacy/config/brand_theme.dart';
|
import 'package:selfprivacy/config/brand_theme.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/models/provider.dart';
|
import 'package:selfprivacy/logic/models/provider.dart';
|
||||||
import 'package:selfprivacy/logic/models/state_types.dart';
|
|
||||||
import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart';
|
import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
|
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
|
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
|
||||||
|
@ -15,9 +14,9 @@ import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart
|
||||||
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
|
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
|
||||||
import 'package:selfprivacy/ui/helpers/modals.dart';
|
import 'package:selfprivacy/ui/helpers/modals.dart';
|
||||||
import 'package:selfprivacy/ui/pages/backup_details/backup_details.dart';
|
import 'package:selfprivacy/ui/pages/backup_details/backup_details.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/dns_details/dns_details.dart';
|
||||||
import 'package:selfprivacy/ui/pages/server_details/server_details.dart';
|
import 'package:selfprivacy/ui/pages/server_details/server_details.dart';
|
||||||
import 'package:selfprivacy/utils/route_transitions/basic.dart';
|
import 'package:selfprivacy/utils/route_transitions/basic.dart';
|
||||||
import 'package:selfprivacy/utils/ui_helpers.dart';
|
|
||||||
|
|
||||||
var navigatorKey = GlobalKey<NavigatorState>();
|
var navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
@ -33,6 +32,18 @@ class _ProvidersPageState extends State<ProvidersPage> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
|
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
|
||||||
var isBackupInitialized = context.watch<BackupsCubit>().state.isInitialized;
|
var isBackupInitialized = context.watch<BackupsCubit>().state.isInitialized;
|
||||||
|
var dnsStatus = context.watch<DnsRecordsCubit>().state.dnsState;
|
||||||
|
|
||||||
|
StateType getDnsStatus() {
|
||||||
|
if (dnsStatus == DnsRecordsStatus.uninitialized ||
|
||||||
|
dnsStatus == DnsRecordsStatus.refreshing) {
|
||||||
|
return StateType.uninitialized;
|
||||||
|
}
|
||||||
|
if (dnsStatus == DnsRecordsStatus.error) {
|
||||||
|
return StateType.warning;
|
||||||
|
}
|
||||||
|
return StateType.stable;
|
||||||
|
}
|
||||||
|
|
||||||
final cards = ProviderType.values
|
final cards = ProviderType.values
|
||||||
.map(
|
.map(
|
||||||
|
@ -43,7 +54,9 @@ class _ProvidersPageState extends State<ProvidersPage> {
|
||||||
state: isReady
|
state: isReady
|
||||||
? (type == ProviderType.backup && !isBackupInitialized
|
? (type == ProviderType.backup && !isBackupInitialized
|
||||||
? StateType.uninitialized
|
? StateType.uninitialized
|
||||||
: StateType.stable)
|
: (type == ProviderType.domain)
|
||||||
|
? getDnsStatus()
|
||||||
|
: StateType.stable)
|
||||||
: StateType.uninitialized,
|
: StateType.uninitialized,
|
||||||
type: type,
|
type: type,
|
||||||
),
|
),
|
||||||
|
@ -103,19 +116,13 @@ class _Card extends StatelessWidget {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ProviderType.domain:
|
case ProviderType.domain:
|
||||||
title = 'providers.domain.card_title'.tr();
|
title = 'providers.domain.screen_title'.tr();
|
||||||
message = domainName;
|
message = domainName;
|
||||||
stableText = 'providers.domain.status'.tr();
|
stableText = 'providers.domain.status'.tr();
|
||||||
|
|
||||||
onTap = () => showBrandBottomSheet<void>(
|
onTap = () => Navigator.of(context).push(materialRoute(
|
||||||
context: context,
|
DnsDetailsPage(),
|
||||||
builder: (BuildContext context) {
|
));
|
||||||
return _ProviderDetails(
|
|
||||||
provider: provider,
|
|
||||||
statusText: stableText,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case ProviderType.backup:
|
case ProviderType.backup:
|
||||||
title = 'providers.backup.card_title'.tr();
|
title = 'providers.backup.card_title'.tr();
|
||||||
|
@ -150,77 +157,3 @@ class _Card extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProviderDetails extends StatelessWidget {
|
|
||||||
const _ProviderDetails({
|
|
||||||
Key? key,
|
|
||||||
required this.provider,
|
|
||||||
required this.statusText,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final ProviderModel provider;
|
|
||||||
final String? statusText;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
late String title;
|
|
||||||
late List<Widget> children;
|
|
||||||
|
|
||||||
var config = context.watch<AppConfigCubit>().state;
|
|
||||||
|
|
||||||
var domainName = UiHelpers.getDomainName(config);
|
|
||||||
|
|
||||||
switch (provider.type) {
|
|
||||||
case ProviderType.server:
|
|
||||||
throw ('wrong type');
|
|
||||||
case ProviderType.domain:
|
|
||||||
title = 'providers.domain.card_title'.tr();
|
|
||||||
children = [
|
|
||||||
BrandText.body1('providers.domain.bottom_sheet.1'.tr()),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
BrandText.body1(domainName),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
BrandText.body1('providers.domain.status'.tr()),
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
case ProviderType.backup:
|
|
||||||
title = 'providers.backup.card_title'.tr();
|
|
||||||
children = [
|
|
||||||
BrandText.body1('providers.backup.bottom_sheet.1'.tr()),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
BrandText.body1(
|
|
||||||
'providers.backup.bottom_sheet.2'.tr(args: [domainName, 'Time'])),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
BrandText.body1('providers.backup.status'.tr()),
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return BrandBottomSheet(
|
|
||||||
child: SafeArea(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(height: 40),
|
|
||||||
Padding(
|
|
||||||
padding: paddingH15V0,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
IconStatusMask(
|
|
||||||
status: provider.state,
|
|
||||||
child: Icon(provider.icon, size: 40, color: Colors.white),
|
|
||||||
),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
BrandText.h1(title),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
...children,
|
|
||||||
SizedBox(height: 30),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue