import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/json/api_token.dart'; import 'package:selfprivacy/logic/models/json/auto_upgrade_settings.dart'; import 'package:selfprivacy/logic/models/json/backup.dart'; import 'package:selfprivacy/logic/models/json/device_token.dart'; import 'package:selfprivacy/logic/models/json/recovery_token_status.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; import 'package:selfprivacy/logic/api_maps/api_map.dart'; class ApiResponse { ApiResponse({ required this.statusCode, required this.data, this.errorMessage, }); final int statusCode; final String? errorMessage; final D data; bool get isSuccess => statusCode >= 200 && statusCode < 300; } class ServerApi extends ApiMap { ServerApi({ this.hasLogger = false, this.isWithToken = true, this.overrideDomain, this.customToken, }); @override bool hasLogger; @override bool isWithToken; String? overrideDomain; String? customToken; @override BaseOptions get options { BaseOptions options = BaseOptions(); if (isWithToken) { final ServerDomain? cloudFlareDomain = getIt().serverDomain; final String domainName = cloudFlareDomain!.domainName; final String? apiToken = getIt().serverDetails?.apiToken; options = BaseOptions( baseUrl: 'https://api.$domainName', headers: { 'Authorization': 'Bearer $apiToken', }, ); } if (overrideDomain != null) { options = BaseOptions( baseUrl: 'https://api.$overrideDomain', headers: customToken != null ? {'Authorization': 'Bearer $customToken'} : null, ); } return options; } Future getApiVersion() async { Response response; final Dio client = await getClient(); String? apiVersion; try { response = await client.get('/api/version'); apiVersion = response.data['version']; } on DioError catch (e) { print(e.message); } finally { close(client); } return apiVersion; } Future isHttpServerWorking() async { bool res = false; Response response; final Dio client = await getClient(); try { response = await client.get('/services/status'); res = response.statusCode == HttpStatus.ok; } on DioError catch (e) { print(e.message); } finally { close(client); } return res; } Future> createUser(final User user) async { Response response; final Dio client = await getClient(); try { response = await client.post( '/users', data: { 'username': user.login, 'password': user.password, }, ); } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.error.toString(), statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: User( login: user.login, password: user.password, isFoundOnServer: false, ), ); } finally { close(client); } bool isFoundOnServer = false; int code = 0; final bool isUserCreated = (response.statusCode != null) && (response.statusCode == HttpStatus.created); if (isUserCreated) { isFoundOnServer = true; code = response.statusCode!; } else { isFoundOnServer = false; code = HttpStatus.notAcceptable; } return ApiResponse( statusCode: code, data: User( login: user.login, password: user.password, isFoundOnServer: isFoundOnServer, ), ); } Future>> getUsersList({ final withMainUser = false, }) async { final List res = []; Response response; final Dio client = await getClient(); try { response = await client.get( '/users', queryParameters: withMainUser ? {'withMainUser': 'true'} : null, ); for (final user in response.data) { res.add(user.toString()); } } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: [], ); } catch (e) { print(e); return ApiResponse( errorMessage: e.toString(), statusCode: HttpStatus.internalServerError, data: [], ); } finally { close(client); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( statusCode: code, data: res, ); } Future> addUserSshKey( final User user, final String sshKey, ) async { late Response response; final Dio client = await getClient(); try { response = await client.post( '/services/ssh/keys/${user.login}', data: { 'public_key': sshKey, }, ); } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: null, ); } finally { close(client); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( statusCode: code, data: null, ); } Future> addRootSshKey(final String ssh) async { late Response response; final Dio client = await getClient(); try { response = await client.put( '/services/ssh/key/send', data: {'public_key': ssh}, ); } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: null, ); } finally { close(client); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( statusCode: code, data: null, ); } Future>> getUserSshKeys(final User user) async { List res; Response response; final Dio client = await getClient(); try { response = await client.get('/services/ssh/keys/${user.login}'); res = (response.data as List) .map((final e) => e as String) .toList(); } on DioError catch (e) { print(e.message); return ApiResponse>( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: [], ); } catch (e) { return ApiResponse>( errorMessage: e.toString(), statusCode: HttpStatus.internalServerError, data: [], ); } finally { close(client); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse>( statusCode: code, data: res, errorMessage: response.data is List ? null : response.data?.containsKey('error') ?? false ? response.data['error'] : null, ); } Future> deleteUserSshKey( final User user, final String sshKey, ) async { Response response; final Dio client = await getClient(); try { response = await client.delete( '/services/ssh/keys/${user.login}', data: {'public_key': sshKey}, ); } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: null, ); } finally { close(client); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( statusCode: code, data: null, errorMessage: response.data?.containsKey('error') ?? false ? response.data['error'] : null, ); } Future deleteUser(final User user) async { bool res = false; Response response; final Dio client = await getClient(); try { response = await client.delete('/users/${user.login}'); res = response.statusCode == HttpStatus.ok || response.statusCode == HttpStatus.notFound; } on DioError catch (e) { print(e.message); res = false; } finally { close(client); } return res; } @override String get rootAddress => throw UnimplementedError('not used in with implementation'); Future apply() async { bool res = false; Response response; final Dio client = await getClient(); try { response = await client.get('/system/configuration/apply'); res = response.statusCode == HttpStatus.ok; } on DioError catch (e) { print(e.message); res = false; } finally { close(client); } return res; } Future switchService( final ServiceTypes type, final bool needToTurnOn, ) async { final Dio client = await getClient(); try { client.post( '/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}', ); } on DioError catch (e) { print(e.message); } finally { close(client); } } Future> servicesPowerCheck() async { Response response; final Dio client = await getClient(); try { response = await client.get('/services/status'); } on DioError catch (e) { print(e.message); return {}; } finally { close(client); } return { ServiceTypes.passwordManager: response.data['bitwarden'] == 0, ServiceTypes.git: response.data['gitea'] == 0, ServiceTypes.cloud: response.data['nextcloud'] == 0, ServiceTypes.vpn: response.data['ocserv'] == 0, ServiceTypes.socialNetwork: response.data['pleroma'] == 0, }; } Future uploadBackblazeConfig(final BackblazeBucket bucket) async { final Dio client = await getClient(); try { client.put( '/services/restic/backblaze/config', data: { 'accountId': bucket.applicationKeyId, 'accountKey': bucket.applicationKey, 'bucket': bucket.bucketName, }, ); } on DioError catch (e) { print(e.message); } finally { close(client); } } Future startBackup() async { final Dio client = await getClient(); try { client.put('/services/restic/backup/create'); } on DioError catch (e) { print(e.message); } finally { close(client); } } Future> getBackups() async { Response response; List backups = []; final Dio client = await getClient(); try { response = await client.get('/services/restic/backup/list'); backups = response.data.map(Backup.fromJson).toList(); } on DioError catch (e) { print(e.message); } catch (e) { print(e); } finally { close(client); } return backups; } Future getBackupStatus() async { Response response; BackupStatus status = BackupStatus( status: BackupStatusEnum.error, errorMessage: 'Network error', progress: 0, ); final Dio client = await getClient(); try { response = await client.get('/services/restic/backup/status'); status = BackupStatus.fromJson(response.data); } on DioError catch (e) { print(e.message); } finally { close(client); } return status; } Future forceBackupListReload() async { final Dio client = await getClient(); try { client.get('/services/restic/backup/reload'); } on DioError catch (e) { print(e.message); } finally { close(client); } } Future restoreBackup(final String backupId) async { final Dio client = await getClient(); try { client.put( '/services/restic/backup/restore', data: {'backupId': backupId}, ); } on DioError catch (e) { print(e.message); } finally { close(client); } } Future pullConfigurationUpdate() async { Response response; bool result = false; final Dio client = await getClient(); try { response = await client.get('/system/configuration/pull'); result = (response.statusCode != null) ? (response.statusCode == HttpStatus.ok) : false; } on DioError catch (e) { print(e.message); } finally { close(client); } return result; } Future reboot() async { Response response; bool result = false; final Dio client = await getClient(); try { response = await client.get('/system/reboot'); result = (response.statusCode != null) ? (response.statusCode == HttpStatus.ok) : false; } on DioError catch (e) { print(e.message); } finally { close(client); } return result; } Future upgrade() async { Response response; bool result = false; final Dio client = await getClient(); try { response = await client.get('/system/configuration/upgrade'); result = (response.statusCode != null) ? (response.statusCode == HttpStatus.ok) : false; } on DioError catch (e) { print(e.message); } finally { close(client); } return result; } Future getAutoUpgradeSettings() async { Response response; AutoUpgradeSettings settings = const AutoUpgradeSettings( enable: false, allowReboot: false, ); final Dio client = await getClient(); try { response = await client.get('/system/configuration/autoUpgrade'); if (response.data != null) { settings = AutoUpgradeSettings.fromJson(response.data); } } on DioError catch (e) { print(e.message); } finally { close(client); } return settings; } Future updateAutoUpgradeSettings( final AutoUpgradeSettings settings, ) async { final Dio client = await getClient(); try { await client.put( '/system/configuration/autoUpgrade', data: settings.toJson(), ); } on DioError catch (e) { print(e.message); } finally { close(client); } } Future getServerTimezone() async { // I am not sure how to initialize TimeZoneSettings with default value... final Dio client = await getClient(); final Response response = await client.get('/system/configuration/timezone'); close(client); return TimeZoneSettings.fromString(response.data); } Future updateServerTimezone(final TimeZoneSettings settings) async { final Dio client = await getClient(); try { await client.put( '/system/configuration/timezone', data: settings.toString(), ); } on DioError catch (e) { print(e.message); } finally { close(client); } } Future getDkim() async { Response response; final Dio client = await getClient(); try { response = await client.get('/services/mailserver/dkim'); } on DioError catch (e) { print(e.message); return null; } finally { close(client); } if (response.statusCode == null) { return null; } if (response.statusCode == HttpStatus.notFound || response.data == null) { throw Exception('No DKIM key found'); } if (response.statusCode != HttpStatus.ok) { return ''; } final Codec base64toString = utf8.fuse(base64); return base64toString .decode(response.data) .split('(')[1] .split(')')[0] .replaceAll('"', ''); } Future> getRecoveryTokenStatus() async { Response response; final Dio client = await getClient(); try { response = await client.get('/auth/recovery_token'); } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: const RecoveryKeyStatus(exists: false, valid: false), ); } finally { close(client); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( statusCode: code, data: response.data != null ? RecoveryKeyStatus.fromJson(response.data) : null, ); } Future> generateRecoveryToken( final DateTime? expiration, final int? uses, ) async { Response response; final Dio client = await getClient(); final Map data = {}; if (expiration != null) { data['expiration'] = '${expiration.toIso8601String()}Z'; print(data['expiration']); } if (uses != null) { data['uses'] = uses; } try { response = await client.post( '/auth/recovery_token', data: data, ); } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: '', ); } finally { close(client); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( statusCode: code, data: response.data != null ? response.data['token'] : '', ); } Future> useRecoveryToken(final DeviceToken token) async { Response response; final Dio client = await getClient(); try { response = await client.post( '/auth/recovery_token/use', data: { 'token': token.token, 'device': token.device, }, ); } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: '', ); } finally { client.close(); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( statusCode: code, data: response.data != null ? response.data['token'] : '', ); } Future> authorizeDevice(final DeviceToken token) async { Response response; final Dio client = await getClient(); try { response = await client.post( '/auth/new_device/authorize', data: { 'token': token.token, 'device': token.device, }, ); } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: '', ); } finally { client.close(); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse(statusCode: code, data: response.data['token'] ?? ''); } Future> createDeviceToken() async { Response response; final Dio client = await getClient(); try { response = await client.post('/auth/new_device'); } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: '', ); } finally { client.close(); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( statusCode: code, data: response.data != null ? response.data['token'] : '', ); } Future> deleteDeviceToken() async { Response response; final Dio client = await getClient(); try { response = await client.delete('/auth/new_device'); } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: '', ); } finally { client.close(); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse(statusCode: code, data: response.data ?? ''); } Future>> getApiTokens() async { Response response; final Dio client = await getClient(); try { response = await client.get('/auth/tokens'); } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: [], ); } finally { client.close(); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( statusCode: code, data: (response.data != null) ? response.data.map(ApiToken.fromJson).toList() : [], ); } Future> refreshCurrentApiToken() async { Response response; final Dio client = await getClient(); try { response = await client.post('/auth/tokens'); } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: '', ); } finally { client.close(); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( statusCode: code, data: response.data != null ? response.data['token'] : '', ); } Future> deleteApiToken(final String device) async { Response response; final Dio client = await getClient(); try { response = await client.delete( '/auth/tokens', data: { 'token_name': device, }, ); } on DioError catch (e) { print(e.message); return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, data: null, ); } finally { client.close(); } final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse(statusCode: code, data: null); } } extension UrlServerExt on ServiceTypes { String get url { switch (this) { // case ServiceTypes.mail: // return ''; // cannot be switch off // case ServiceTypes.messenger: // return ''; // external service // case ServiceTypes.video: // return ''; // jitsi meet not working case ServiceTypes.passwordManager: return 'bitwarden'; case ServiceTypes.cloud: return 'nextcloud'; case ServiceTypes.socialNetwork: return 'pleroma'; case ServiceTypes.git: return 'gitea'; case ServiceTypes.vpn: return 'ocserv'; default: throw Exception('wrong state'); } } }