refactor: Implement Cloudflare objects to avoid usage of dynamic blobs

- Get rid of ZoneId term outside of Cloudflare
pull/268/head
NaiJi ✨ 2023-07-31 23:40:25 -03:00
parent 11e745f822
commit b2c67c80bd
12 changed files with 296 additions and 136 deletions

View File

@ -4,8 +4,7 @@ import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/models/json/cloudflare_dns_info.dart';
class CloudflareApi extends RestApiMap {
CloudflareApi({
@ -92,9 +91,9 @@ class CloudflareApi extends RestApiMap {
);
}
Future<GenericResult<List>> getDomains() async {
Future<GenericResult<List<CloudflareZone>>> getZones() async {
final String url = '$rootAddress/zones';
List domains = [];
List<CloudflareZone> domains = [];
late final Response? response;
final Dio client = await getClient();
@ -103,7 +102,11 @@ class CloudflareApi extends RestApiMap {
url,
queryParameters: {'per_page': 50},
);
domains = response.data['result'];
domains = response.data['result']!
.map<CloudflareZone>(
(final json) => CloudflareZone.fromJson(json),
)
.toList();
} catch (e) {
print(e);
return GenericResult(
@ -125,18 +128,17 @@ class CloudflareApi extends RestApiMap {
}
Future<GenericResult<void>> createMultipleDnsRecords({
required final ServerDomain domain,
required final List<DnsRecord> records,
required final String zoneId,
required final List<CloudflareDnsRecord> records,
}) async {
final String domainZoneId = domain.zoneId;
final List<Future> allCreateFutures = <Future>[];
final Dio client = await getClient();
try {
for (final DnsRecord record in records) {
for (final CloudflareDnsRecord record in records) {
allCreateFutures.add(
client.post(
'/zones/$domainZoneId/dns_records',
'/zones/$zoneId/dns_records',
data: record.toJson(),
),
);
@ -160,11 +162,10 @@ class CloudflareApi extends RestApiMap {
}
Future<GenericResult<void>> removeSimilarRecords({
required final ServerDomain domain,
required final List records,
required final String zoneId,
required final List<CloudflareDnsRecord> records,
}) async {
final String domainZoneId = domain.zoneId;
final String url = '/zones/$domainZoneId/dns_records';
final String url = '/zones/$zoneId/dns_records';
final Dio client = await getClient();
try {
@ -172,7 +173,7 @@ class CloudflareApi extends RestApiMap {
for (final record in records) {
allDeleteFutures.add(
client.delete('$url/${record["id"]}'),
client.delete('$url/${record.id}'),
);
}
await Future.wait(allDeleteFutures);
@ -190,26 +191,21 @@ class CloudflareApi extends RestApiMap {
return GenericResult(success: true, data: null);
}
Future<GenericResult<List>> getDnsRecords({
required final ServerDomain domain,
Future<GenericResult<List<CloudflareDnsRecord>>> getDnsRecords({
required final String zoneId,
}) async {
Response response;
final String domainName = domain.domainName;
final String domainZoneId = domain.zoneId;
final List allRecords = [];
final String url = '/zones/$domainZoneId/dns_records';
List<CloudflareDnsRecord> allRecords = [];
final String url = '/zones/$zoneId/dns_records';
final Dio client = await getClient();
try {
response = await client.get(url);
final List records = response.data['result'] ?? [];
for (final record in records) {
if (record['zone_name'] == domainName) {
allRecords.add(record);
}
}
allRecords = response.data['result']!
.map<CloudflareDnsRecord>(
(final json) => CloudflareDnsRecord.fromJson(json),
)
.toList();
} catch (e) {
print(e);
return GenericResult(
@ -223,30 +219,4 @@ class CloudflareApi extends RestApiMap {
return GenericResult(data: allRecords, success: true);
}
Future<GenericResult<List<dynamic>>> getZones(final String domain) async {
List zones = [];
late final Response? response;
final Dio client = await getClient();
try {
response = await client.get(
'/zones',
queryParameters: {'name': domain},
);
zones = response.data['result'];
} catch (e) {
print(e);
GenericResult(
success: false,
data: zones,
code: response?.statusCode,
message: response?.statusMessage,
);
} finally {
close(client);
}
return GenericResult(success: true, data: zones);
}
}

View File

@ -25,23 +25,17 @@ class DomainSetupCubit extends Cubit<DomainSetupState> {
Future<void> saveDomain() async {
assert(state is Loaded, 'wrong state');
final String domainName = (state as Loaded).domain;
emit(Loading(LoadingTypes.saving));
final dnsProvider = ProvidersController.currentDnsProvider!;
final GenericResult<String?> zoneIdResult =
await dnsProvider.getZoneId(domainName);
if (zoneIdResult.success || zoneIdResult.data != null) {
final ServerDomain domain = ServerDomain(
domainName: domainName,
zoneId: zoneIdResult.data!,
provider: dnsProvider.type,
);
final ServerDomain domain = ServerDomain(
domainName: domainName,
provider: dnsProvider.type,
);
serverInstallationCubit.setDomain(domain);
emit(DomainSet());
}
serverInstallationCubit.setDomain(domain);
emit(DomainSet());
}
}

View File

@ -467,7 +467,6 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
final ServerDomain serverDomain = ServerDomain(
domainName: domain,
provider: DnsProviderType.unknown,
zoneId: '',
);
final ServerRecoveryCapabilities recoveryCapabilities =
await repository.getRecoveryCapabilities(serverDomain);
@ -681,9 +680,9 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
if (serverDomain == null) {
return;
}
final String? zoneId =
await repository.getDomainId(token, serverDomain.domainName);
if (zoneId == null) {
final isTokenValid =
await repository.validateDnsToken(token, serverDomain.domainName);
if (!isTokenValid) {
getIt<NavigationService>()
.showSnackBar('recovering.domain_not_available_on_token'.tr());
return;
@ -695,16 +694,13 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
await repository.saveDomain(
ServerDomain(
domainName: serverDomain.domainName,
zoneId: zoneId,
provider: dnsProviderType,
),
);
// await repository.setDnsApiToken(token);
emit(
dataState.copyWith(
serverDomain: ServerDomain(
domainName: serverDomain.domainName,
zoneId: zoneId,
provider: dnsProviderType,
),
dnsApiToken: token,

View File

@ -193,17 +193,23 @@ class ServerInstallationRepository {
return server;
}
Future<String?> getDomainId(final String token, final String domain) async {
Future<bool> validateDnsToken(
final String token,
final String domain,
) async {
final result =
await ProvidersController.currentDnsProvider!.tryInitApiByToken(token);
if (!result.success) {
return null;
return false;
}
await setDnsApiToken(token);
return (await ProvidersController.currentDnsProvider!.getZoneId(
domain,
))
.data;
final domainResult =
await ProvidersController.currentDnsProvider!.domainList();
if (!domainResult.success || domainResult.data.isEmpty) {
return false;
}
return domain == domainResult.data[0];
}
Future<Map<String, bool>> isDnsAddressesMatch(

View File

@ -7,21 +7,16 @@ part 'server_domain.g.dart';
class ServerDomain {
ServerDomain({
required this.domainName,
required this.zoneId,
required this.provider,
});
@HiveField(0)
final String domainName;
@HiveField(1)
final String zoneId;
// @HiveField(1)
@HiveField(2, defaultValue: DnsProviderType.cloudflare)
final DnsProviderType provider;
@override
String toString() => '$domainName: $zoneId';
}
@HiveType(typeId: 100)

View File

@ -18,7 +18,6 @@ class ServerDomainAdapter extends TypeAdapter<ServerDomain> {
};
return ServerDomain(
domainName: fields[0] as String,
zoneId: fields[1] as String,
provider: fields[2] == null
? DnsProviderType.cloudflare
: fields[2] as DnsProviderType,
@ -28,11 +27,9 @@ class ServerDomainAdapter extends TypeAdapter<ServerDomain> {
@override
void write(BinaryWriter writer, ServerDomain obj) {
writer
..writeByte(3)
..writeByte(2)
..writeByte(0)
..write(obj.domainName)
..writeByte(1)
..write(obj.zoneId)
..writeByte(2)
..write(obj.provider);
}

View File

@ -0,0 +1,86 @@
import 'package:json_annotation/json_annotation.dart';
part 'cloudflare_dns_info.g.dart';
/// https://developers.cloudflare.com/api/operations/zones-get
@JsonSerializable()
class CloudflareZone {
CloudflareZone({
required this.id,
required this.name,
});
/// Zone identifier
///
/// `<= 32 characters`
///
/// Example: 023e105f4ecef8ad9ca31a8372d0c353
final String id;
/// The domain name
///
/// `<= 253 characters`
///
/// Example: example.com
final String name;
static CloudflareZone fromJson(final Map<String, dynamic> json) =>
_$CloudflareZoneFromJson(json);
}
/// https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-list-dns-records
@JsonSerializable()
class CloudflareDnsRecord {
CloudflareDnsRecord({
required this.type,
required this.name,
required this.content,
required this.zoneName,
this.ttl = 3600,
this.priority = 10,
this.id,
});
/// Record identifier
///
/// `<= 32 characters`
/// Example: 023e105f4ecef8ad9ca31a8372d0c353
final String? id;
/// Record type.
///
/// Example: A
final String type;
/// DNS record name (or @ for the zone apex) in Punycode.
///
/// `<= 255 characters`
///
/// Example: example.com
final String? name;
/// Valid DNS Record string content.
///
/// Example: A valid IPv4 address "198.51.100.4"
final String? content;
/// The domain of the record.
///
/// Example: example.com
@JsonKey(name: 'zone_name')
final String zoneName;
/// Time To Live (TTL) of the DNS record in seconds. Setting to 1 means 'automatic'.
///
/// Value must be between 60 and 86400, with the minimum reduced to 30 for Enterprise zones.
final int ttl;
/// Required for MX, SRV and URI records; unused by other record types. Records with lower priorities are preferred.
///
/// `>= 0 <= 65535`
final int priority;
static CloudflareDnsRecord fromJson(final Map<String, dynamic> json) =>
_$CloudflareDnsRecordFromJson(json);
Map<String, dynamic> toJson() => _$CloudflareDnsRecordToJson(this);
}

View File

@ -0,0 +1,42 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'cloudflare_dns_info.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CloudflareZone _$CloudflareZoneFromJson(Map<String, dynamic> json) =>
CloudflareZone(
id: json['id'] as String,
name: json['name'] as String,
);
Map<String, dynamic> _$CloudflareZoneToJson(CloudflareZone instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
};
CloudflareDnsRecord _$CloudflareDnsRecordFromJson(Map<String, dynamic> json) =>
CloudflareDnsRecord(
type: json['type'] as String,
name: json['name'] as String?,
content: json['content'] as String?,
zoneName: json['zone_name'] as String,
ttl: json['ttl'] as int? ?? 3600,
priority: json['priority'] as int? ?? 10,
id: json['id'] as String?,
);
Map<String, dynamic> _$CloudflareDnsRecordToJson(
CloudflareDnsRecord instance) =>
<String, dynamic>{
'id': instance.id,
'type': instance.type,
'name': instance.name,
'content': instance.content,
'zone_name': instance.zoneName,
'ttl': instance.ttl,
'priority': instance.priority,
};

View File

@ -1,12 +1,16 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_api.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/cloudflare_dns_info.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
class ApiAdapter {
ApiAdapter({final bool isWithToken = true})
: _api = CloudflareApi(
ApiAdapter({
final bool isWithToken = true,
this.cachedDomain = '',
this.cachedZoneId = '',
}) : _api = CloudflareApi(
isWithToken: isWithToken,
);
@ -17,6 +21,8 @@ class ApiAdapter {
);
final CloudflareApi _api;
final String cachedZoneId;
final String cachedDomain;
}
class CloudflareDnsProvider extends DnsProvider {
@ -47,7 +53,7 @@ class CloudflareDnsProvider extends DnsProvider {
@override
Future<GenericResult<List<String>>> domainList() async {
List<String> domains = [];
final result = await _adapter.api().getDomains();
final result = await _adapter.api().getZones();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
@ -59,10 +65,18 @@ class CloudflareDnsProvider extends DnsProvider {
domains = result.data
.map<String>(
(final el) => el['name'] as String,
(final el) => el.name,
)
.toList();
/// TODO: Remove when domain selection for more than one domain on account is implemented, move cachedZoneId writing to domain saving method
_adapter = ApiAdapter(
isWithToken: true,
cachedDomain: result.data[0].name,
cachedZoneId: result.data[0].id,
);
print('cachedZoneId saved for the first time!!!');
return GenericResult(
success: true,
data: domains,
@ -73,11 +87,27 @@ class CloudflareDnsProvider extends DnsProvider {
Future<GenericResult<void>> createDomainRecords({
required final ServerDomain domain,
final String? ip4,
}) {
}) async {
final syncZoneIdResult = await syncZoneId(domain.domainName);
if (!syncZoneIdResult.success) {
return syncZoneIdResult;
}
final records = getProjectDnsRecords(domain.domainName, ip4);
return _adapter.api().createMultipleDnsRecords(
domain: domain,
records: records,
zoneId: _adapter.cachedZoneId,
records: records
.map<CloudflareDnsRecord>(
(final rec) => CloudflareDnsRecord(
content: rec.content,
name: rec.name,
type: rec.type,
zoneName: domain.domainName,
id: null,
ttl: rec.ttl,
),
)
.toList(),
);
}
@ -86,7 +116,13 @@ class CloudflareDnsProvider extends DnsProvider {
required final ServerDomain domain,
final String? ip4,
}) async {
final result = await _adapter.api().getDnsRecords(domain: domain);
final syncZoneIdResult = await syncZoneId(domain.domainName);
if (!syncZoneIdResult.success) {
return syncZoneIdResult;
}
final result =
await _adapter.api().getDnsRecords(zoneId: _adapter.cachedZoneId);
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
@ -97,7 +133,7 @@ class CloudflareDnsProvider extends DnsProvider {
}
return _adapter.api().removeSimilarRecords(
domain: domain,
zoneId: _adapter.cachedZoneId,
records: result.data,
);
}
@ -106,8 +142,19 @@ class CloudflareDnsProvider extends DnsProvider {
Future<GenericResult<List<DnsRecord>>> getDnsRecords({
required final ServerDomain domain,
}) async {
final syncZoneIdResult = await syncZoneId(domain.domainName);
if (!syncZoneIdResult.success) {
return GenericResult(
success: syncZoneIdResult.success,
data: [],
code: syncZoneIdResult.code,
message: syncZoneIdResult.message,
);
}
final List<DnsRecord> records = [];
final result = await _adapter.api().getDnsRecords(domain: domain);
final result =
await _adapter.api().getDnsRecords(zoneId: _adapter.cachedZoneId);
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
@ -120,11 +167,10 @@ class CloudflareDnsProvider extends DnsProvider {
for (final rawRecord in result.data) {
records.add(
DnsRecord(
name: rawRecord['name'],
type: rawRecord['type'],
content: rawRecord['content'],
ttl: rawRecord['ttl'],
proxied: rawRecord['proxied'],
name: rawRecord.name,
type: rawRecord.type,
content: rawRecord.content,
ttl: rawRecord.ttl,
),
);
}
@ -139,11 +185,26 @@ class CloudflareDnsProvider extends DnsProvider {
Future<GenericResult<void>> setDnsRecord(
final DnsRecord record,
final ServerDomain domain,
) async =>
_adapter.api().createMultipleDnsRecords(
domain: domain,
records: [record],
);
) async {
final syncZoneIdResult = await syncZoneId(domain.domainName);
if (!syncZoneIdResult.success) {
return syncZoneIdResult;
}
return _adapter.api().createMultipleDnsRecords(
zoneId: _adapter.cachedZoneId,
records: [
CloudflareDnsRecord(
content: record.content,
id: null,
name: record.name,
type: record.type,
zoneName: domain.domainName,
ttl: record.ttl,
),
],
);
}
@override
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
@ -336,10 +397,41 @@ class CloudflareDnsProvider extends DnsProvider {
];
}
@override
Future<GenericResult<void>> syncZoneId(final String domain) async {
if (domain == _adapter.cachedDomain && _adapter.cachedZoneId.isNotEmpty) {
return GenericResult(
success: true,
data: null,
);
}
print('syncZoneId!!!');
final getZoneIdResult = await getZoneId(domain);
if (!getZoneIdResult.success || getZoneIdResult.data == null) {
return GenericResult(
success: false,
data: null,
code: getZoneIdResult.code,
message: getZoneIdResult.message,
);
}
_adapter = ApiAdapter(
isWithToken: true,
cachedDomain: domain,
cachedZoneId: getZoneIdResult.data!,
);
return GenericResult(
success: true,
data: null,
);
}
Future<GenericResult<String?>> getZoneId(final String domain) async {
String? id;
final result = await _adapter.api().getZones(domain);
final result = await _adapter.api().getZones();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
@ -349,8 +441,12 @@ class CloudflareDnsProvider extends DnsProvider {
);
}
id = result.data[0]['id'];
for (final availableDomain in result.data) {
if (availableDomain.name == domain) {
id = availableDomain.id;
}
}
return GenericResult(success: true, data: id);
return GenericResult(success: id != null, data: id);
}
}

View File

@ -408,11 +408,4 @@ class DesecDnsProvider extends DnsProvider {
),
];
}
@override
Future<GenericResult<String?>> getZoneId(final String domain) async =>
GenericResult(
data: domain,
success: true,
);
}

View File

@ -369,11 +369,4 @@ class DigitalOceanDnsProvider extends DnsProvider {
),
];
}
@override
Future<GenericResult<String?>> getZoneId(final String domain) async =>
GenericResult(
data: domain,
success: true,
);
}

View File

@ -73,12 +73,4 @@ abstract class DnsProvider {
final String? ip4,
final String? dkimPublicKey,
);
/// Tries to access zone of requested domain.
///
/// If a DNS provider doesn't support zones,
/// will return domain without any changes.
///
/// If success, returns an initializing string of zone id.
Future<GenericResult<String?>> getZoneId(final String domain);
}