feat(digital-ocean): Implement provider picker pages

routes-refactor
NaiJi ✨ 2022-10-11 20:11:13 +00:00
parent ee160042f8
commit 70330c59ab
18 changed files with 843 additions and 80 deletions

View File

@ -264,9 +264,10 @@
},
"initializing": {
"connect_to_server": "Connect a server",
"select_provider": "Select your provider",
"place_where_data": "A place where your data and SelfPrivacy services will reside:",
"how": "How to obtain API token",
"hetzner_bad_key_error": "Hetzner API key is invalid",
"provider_bad_key_error": "Provider API key is invalid",
"cloudflare_bad_key_error": "Cloudflare API key is invalid",
"backblaze_bad_key_error": "Backblaze storage information is invalid",
"connect_cloudflare": "Connect CloudFlare",

View File

@ -266,7 +266,7 @@
"connect_to_server": "Подключите сервер",
"place_where_data": "Здесь будут жить ваши данные и SelfPrivacy-сервисы:",
"how": "Как получить API Token",
"hetzner_bad_key_error": "Hetzner API ключ неверен",
"provider_bad_key_error": "API ключ провайдера неверен",
"cloudflare_bad_key_error": "Cloudflare API ключ неверен",
"backblaze_bad_key_error": "Информация о Backblaze хранилище неверна",
"connect_cloudflare": "Подключите CloudFlare",

View File

@ -1,5 +1,6 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_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';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
@ -17,6 +18,8 @@ class ApiFactoryCreator {
switch (provider) {
case ServerProvider.hetzner:
return HetznerApiFactory();
case ServerProvider.digitalOcean:
return DigitalOceanApiFactory();
case ServerProvider.unknown:
throw UnknownApiProviderException('Unknown server provider');
}
@ -41,6 +44,8 @@ class VolumeApiFactoryCreator {
switch (provider) {
case ServerProvider.hetzner:
return HetznerApiFactory();
case ServerProvider.digitalOcean:
return DigitalOceanApiFactory();
case ServerProvider.unknown:
throw UnknownApiProviderException('Unknown volume provider');
}

View File

@ -0,0 +1,559 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi {
DigitalOceanApi(
{final this.hasLogger = false, final this.isWithToken = true});
@override
bool hasLogger;
@override
bool isWithToken;
@override
BaseOptions get options {
final BaseOptions options = BaseOptions(baseUrl: rootAddress);
if (isWithToken) {
final String? token = getIt<ApiConfigModel>().serverProviderKey;
assert(token != null);
options.headers = {'Authorization': 'Bearer $token'};
}
if (validateStatus != null) {
options.validateStatus = validateStatus!;
}
return options;
}
@override
String rootAddress = 'https://api.digitalocean.com/v2';
@override
Future<bool> isApiTokenValid(final String token) async {
bool isValid = false;
Response? response;
final Dio client = await getClient();
try {
response = await client.get(
'/servers',
options: Options(
headers: {'Authorization': 'Bearer $token'},
),
);
} catch (e) {
print(e);
isValid = false;
} finally {
close(client);
}
if (response != null) {
if (response.statusCode == HttpStatus.ok) {
isValid = true;
} else if (response.statusCode == HttpStatus.unauthorized) {
isValid = false;
} else {
throw Exception('code: ${response.statusCode}');
}
}
return isValid;
}
@override
RegExp getApiTokenValidation() =>
RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]');
@override
Future<double?> getPricePerGb() async {
double? price;
final Response dbGetResponse;
final Dio client = await getClient();
try {
dbGetResponse = await client.get('/pricing');
final volume = dbGetResponse.data['pricing']['volume'];
final volumePrice = volume['price_per_gb_month']['gross'];
price = double.parse(volumePrice);
} catch (e) {
print(e);
} finally {
client.close();
}
return price;
}
@override
Future<ServerVolume?> createVolume() async {
ServerVolume? volume;
final Response dbCreateResponse;
final Dio client = await getClient();
try {
dbCreateResponse = await client.post(
'/volumes',
data: {
'size': 10,
'name': StringGenerators.dbStorageName(),
'labels': {'labelkey': 'value'},
'location': 'fsn1',
'automount': false,
'format': 'ext4'
},
);
final dbId = dbCreateResponse.data['volume']['id'];
final dbSize = dbCreateResponse.data['volume']['size'];
final dbServer = dbCreateResponse.data['volume']['server'];
final dbName = dbCreateResponse.data['volume']['name'];
final dbDevice = dbCreateResponse.data['volume']['linux_device'];
volume = ServerVolume(
id: dbId,
name: dbName,
sizeByte: dbSize,
serverId: dbServer,
linuxDevice: dbDevice,
);
} catch (e) {
print(e);
} finally {
client.close();
}
return volume;
}
@override
Future<List<ServerVolume>> getVolumes({final String? status}) async {
final List<ServerVolume> volumes = [];
final Response dbGetResponse;
final Dio client = await getClient();
try {
dbGetResponse = await client.get(
'/volumes',
queryParameters: {
'status': status,
},
);
final List<dynamic> rawVolumes = dbGetResponse.data['volumes'];
for (final rawVolume in rawVolumes) {
final int dbId = rawVolume['id'];
final int dbSize = rawVolume['size'] * 1024 * 1024 * 1024;
final dbServer = rawVolume['server'];
final String dbName = rawVolume['name'];
final dbDevice = rawVolume['linux_device'];
final volume = ServerVolume(
id: dbId,
name: dbName,
sizeByte: dbSize,
serverId: dbServer,
linuxDevice: dbDevice,
);
volumes.add(volume);
}
} catch (e) {
print(e);
} finally {
client.close();
}
return volumes;
}
@override
Future<ServerVolume?> getVolume(final int id) async {
ServerVolume? volume;
final Response dbGetResponse;
final Dio client = await getClient();
try {
dbGetResponse = await client.get('/volumes/$id');
final int dbId = dbGetResponse.data['volume']['id'];
final int dbSize = dbGetResponse.data['volume']['size'];
final int dbServer = dbGetResponse.data['volume']['server'];
final String dbName = dbGetResponse.data['volume']['name'];
final dbDevice = dbGetResponse.data['volume']['linux_device'];
volume = ServerVolume(
id: dbId,
name: dbName,
sizeByte: dbSize,
serverId: dbServer,
linuxDevice: dbDevice,
);
} catch (e) {
print(e);
} finally {
client.close();
}
return volume;
}
@override
Future<void> deleteVolume(final int id) async {
final Dio client = await getClient();
try {
await client.delete('/volumes/$id');
} catch (e) {
print(e);
} finally {
client.close();
}
}
@override
Future<bool> attachVolume(final int volumeId, final int serverId) async {
bool success = false;
final Response dbPostResponse;
final Dio client = await getClient();
try {
dbPostResponse = await client.post(
'/volumes/$volumeId/actions/attach',
data: {
'automount': true,
'server': serverId,
},
);
success = dbPostResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
} finally {
client.close();
}
return success;
}
@override
Future<bool> detachVolume(final int volumeId) async {
bool success = false;
final Response dbPostResponse;
final Dio client = await getClient();
try {
dbPostResponse = await client.post('/volumes/$volumeId/actions/detach');
success = dbPostResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
} finally {
client.close();
}
return success;
}
@override
Future<bool> resizeVolume(final int volumeId, final int sizeGb) async {
bool success = false;
final Response dbPostResponse;
final Dio client = await getClient();
try {
dbPostResponse = await client.post(
'/volumes/$volumeId/actions/resize',
data: {
'size': sizeGb,
},
);
success = dbPostResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
} finally {
client.close();
}
return success;
}
@override
Future<ServerHostingDetails?> createServer({
required final String dnsApiToken,
required final User rootUser,
required final String domainName,
}) async {
ServerHostingDetails? details;
final ServerVolume? newVolume = await createVolume();
if (newVolume == null) {
return details;
}
details = await createServerWithVolume(
dnsApiToken: dnsApiToken,
rootUser: rootUser,
domainName: domainName,
dataBase: newVolume,
);
return details;
}
Future<ServerHostingDetails?> createServerWithVolume({
required final String dnsApiToken,
required final User rootUser,
required final String domainName,
required final ServerVolume dataBase,
}) async {
final Dio client = await getClient();
final String dbPassword = StringGenerators.dbPassword();
final int dbId = dataBase.id;
final String apiToken = StringGenerators.apiToken();
final String hostname = getHostnameFromDomain(domainName);
final String base64Password =
base64.encode(utf8.encode(rootUser.password ?? 'PASS'));
print('hostname: $hostname');
/// add ssh key when you need it: e.g. "ssh_keys":["kherel"]
/// check the branch name, it could be "development" or "master".
///
final String userdataString =
"#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' CF_TOKEN=$dnsApiToken DB_PASSWORD=$dbPassword API_TOKEN=$apiToken HOSTNAME=$hostname bash 2>&1 | tee /tmp/infect.log";
print(userdataString);
final Map<String, Object> data = {
'name': hostname,
'server_type': 'cx11',
'start_after_create': false,
'image': 'ubuntu-20.04',
'volumes': [dbId],
'networks': [],
'user_data': userdataString,
'labels': {},
'automount': true,
'location': 'fsn1'
};
print('Decoded data: $data');
ServerHostingDetails? serverDetails;
DioError? hetznerError;
bool success = false;
try {
final Response serverCreateResponse = await client.post(
'/servers',
data: data,
);
print(serverCreateResponse.data);
serverDetails = ServerHostingDetails(
id: serverCreateResponse.data['server']['id'],
ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'],
createTime: DateTime.now(),
volume: dataBase,
apiToken: apiToken,
provider: ServerProvider.hetzner,
);
success = true;
} on DioError catch (e) {
print(e);
hetznerError = e;
} catch (e) {
print(e);
} finally {
client.close();
}
if (!success) {
await Future.delayed(const Duration(seconds: 10));
await deleteVolume(dbId);
}
if (hetznerError != null) {
throw hetznerError;
}
return serverDetails;
}
static String getHostnameFromDomain(final String domain) {
// Replace all non-alphanumeric characters with an underscore
String hostname =
domain.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
if (hostname.endsWith('-')) {
hostname = hostname.substring(0, hostname.length - 1);
}
if (hostname.startsWith('-')) {
hostname = hostname.substring(1);
}
if (hostname.isEmpty) {
hostname = 'selfprivacy-server';
}
return hostname;
}
@override
Future<void> deleteServer({
required final String domainName,
}) async {
final Dio client = await getClient();
final String hostname = getHostnameFromDomain(domainName);
final Response serversReponse = await client.get('/servers');
final List servers = serversReponse.data['servers'];
final Map server = servers.firstWhere((final el) => el['name'] == hostname);
final List volumes = server['volumes'];
final List<Future> laterFutures = <Future>[];
for (final volumeId in volumes) {
await client.post('/volumes/$volumeId/actions/detach');
}
await Future.delayed(const Duration(seconds: 10));
for (final volumeId in volumes) {
laterFutures.add(client.delete('/volumes/$volumeId'));
}
laterFutures.add(client.delete('/servers/${server['id']}'));
await Future.wait(laterFutures);
close(client);
}
@override
Future<ServerHostingDetails> restart() async {
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
final Dio client = await getClient();
try {
await client.post('/servers/${server.id}/actions/reset');
} catch (e) {
print(e);
} finally {
close(client);
}
return server.copyWith(startTime: DateTime.now());
}
@override
Future<ServerHostingDetails> powerOn() async {
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
final Dio client = await getClient();
try {
await client.post('/servers/${server.id}/actions/poweron');
} catch (e) {
print(e);
} finally {
close(client);
}
return server.copyWith(startTime: DateTime.now());
}
Future<Map<String, dynamic>> getMetrics(
final DateTime start,
final DateTime end,
final String type,
) async {
final ServerHostingDetails? hetznerServer =
getIt<ApiConfigModel>().serverDetails;
Map<String, dynamic> metrics = {};
final Dio client = await getClient();
try {
final Map<String, dynamic> queryParameters = {
'start': start.toUtc().toIso8601String(),
'end': end.toUtc().toIso8601String(),
'type': type
};
final Response res = await client.get(
'/servers/${hetznerServer!.id}/metrics',
queryParameters: queryParameters,
);
metrics = res.data;
} catch (e) {
print(e);
} finally {
close(client);
}
return metrics;
}
Future<HetznerServerInfo> getInfo() async {
final ServerHostingDetails? hetznerServer =
getIt<ApiConfigModel>().serverDetails;
final Dio client = await getClient();
final Response response = await client.get('/servers/${hetznerServer!.id}');
close(client);
return HetznerServerInfo.fromJson(response.data!['server']);
}
@override
Future<List<ServerBasicInfo>> getServers() async {
List<ServerBasicInfo> servers = [];
final Dio client = await getClient();
try {
final Response response = await client.get('/servers');
servers = response.data!['servers']
.map<HetznerServerInfo>(
(final e) => HetznerServerInfo.fromJson(e),
)
.toList()
.where(
(final server) => server.publicNet.ipv4 != null,
)
.map<ServerBasicInfo>(
(final server) => ServerBasicInfo(
id: server.id,
name: server.name,
ip: server.publicNet.ipv4.ip,
reverseDns: server.publicNet.ipv4.reverseDns,
created: server.created,
volumeId: server.volumes.isNotEmpty ? server.volumes[0] : 0,
),
)
.toList();
} catch (e) {
print(e);
} finally {
close(client);
}
print(servers);
return servers;
}
@override
Future<void> createReverseDns({
required final ServerHostingDetails serverDetails,
required final ServerDomain domain,
}) async {
final Dio client = await getClient();
try {
await client.post(
'/servers/${serverDetails.id}/actions/change_dns_ptr',
data: {
'ip': serverDetails.ip4,
'dns_ptr': domain.domainName,
},
);
} catch (e) {
print(e);
} finally {
close(client);
}
}
}

View File

@ -0,0 +1,26 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/provider_api_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
class DigitalOceanApiFactory extends ServerProviderApiFactory
with VolumeProviderApiFactory {
@override
ServerProviderApi getServerProvider({
final ProviderApiSettings settings = const ProviderApiSettings(),
}) =>
DigitalOceanApi(
hasLogger: settings.hasLogger,
isWithToken: settings.isWithToken,
);
@override
VolumeProviderApi getVolumeProvider({
final ProviderApiSettings settings = const ProviderApiSettings(),
}) =>
DigitalOceanApi(
hasLogger: settings.hasLogger,
isWithToken: settings.isWithToken,
);
}

View File

@ -23,7 +23,7 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
BaseOptions get options {
final BaseOptions options = BaseOptions(baseUrl: rootAddress);
if (isWithToken) {
final String? token = getIt<ApiConfigModel>().hetznerKey;
final String? token = getIt<ApiConfigModel>().serverProviderKey;
assert(token != null);
options.headers = {'Authorization': 'Bearer $token'};
}

View File

@ -26,7 +26,7 @@ class ProviderFormCubit extends FormCubit {
@override
FutureOr<void> onSubmit() async {
serverInstallationCubit.setHetznerKey(apiKey.state.value);
serverInstallationCubit.setServerProviderKey(apiKey.state.value);
}
final ServerInstallationCubit serverInstallationCubit;
@ -45,7 +45,7 @@ class ProviderFormCubit extends FormCubit {
}
if (!isKeyValid) {
apiKey.setError('initializing.hetzner_bad_key_error'.tr());
apiKey.setError('initializing.provider_bad_key_error'.tr());
return false;
}

View File

@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_creator.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/provider_api_settings.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart';
@ -51,6 +52,13 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
}
}
void setServerProviderType(final ServerProvider providerType) {
repository.serverProviderApiFactory =
ApiFactoryCreator.createServerProviderApiFactory(
providerType,
);
}
RegExp getServerProviderApiTokenValidation() =>
repository.serverProviderApiFactory!
.getServerProvider()
@ -78,13 +86,13 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
)
.isApiTokenValid(providerToken);
void setHetznerKey(final String hetznerKey) async {
await repository.saveHetznerKey(hetznerKey);
void setServerProviderKey(final String serverProviderKey) async {
await repository.saveServerProviderKey(serverProviderKey);
if (state is ServerInstallationRecovery) {
emit(
(state as ServerInstallationRecovery).copyWith(
providerApiToken: hetznerKey,
providerApiToken: serverProviderKey,
currentStep: RecoveryStep.serverSelection,
),
);
@ -93,7 +101,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
emit(
(state as ServerInstallationNotFinished).copyWith(
providerApiToken: hetznerKey,
providerApiToken: serverProviderKey,
),
);
}
@ -427,7 +435,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
emit(
dataState.copyWith(
serverDetails: serverDetails,
currentStep: RecoveryStep.hetznerToken,
currentStep: RecoveryStep.serverProviderToken,
),
);
} on ServerAuthorizationException {

View File

@ -39,17 +39,14 @@ class ServerAuthorizationException implements Exception {
class ServerInstallationRepository {
Box box = Hive.box(BNames.serverInstallationBox);
Box<User> usersBox = Hive.box(BNames.usersBox);
ServerProviderApiFactory? serverProviderApiFactory =
ApiFactoryCreator.createServerProviderApiFactory(
ServerProvider.hetzner, // TODO: HARDCODE FOR NOW!!!
); // TODO: Remove when provider selection is implemented.
ServerProviderApiFactory? serverProviderApiFactory;
DnsProviderApiFactory? dnsProviderApiFactory =
ApiFactoryCreator.createDnsProviderApiFactory(
DnsProvider.cloudflare, // TODO: HARDCODE FOR NOW!!!
);
Future<ServerInstallationState> load() async {
final String? providerApiToken = getIt<ApiConfigModel>().hetznerKey;
final String? providerApiToken = getIt<ApiConfigModel>().serverProviderKey;
final String? cloudflareToken = getIt<ApiConfigModel>().cloudFlareKey;
final ServerDomain? serverDomain = getIt<ApiConfigModel>().serverDomain;
final BackblazeCredential? backblazeCredential =
@ -124,13 +121,13 @@ class ServerInstallationRepository {
}
RecoveryStep _getCurrentRecoveryStep(
final String? hetznerToken,
final String? serverProviderToken,
final String? cloudflareToken,
final ServerDomain serverDomain,
final ServerHostingDetails? serverDetails,
) {
if (serverDetails != null) {
if (hetznerToken != null) {
if (serverProviderToken != null) {
if (serverDetails.provider != ServerProvider.unknown) {
if (serverDomain.provider != DnsProvider.unknown) {
return RecoveryStep.backblazeToken;
@ -139,7 +136,7 @@ class ServerInstallationRepository {
}
return RecoveryStep.serverSelection;
}
return RecoveryStep.hetznerToken;
return RecoveryStep.serverProviderToken;
}
return RecoveryStep.selecting;
}
@ -150,7 +147,7 @@ class ServerInstallationRepository {
}
Future<ServerHostingDetails> startServer(
final ServerHostingDetails hetznerServer,
final ServerHostingDetails server,
) async {
ServerHostingDetails serverDetails;
@ -670,12 +667,11 @@ class ServerInstallationRepository {
getIt<ApiConfigModel>().init();
}
Future<void> saveHetznerKey(final String key) async {
print('saved');
await getIt<ApiConfigModel>().storeHetznerKey(key);
Future<void> saveServerProviderKey(final String key) async {
await getIt<ApiConfigModel>().storeServerProviderKey(key);
}
Future<void> deleteHetznerKey() async {
Future<void> deleteServerProviderKey() async {
await box.delete(BNames.hetznerKey);
getIt<ApiConfigModel>().init();
}

View File

@ -238,7 +238,7 @@ enum RecoveryStep {
recoveryKey,
newDeviceKey,
oldToken,
hetznerToken,
serverProviderToken,
serverSelection,
cloudflareToken,
backblazeToken,

View File

@ -9,22 +9,22 @@ class ApiConfigModel {
final Box _box = Hive.box(BNames.serverInstallationBox);
ServerHostingDetails? get serverDetails => _serverDetails;
String? get hetznerKey => _hetznerKey;
String? get serverProviderKey => _serverProviderKey;
String? get cloudFlareKey => _cloudFlareKey;
BackblazeCredential? get backblazeCredential => _backblazeCredential;
ServerDomain? get serverDomain => _serverDomain;
BackblazeBucket? get backblazeBucket => _backblazeBucket;
String? _hetznerKey;
String? _serverProviderKey;
String? _cloudFlareKey;
ServerHostingDetails? _serverDetails;
BackblazeCredential? _backblazeCredential;
ServerDomain? _serverDomain;
BackblazeBucket? _backblazeBucket;
Future<void> storeHetznerKey(final String value) async {
Future<void> storeServerProviderKey(final String value) async {
await _box.put(BNames.hetznerKey, value);
_hetznerKey = value;
_serverProviderKey = value;
}
Future<void> storeCloudFlareKey(final String value) async {
@ -53,7 +53,7 @@ class ApiConfigModel {
}
void clear() {
_hetznerKey = null;
_serverProviderKey = null;
_cloudFlareKey = null;
_backblazeCredential = null;
_serverDomain = null;
@ -62,7 +62,7 @@ class ApiConfigModel {
}
void init() {
_hetznerKey = _box.get(BNames.hetznerKey);
_serverProviderKey = _box.get(BNames.hetznerKey);
_cloudFlareKey = _box.get(BNames.cloudFlareKey);
_backblazeCredential = _box.get(BNames.backblazeCredential);
_serverDomain = _box.get(BNames.serverDomain);

View File

@ -78,4 +78,6 @@ enum ServerProvider {
unknown,
@HiveField(1)
hetzner,
@HiveField(2)
digitalOcean,
}

View File

@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
import 'package:selfprivacy/ui/pages/setup/initializing.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:wakelock/wakelock.dart';

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/text_themes.dart';
import 'package:selfprivacy/ui/pages/setup/initializing.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:easy_localization/easy_localization.dart';

View File

@ -11,7 +11,7 @@ import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/pages/devices/devices.dart';
import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart';
import 'package:selfprivacy/ui/pages/server_storage/binds_migration/services_migration.dart';
import 'package:selfprivacy/ui/pages/setup/initializing.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/ui/pages/users/users.dart';

View File

@ -17,6 +17,7 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart';
import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/provider_picker.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
@ -139,52 +140,8 @@ class InitializingPage extends StatelessWidget {
}
Widget _stepHetzner(final ServerInstallationCubit serverInstallationCubit) =>
BlocProvider(
create: (final context) => ProviderFormCubit(
serverInstallationCubit,
),
child: Builder(
builder: (final context) {
final formCubitState = context.watch<ProviderFormCubit>().state;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
'assets/images/logos/hetzner.png',
width: 150,
),
const SizedBox(height: 10),
BrandText.h2('initializing.connect_to_server'.tr()),
const SizedBox(height: 10),
BrandText.body2('initializing.place_where_data'.tr()),
const Spacer(),
CubitFormTextField(
formFieldCubit: context.read<ProviderFormCubit>().apiKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: const InputDecoration(
hintText: 'Hetzner API Token',
),
),
const Spacer(),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
: () => context.read<ProviderFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
const SizedBox(height: 10),
BrandButton.text(
onPressed: () => _showModal(
context,
const _HowTo(fileName: 'how_hetzner'),
),
title: 'initializing.how'.tr(),
),
],
);
},
),
ProviderPicker(
serverInstallationCubit: serverInstallationCubit,
);
void _showModal(final BuildContext context, final Widget widget) {

View File

@ -0,0 +1,209 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/provider_form_cubit.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart';
import 'package:selfprivacy/ui/components/brand_button/filled_button.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
class ProviderPicker extends StatefulWidget {
const ProviderPicker({
required this.serverInstallationCubit,
super.key,
});
final ServerInstallationCubit serverInstallationCubit;
@override
State<ProviderPicker> createState() => _ProviderPickerState();
}
class _ProviderPickerState extends State<ProviderPicker> {
ServerProvider selectedProvider = ServerProvider.unknown;
void setProvider(final ServerProvider provider) {
setState(() {
selectedProvider = provider;
});
}
@override
Widget build(final BuildContext context) {
switch (selectedProvider) {
case ServerProvider.unknown:
return ProviderSelectionPage(
callback: setProvider,
);
case ServerProvider.hetzner:
return ProviderInputDataPage(
serverInstallationCubit: widget.serverInstallationCubit,
providerInfo: ProviderPageInfo(
providerType: ServerProvider.hetzner,
pathToHow: 'hetzner_how',
image: Image.asset(
'assets/images/logos/hetzner.png',
width: 150,
),
),
);
case ServerProvider.digitalOcean:
return ProviderInputDataPage(
serverInstallationCubit: widget.serverInstallationCubit,
providerInfo: ProviderPageInfo(
providerType: ServerProvider.digitalOcean,
pathToHow: 'hetzner_how',
image: Image.asset(
'assets/images/logos/digital_ocean.png',
width: 150,
),
),
);
}
}
}
class ProviderPageInfo {
const ProviderPageInfo({
required this.providerType,
required this.pathToHow,
required this.image,
});
final String pathToHow;
final Image image;
final ServerProvider providerType;
}
class ProviderInputDataPage extends StatelessWidget {
const ProviderInputDataPage({
required this.providerInfo,
required this.serverInstallationCubit,
super.key,
});
final ProviderPageInfo providerInfo;
final ServerInstallationCubit serverInstallationCubit;
@override
Widget build(final BuildContext context) => BlocProvider(
create: (final context) => ProviderFormCubit(
serverInstallationCubit,
),
child: Builder(
builder: (final context) {
final formCubitState = context.watch<ProviderFormCubit>().state;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
providerInfo.image,
const SizedBox(height: 10),
Text(
'initializing.connect_to_server'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
CubitFormTextField(
formFieldCubit: context.read<ProviderFormCubit>().apiKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: const InputDecoration(
hintText: 'Provider API Token',
),
),
const Spacer(),
FilledButton(
title: 'basis.connect'.tr(),
onPressed: () => formCubitState.isSubmitting
? null
: () => context.read<ProviderFormCubit>().trySubmit(),
),
const SizedBox(height: 10),
OutlinedButton(
child: Text('initializing.how'.tr()),
onPressed: () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (final BuildContext context) => BrandBottomSheet(
isExpended: true,
child: Padding(
padding: paddingH15V0,
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 16),
children: [
BrandMarkdown(
fileName: providerInfo.pathToHow,
),
],
),
),
),
),
),
],
);
},
),
);
}
class ProviderSelectionPage extends StatelessWidget {
const ProviderSelectionPage({
required this.callback,
super.key,
});
final Function callback;
@override
Widget build(final BuildContext context) => Column(
children: [
Text(
'initializing.select_provider'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 10),
Text(
'initializing.place_where_data'.tr(),
),
const SizedBox(height: 10),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 320,
),
child: Row(
children: [
InkWell(
onTap: () {
context.read<ServerInstallationCubit>().setServerProviderType(ServerProvider.hetzner);
callback(ServerProvider.hetzner);
},
child: Image.asset(
'assets/images/logos/hetzner.png',
width: 150,
),
),
const SizedBox(
width: 20,
),
InkWell(
onTap: () {
context.read<ServerInstallationCubit>().setServerProviderType(ServerProvider.digitalOcean);
callback(ServerProvider.digitalOcean);
},
child: Image.asset(
'assets/images/logos/digital_ocean.png',
width: 150,
),
),
],
),
),
],
);
}

View File

@ -47,7 +47,7 @@ class RecoveryRouting extends StatelessWidget {
case RecoveryStep.oldToken:
currentPage = const RecoverByOldToken();
break;
case RecoveryStep.hetznerToken:
case RecoveryStep.serverProviderToken:
currentPage = const RecoveryHetznerConnected();
break;
case RecoveryStep.serverSelection: