chore: Create infrastructure for Digital Ocean DNS provider

Also rename hardcoded cloudflare names from backend
pull/213/head
NaiJi ✨ 2022-12-17 14:26:19 +04:00
parent 968667e4bf
commit 18d3039dc4
11 changed files with 371 additions and 46 deletions

View File

@ -1,5 +1,6 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_factory.dart';
@ -32,6 +33,8 @@ class ApiFactoryCreator {
switch (settings.provider) {
case DnsProvider.cloudflare:
return CloudflareApiFactory();
case DnsProvider.digitalOcean:
return DigitalOceanDnsApiFactory();
case DnsProvider.unknown:
throw UnknownApiProviderException('Unknown DNS provider');
}

View File

@ -27,7 +27,7 @@ class CloudflareApi extends DnsProviderApi {
BaseOptions get options {
final BaseOptions options = BaseOptions(baseUrl: rootAddress);
if (isWithToken) {
final String? token = getIt<ApiConfigModel>().cloudFlareKey;
final String? token = getIt<ApiConfigModel>().dnsProviderKey;
assert(token != null);
options.headers = {'Authorization': 'Bearer $token'};
}

View File

@ -0,0 +1,304 @@
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 DigitalOceanDnsApi extends DnsProviderApi {
DigitalOceanDnsApi({
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': 'Bearer $token'};
}
if (customToken != null) {
options.headers = {'Authorization': 'Bearer $customToken'};
}
if (validateStatus != null) {
options.validateStatus = validateStatus!;
}
return options;
}
@override
String rootAddress = 'https://api.digitalocean.com/v2';
@override
Future<APIGenericResult<bool>> isApiTokenValid(final String token) async {
bool isValid = false;
Response? response;
String message = '';
final Dio client = await getClient();
try {
response = await client.get(
'/account',
options: Options(
followRedirects: false,
validateStatus: (final status) =>
status != null && (status >= 200 || status == 401),
headers: {'Authorization': 'Bearer $token'},
),
);
} catch (e) {
print(e);
isValid = false;
message = e.toString();
} finally {
close(client);
}
if (response == null) {
return APIGenericResult(
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 APIGenericResult(
data: isValid,
success: true,
message: response.statusMessage,
);
}
@override
// TODO: Remove from DnsProviderInterface, stub for now
Future<String?> getZoneId(final String domain) async => domain;
@override
Future<APIGenericResult<void>> removeSimilarRecords({
required final ServerDomain domain,
final String? ip4,
}) async {
final String domainName = domain.domainName;
final String domainZoneId = domain.zoneId;
final String url = '/zones/$domainZoneId/dns_records';
final Dio client = await getClient();
try {
final Response response = await client.get(url);
final List records = response.data['result'] ?? [];
final List<Future> allDeleteFutures = <Future>[];
for (final record in records) {
if (record['zone_name'] == domainName) {
allDeleteFutures.add(
client.delete('$url/${record["id"]}'),
);
}
}
await Future.wait(allDeleteFutures);
} catch (e) {
print(e);
return APIGenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(success: true, data: null);
}
@override
Future<List<DnsRecord>> getDnsRecords({
required final ServerDomain domain,
}) async {
Response response;
final String domainName = domain.domainName;
final String domainZoneId = domain.zoneId;
final List<DnsRecord> allRecords = <DnsRecord>[];
final String url = '/zones/$domainZoneId/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(
DnsRecord(
name: record['name'],
type: record['type'],
content: record['content'],
ttl: record['ttl'],
proxied: record['proxied'],
),
);
}
}
} catch (e) {
print(e);
} finally {
close(client);
}
return allRecords;
}
@override
Future<APIGenericResult<void>> createMultipleDnsRecords({
required final ServerDomain domain,
final String? ip4,
}) async {
final String domainName = domain.domainName;
final String domainZoneId = domain.zoneId;
final List<DnsRecord> listDnsRecords = projectDnsRecords(domainName, ip4);
final List<Future> allCreateFutures = <Future>[];
final Dio client = await getClient();
try {
for (final DnsRecord record in listDnsRecords) {
allCreateFutures.add(
client.post(
'/zones/$domainZoneId/dns_records',
data: record.toJson(),
),
);
}
await Future.wait(allCreateFutures);
} on DioError catch (e) {
print(e.message);
rethrow;
} catch (e) {
print(e);
return APIGenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(success: true, data: null);
}
List<DnsRecord> projectDnsRecords(
final String? domainName,
final String? ip4,
) {
final DnsRecord domainA =
DnsRecord(type: 'A', name: domainName, content: ip4);
final DnsRecord mx = DnsRecord(type: 'MX', name: '@', content: 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: domainName,
content: 'v=spf1 a mx ip4:$ip4 -all',
ttl: 18000,
);
return <DnsRecord>[
domainA,
apiA,
cloudA,
gitA,
meetA,
passwordA,
socialA,
mx,
txt1,
txt2,
vpn
];
}
@override
Future<void> setDnsRecord(
final DnsRecord record,
final ServerDomain domain,
) async {
final String domainZoneId = domain.zoneId;
final String url = '$rootAddress/zones/$domainZoneId/dns_records';
final Dio client = await getClient();
try {
await client.post(
url,
data: record.toJson(),
);
} catch (e) {
print(e);
} finally {
close(client);
}
}
@override
Future<List<String>> domainList() async {
final String url = '$rootAddress/zones';
List<String> domains = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
url,
queryParameters: {'per_page': 50},
);
domains = response.data['result']
.map<String>((final el) => el['name'] as String)
.toList();
} catch (e) {
print(e);
} finally {
close(client);
}
return domains;
}
}

View File

@ -0,0 +1,16 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns.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 DigitalOceanDnsApiFactory extends DnsProviderApiFactory {
@override
DnsProviderApi getDnsProvider({
final DnsProviderApiSettings settings = const DnsProviderApiSettings(),
}) =>
DigitalOceanDnsApi(
hasLogger: settings.hasLogger,
isWithToken: settings.isWithToken,
customToken: settings.customToken,
);
}

View File

@ -20,7 +20,7 @@ class DnsProviderFormCubit extends FormCubit {
@override
FutureOr<void> onSubmit() async {
initializingCubit.setCloudflareKey(apiKey.state.value);
initializingCubit.setDnsApiToken(apiKey.state.value);
}
final ServerInstallationCubit initializingCubit;

View File

@ -214,16 +214,16 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
);
}
void setCloudflareKey(final String cloudFlareKey) async {
void setDnsApiToken(final String dnsApiToken) async {
if (state is ServerInstallationRecovery) {
setAndValidateCloudflareToken(cloudFlareKey);
setAndValidateCloudflareToken(dnsApiToken);
return;
}
await repository.saveCloudFlareKey(cloudFlareKey);
await repository.setDnsApiToken(dnsApiToken);
emit(
(state as ServerInstallationNotFinished)
.copyWith(cloudFlareKey: cloudFlareKey),
.copyWith(dnsApiToken: dnsApiToken),
);
}
@ -284,7 +284,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
await repository.createServer(
state.rootUser!,
state.serverDomain!.domainName,
state.cloudFlareKey!,
state.dnsApiToken!,
state.backblazeCredential!,
onCancel: onCancel,
onSuccess: onSuccess,
@ -586,7 +586,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
),
);
break;
case RecoveryStep.cloudflareToken:
case RecoveryStep.dnsProviderToken:
repository.deleteServerDetails();
emit(
dataState.copyWith(
@ -673,7 +673,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
emit(
dataState.copyWith(
serverDetails: serverDetails,
currentStep: RecoveryStep.cloudflareToken,
currentStep: RecoveryStep.dnsProviderToken,
),
);
}
@ -699,7 +699,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
provider: DnsProvider.cloudflare,
),
);
await repository.saveCloudFlareKey(token);
await repository.setDnsApiToken(token);
emit(
dataState.copyWith(
serverDomain: ServerDomain(
@ -707,7 +707,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
zoneId: zoneId,
provider: DnsProvider.cloudflare,
),
cloudFlareKey: token,
dnsApiToken: token,
currentStep: RecoveryStep.backblazeToken,
),
);
@ -754,7 +754,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
ServerInstallationNotFinished(
providerApiToken: state.providerApiToken,
serverDomain: state.serverDomain,
cloudFlareKey: state.cloudFlareKey,
dnsApiToken: state.dnsApiToken,
backblazeCredential: state.backblazeCredential,
rootUser: state.rootUser,
serverDetails: null,

View File

@ -45,7 +45,7 @@ class ServerInstallationRepository {
Future<ServerInstallationState> load() async {
final String? providerApiToken = getIt<ApiConfigModel>().serverProviderKey;
final String? location = getIt<ApiConfigModel>().serverLocation;
final String? cloudflareToken = getIt<ApiConfigModel>().cloudFlareKey;
final String? cloudflareToken = getIt<ApiConfigModel>().dnsProviderKey;
final String? serverTypeIdentificator = getIt<ApiConfigModel>().serverType;
final ServerDomain? serverDomain = getIt<ApiConfigModel>().serverDomain;
final ServerProvider? serverProvider =
@ -86,7 +86,7 @@ class ServerInstallationRepository {
return ServerInstallationFinished(
providerApiToken: providerApiToken!,
serverTypeIdentificator: serverTypeIdentificator ?? '',
cloudFlareKey: cloudflareToken!,
dnsApiToken: cloudflareToken!,
serverDomain: serverDomain!,
backblazeCredential: backblazeCredential!,
serverDetails: serverDetails!,
@ -103,7 +103,7 @@ class ServerInstallationRepository {
serverDomain != null) {
return ServerInstallationRecovery(
providerApiToken: providerApiToken,
cloudFlareKey: cloudflareToken,
dnsApiToken: cloudflareToken,
serverDomain: serverDomain,
backblazeCredential: backblazeCredential,
serverDetails: serverDetails,
@ -120,7 +120,7 @@ class ServerInstallationRepository {
return ServerInstallationNotFinished(
providerApiToken: providerApiToken,
cloudFlareKey: cloudflareToken,
dnsApiToken: cloudflareToken,
serverDomain: serverDomain,
backblazeCredential: backblazeCredential,
serverDetails: serverDetails,
@ -147,7 +147,7 @@ class ServerInstallationRepository {
if (serverDomain.provider != DnsProvider.unknown) {
return RecoveryStep.backblazeToken;
}
return RecoveryStep.cloudflareToken;
return RecoveryStep.dnsProviderToken;
}
return RecoveryStep.serverSelection;
}
@ -717,8 +717,8 @@ class ServerInstallationRepository {
getIt<ApiConfigModel>().init();
}
Future<void> saveCloudFlareKey(final String key) async {
await getIt<ApiConfigModel>().storeCloudFlareKey(key);
Future<void> setDnsApiToken(final String key) async {
await getIt<ApiConfigModel>().storeDnsProviderKey(key);
}
Future<void> deleteCloudFlareKey() async {

View File

@ -4,7 +4,7 @@ abstract class ServerInstallationState extends Equatable {
const ServerInstallationState({
required this.providerApiToken,
required this.serverTypeIdentificator,
required this.cloudFlareKey,
required this.dnsApiToken,
required this.backblazeCredential,
required this.serverDomain,
required this.rootUser,
@ -18,7 +18,7 @@ abstract class ServerInstallationState extends Equatable {
List<Object?> get props => [
providerApiToken,
serverTypeIdentificator,
cloudFlareKey,
dnsApiToken,
backblazeCredential,
serverDomain,
rootUser,
@ -28,7 +28,7 @@ abstract class ServerInstallationState extends Equatable {
];
final String? providerApiToken;
final String? cloudFlareKey;
final String? dnsApiToken;
final String? serverTypeIdentificator;
final BackblazeCredential? backblazeCredential;
final ServerDomain? serverDomain;
@ -40,7 +40,7 @@ abstract class ServerInstallationState extends Equatable {
bool get isServerProviderApiKeyFilled => providerApiToken != null;
bool get isServerTypeFilled => serverTypeIdentificator != null;
bool get isDnsProviderFilled => cloudFlareKey != null;
bool get isDnsProviderFilled => dnsApiToken != null;
bool get isBackupsProviderFilled => backblazeCredential != null;
bool get isDomainSelected => serverDomain != null;
bool get isPrimaryUserFilled => rootUser != null;
@ -87,7 +87,7 @@ class TimerState extends ServerInstallationNotFinished {
}) : super(
providerApiToken: dataState.providerApiToken,
serverTypeIdentificator: dataState.serverTypeIdentificator,
cloudFlareKey: dataState.cloudFlareKey,
dnsApiToken: dataState.dnsApiToken,
backblazeCredential: dataState.backblazeCredential,
serverDomain: dataState.serverDomain,
rootUser: dataState.rootUser,
@ -114,7 +114,7 @@ enum ServerSetupProgress {
nothingYet,
serverProviderFilled,
servertTypeFilled,
cloudFlareFilled,
dnsProviderFilled,
backblazeFilled,
domainFilled,
userFilled,
@ -133,7 +133,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
required this.dnsMatches,
super.providerApiToken,
super.serverTypeIdentificator,
super.cloudFlareKey,
super.dnsApiToken,
super.backblazeCredential,
super.serverDomain,
super.rootUser,
@ -146,7 +146,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
List<Object?> get props => [
providerApiToken,
serverTypeIdentificator,
cloudFlareKey,
dnsApiToken,
backblazeCredential,
serverDomain,
rootUser,
@ -160,7 +160,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
ServerInstallationNotFinished copyWith({
final String? providerApiToken,
final String? serverTypeIdentificator,
final String? cloudFlareKey,
final String? dnsApiToken,
final BackblazeCredential? backblazeCredential,
final ServerDomain? serverDomain,
final User? rootUser,
@ -175,7 +175,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
providerApiToken: providerApiToken ?? this.providerApiToken,
serverTypeIdentificator:
serverTypeIdentificator ?? this.serverTypeIdentificator,
cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey,
dnsApiToken: dnsApiToken ?? this.dnsApiToken,
backblazeCredential: backblazeCredential ?? this.backblazeCredential,
serverDomain: serverDomain ?? this.serverDomain,
rootUser: rootUser ?? this.rootUser,
@ -192,7 +192,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
ServerInstallationFinished finish() => ServerInstallationFinished(
providerApiToken: providerApiToken!,
serverTypeIdentificator: serverTypeIdentificator ?? '',
cloudFlareKey: cloudFlareKey!,
dnsApiToken: dnsApiToken!,
backblazeCredential: backblazeCredential!,
serverDomain: serverDomain!,
rootUser: rootUser!,
@ -208,7 +208,7 @@ class ServerInstallationEmpty extends ServerInstallationNotFinished {
: super(
providerApiToken: null,
serverTypeIdentificator: null,
cloudFlareKey: null,
dnsApiToken: null,
backblazeCredential: null,
serverDomain: null,
rootUser: null,
@ -225,7 +225,7 @@ class ServerInstallationFinished extends ServerInstallationState {
const ServerInstallationFinished({
required String super.providerApiToken,
required String super.serverTypeIdentificator,
required String super.cloudFlareKey,
required String super.dnsApiToken,
required BackblazeCredential super.backblazeCredential,
required ServerDomain super.serverDomain,
required User super.rootUser,
@ -239,7 +239,7 @@ class ServerInstallationFinished extends ServerInstallationState {
List<Object?> get props => [
providerApiToken,
serverTypeIdentificator,
cloudFlareKey,
dnsApiToken,
backblazeCredential,
serverDomain,
rootUser,
@ -256,7 +256,7 @@ enum RecoveryStep {
oldToken,
serverProviderToken,
serverSelection,
cloudflareToken,
dnsProviderToken,
backblazeToken,
}
@ -278,7 +278,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
required this.recoveryCapabilities,
super.providerApiToken,
super.serverTypeIdentificator,
super.cloudFlareKey,
super.dnsApiToken,
super.backblazeCredential,
super.serverDomain,
super.rootUser,
@ -295,7 +295,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
List<Object?> get props => [
providerApiToken,
serverTypeIdentificator,
cloudFlareKey,
dnsApiToken,
backblazeCredential,
serverDomain,
rootUser,
@ -308,7 +308,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
ServerInstallationRecovery copyWith({
final String? providerApiToken,
final String? serverTypeIdentificator,
final String? cloudFlareKey,
final String? dnsApiToken,
final BackblazeCredential? backblazeCredential,
final ServerDomain? serverDomain,
final User? rootUser,
@ -320,7 +320,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
providerApiToken: providerApiToken ?? this.providerApiToken,
serverTypeIdentificator:
serverTypeIdentificator ?? this.serverTypeIdentificator,
cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey,
dnsApiToken: dnsApiToken ?? this.dnsApiToken,
backblazeCredential: backblazeCredential ?? this.backblazeCredential,
serverDomain: serverDomain ?? this.serverDomain,
rootUser: rootUser ?? this.rootUser,
@ -332,7 +332,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
ServerInstallationFinished finish() => ServerInstallationFinished(
providerApiToken: providerApiToken!,
serverTypeIdentificator: serverTypeIdentificator ?? '',
cloudFlareKey: cloudFlareKey!,
dnsApiToken: dnsApiToken!,
backblazeCredential: backblazeCredential!,
serverDomain: serverDomain!,
rootUser: rootUser!,

View File

@ -12,7 +12,7 @@ class ApiConfigModel {
String? get serverProviderKey => _serverProviderKey;
String? get serverLocation => _serverLocation;
String? get serverType => _serverType;
String? get cloudFlareKey => _cloudFlareKey;
String? get dnsProviderKey => _dnsProviderKey;
ServerProvider? get serverProvider => _serverProvider;
BackblazeCredential? get backblazeCredential => _backblazeCredential;
ServerDomain? get serverDomain => _serverDomain;
@ -20,7 +20,7 @@ class ApiConfigModel {
String? _serverProviderKey;
String? _serverLocation;
String? _cloudFlareKey;
String? _dnsProviderKey;
String? _serverType;
ServerProvider? _serverProvider;
ServerHostingDetails? _serverDetails;
@ -38,9 +38,9 @@ class ApiConfigModel {
_serverProviderKey = value;
}
Future<void> storeCloudFlareKey(final String value) async {
Future<void> storeDnsProviderKey(final String value) async {
await _box.put(BNames.cloudFlareKey, value);
_cloudFlareKey = value;
_dnsProviderKey = value;
}
Future<void> storeServerTypeIdentifier(final String typeIdentifier) async {
@ -76,7 +76,7 @@ class ApiConfigModel {
void clear() {
_serverProviderKey = null;
_serverLocation = null;
_cloudFlareKey = null;
_dnsProviderKey = null;
_backblazeCredential = null;
_serverDomain = null;
_serverDetails = null;
@ -88,7 +88,7 @@ class ApiConfigModel {
void init() {
_serverProviderKey = _box.get(BNames.hetznerKey);
_serverLocation = _box.get(BNames.serverLocation);
_cloudFlareKey = _box.get(BNames.cloudFlareKey);
_dnsProviderKey = _box.get(BNames.cloudFlareKey);
_backblazeCredential = _box.get(BNames.backblazeCredential);
_serverDomain = _box.get(BNames.serverDomain);
_serverDetails = _box.get(BNames.serverDetails);

View File

@ -29,4 +29,6 @@ enum DnsProvider {
unknown,
@HiveField(1)
cloudflare,
@HiveField(2)
digitalOcean,
}

View File

@ -53,7 +53,7 @@ class RecoveryRouting extends StatelessWidget {
case RecoveryStep.serverSelection:
currentPage = const RecoveryConfirmServer();
break;
case RecoveryStep.cloudflareToken:
case RecoveryStep.dnsProviderToken:
currentPage = const RecoveryConfirmCloudflare();
break;
case RecoveryStep.backblazeToken: