123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794 |
- import 'dart:typed_data';
- import 'package:dio/dio.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter_sodium/flutter_sodium.dart';
- import 'package:logging/logging.dart';
- import 'package:photos/core/configuration.dart';
- import 'package:photos/core/event_bus.dart';
- import 'package:photos/core/network.dart';
- import 'package:photos/db/public_keys_db.dart';
- import 'package:photos/events/two_factor_status_change_event.dart';
- import 'package:photos/events/user_details_changed_event.dart';
- import 'package:photos/models/key_attributes.dart';
- import 'package:photos/models/key_gen_result.dart';
- import 'package:photos/models/public_key.dart';
- import 'package:photos/models/sessions.dart';
- import 'package:photos/models/set_keys_request.dart';
- import 'package:photos/models/set_recovery_key_request.dart';
- import 'package:photos/models/user_details.dart';
- import 'package:photos/ui/account/login_page.dart';
- import 'package:photos/ui/account/ott_verification_page.dart';
- import 'package:photos/ui/account/password_entry_page.dart';
- import 'package:photos/ui/account/password_reentry_page.dart';
- import 'package:photos/ui/account/two_factor_authentication_page.dart';
- import 'package:photos/ui/account/two_factor_recovery_page.dart';
- import 'package:photos/ui/account/two_factor_setup_page.dart';
- import 'package:photos/utils/crypto_util.dart';
- import 'package:photos/utils/dialog_util.dart';
- import 'package:photos/utils/navigation_util.dart';
- import 'package:photos/utils/toast_util.dart';
- class UserService {
- final _dio = Network.instance.getDio();
- final _logger = Logger((UserService).toString());
- final _config = Configuration.instance;
- ValueNotifier<String> emailValueNotifier;
- UserService._privateConstructor();
- static final UserService instance = UserService._privateConstructor();
- Future<void> init() async {
- emailValueNotifier =
- ValueNotifier<String>(Configuration.instance.getEmail());
- }
- Future<void> getOtt(
- BuildContext context,
- String email, {
- bool isChangeEmail = false,
- bool isCreateAccountScreen = false,
- }) async {
- final dialog = createProgressDialog(context, "Please wait...");
- await dialog.show();
- try {
- final response = await _dio.get(
- _config.getHttpEndpoint() + "/users/ott",
- queryParameters: {
- "email": email,
- "purpose": isChangeEmail ? "change" : ""
- },
- );
- await dialog.hide();
- if (response != null && response.statusCode == 200) {
- Navigator.of(context).push(
- MaterialPageRoute(
- builder: (BuildContext context) {
- return OTTVerificationPage(
- email,
- isChangeEmail: isChangeEmail,
- isCreateAccountScreen: isCreateAccountScreen,
- );
- },
- ),
- );
- return;
- }
- showGenericErrorDialog(context);
- } on DioError catch (e) {
- await dialog.hide();
- _logger.info(e);
- if (e.response != null && e.response.statusCode == 403) {
- showErrorDialog(context, "Oops", "This email is already in use");
- } else {
- showGenericErrorDialog(context);
- }
- } catch (e) {
- await dialog.hide();
- _logger.severe(e);
- showGenericErrorDialog(context);
- }
- }
- Future<String> getPublicKey(String email) async {
- try {
- final response = await _dio.get(
- _config.getHttpEndpoint() + "/users/public-key",
- queryParameters: {"email": email},
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- final publicKey = response.data["publicKey"];
- await PublicKeysDB.instance.setKey(PublicKey(email, publicKey));
- return publicKey;
- } on DioError catch (e) {
- _logger.info(e);
- return null;
- }
- }
- Future<UserDetails> getUserDetailsV2({bool memoryCount = true}) async {
- try {
- final response = await _dio.get(
- _config.getHttpEndpoint() +
- "/users/details/v2?memoryCount=$memoryCount",
- queryParameters: {
- "memoryCount": memoryCount,
- },
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- return UserDetails.fromMap(response.data);
- } on DioError catch (e) {
- _logger.info(e);
- rethrow;
- }
- }
- Future<Sessions> getActiveSessions() async {
- try {
- final response = await _dio.get(
- _config.getHttpEndpoint() + "/users/sessions",
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- return Sessions.fromMap(response.data);
- } on DioError catch (e) {
- _logger.info(e);
- rethrow;
- }
- }
- Future<void> terminateSession(String token) async {
- try {
- await _dio.delete(
- _config.getHttpEndpoint() + "/users/session",
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- queryParameters: {
- "token": token,
- },
- );
- } on DioError catch (e) {
- _logger.info(e);
- rethrow;
- }
- }
- Future<void> leaveFamilyPlan() async {
- try {
- await _dio.delete(
- _config.getHttpEndpoint() + "/family/leave",
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- } on DioError catch (e) {
- _logger.warning('failed to leave family plan', e);
- rethrow;
- }
- }
- Future<void> logout(BuildContext context) async {
- final dialog = createProgressDialog(context, "Logging out...");
- await dialog.show();
- try {
- final response = await _dio.post(
- _config.getHttpEndpoint() + "/users/logout",
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- if (response != null && response.statusCode == 200) {
- await Configuration.instance.logout();
- await dialog.hide();
- Navigator.of(context).popUntil((route) => route.isFirst);
- } else {
- throw Exception("Log out action failed");
- }
- } catch (e) {
- _logger.severe(e);
- await dialog.hide();
- showGenericErrorDialog(context);
- }
- }
- Future<void> verifyEmail(BuildContext context, String ott) async {
- final dialog = createProgressDialog(context, "Please wait...");
- await dialog.show();
- try {
- final response = await _dio.post(
- _config.getHttpEndpoint() + "/users/verify-email",
- data: {
- "email": _config.getEmail(),
- "ott": ott,
- },
- );
- await dialog.hide();
- if (response != null && response.statusCode == 200) {
- showShortToast(context, "Email verification successful!");
- Widget page;
- final String twoFASessionID = response.data["twoFactorSessionID"];
- if (twoFASessionID != null && twoFASessionID.isNotEmpty) {
- page = TwoFactorAuthenticationPage(twoFASessionID);
- } else {
- await _saveConfiguration(response);
- if (Configuration.instance.getEncryptedToken() != null) {
- page = const PasswordReentryPage();
- } else {
- page = const PasswordEntryPage();
- }
- }
- Navigator.of(context).pushAndRemoveUntil(
- MaterialPageRoute(
- builder: (BuildContext context) {
- return page;
- },
- ),
- (route) => route.isFirst,
- );
- } else {
- // should never reach here
- throw Exception("unexpected response during email verification");
- }
- } on DioError catch (e) {
- _logger.info(e);
- await dialog.hide();
- if (e.response != null && e.response.statusCode == 410) {
- await showErrorDialog(
- context,
- "Oops",
- "Your verification code has expired",
- );
- Navigator.of(context).pop();
- } else {
- showErrorDialog(
- context,
- "Incorrect code",
- "Sorry, the code you've entered is incorrect",
- );
- }
- } catch (e) {
- await dialog.hide();
- _logger.severe(e);
- showErrorDialog(context, "Oops", "Verification failed, please try again");
- }
- }
- Future<void> setEmail(String email) async {
- await _config.setEmail(email);
- emailValueNotifier.value = email ?? "";
- }
- Future<void> changeEmail(
- BuildContext context,
- String email,
- String ott,
- ) async {
- final dialog = createProgressDialog(context, "Please wait...");
- await dialog.show();
- try {
- final response = await _dio.post(
- _config.getHttpEndpoint() + "/users/change-email",
- data: {
- "email": email,
- "ott": ott,
- },
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- await dialog.hide();
- if (response != null && response.statusCode == 200) {
- showToast(context, "Email changed to " + email);
- await setEmail(email);
- Navigator.of(context).popUntil((route) => route.isFirst);
- Bus.instance.fire(UserDetailsChangedEvent());
- return;
- }
- showErrorDialog(context, "Oops", "Verification failed, please try again");
- } on DioError catch (e) {
- await dialog.hide();
- if (e.response != null && e.response.statusCode == 403) {
- showErrorDialog(context, "Oops", "This email is already in use");
- } else {
- showErrorDialog(
- context,
- "Incorrect code",
- "Authentication failed, please try again",
- );
- }
- } catch (e) {
- await dialog.hide();
- _logger.severe(e);
- showErrorDialog(context, "Oops", "Verification failed, please try again");
- }
- }
- Future<void> setAttributes(KeyGenResult result) async {
- try {
- final name = _config.getName();
- await _dio.put(
- _config.getHttpEndpoint() + "/users/attributes",
- data: {
- "name": name,
- "keyAttributes": result.keyAttributes.toMap(),
- },
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- await _config.setKey(result.privateKeyAttributes.key);
- await _config.setSecretKey(result.privateKeyAttributes.secretKey);
- await _config.setKeyAttributes(result.keyAttributes);
- } catch (e) {
- _logger.severe(e);
- rethrow;
- }
- }
- Future<void> updateKeyAttributes(KeyAttributes keyAttributes) async {
- try {
- final setKeyRequest = SetKeysRequest(
- kekSalt: keyAttributes.kekSalt,
- encryptedKey: keyAttributes.encryptedKey,
- keyDecryptionNonce: keyAttributes.keyDecryptionNonce,
- memLimit: keyAttributes.memLimit,
- opsLimit: keyAttributes.opsLimit,
- );
- await _dio.put(
- _config.getHttpEndpoint() + "/users/keys",
- data: setKeyRequest.toMap(),
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- await _config.setKeyAttributes(keyAttributes);
- } catch (e) {
- _logger.severe(e);
- rethrow;
- }
- }
- Future<void> setRecoveryKey(KeyAttributes keyAttributes) async {
- try {
- final setRecoveryKeyRequest = SetRecoveryKeyRequest(
- keyAttributes.masterKeyEncryptedWithRecoveryKey,
- keyAttributes.masterKeyDecryptionNonce,
- keyAttributes.recoveryKeyEncryptedWithMasterKey,
- keyAttributes.recoveryKeyDecryptionNonce,
- );
- await _dio.put(
- _config.getHttpEndpoint() + "/users/recovery-key",
- data: setRecoveryKeyRequest.toMap(),
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- await _config.setKeyAttributes(keyAttributes);
- } catch (e) {
- _logger.severe(e);
- rethrow;
- }
- }
- Future<void> verifyTwoFactor(
- BuildContext context,
- String sessionID,
- String code,
- ) async {
- final dialog = createProgressDialog(context, "Authenticating...");
- await dialog.show();
- try {
- final response = await _dio.post(
- _config.getHttpEndpoint() + "/users/two-factor/verify",
- data: {
- "sessionID": sessionID,
- "code": code,
- },
- );
- await dialog.hide();
- if (response != null && response.statusCode == 200) {
- showToast(context, "Authentication successful!");
- await _saveConfiguration(response);
- Navigator.of(context).pushAndRemoveUntil(
- MaterialPageRoute(
- builder: (BuildContext context) {
- return const PasswordReentryPage();
- },
- ),
- (route) => route.isFirst,
- );
- }
- } on DioError catch (e) {
- await dialog.hide();
- _logger.severe(e);
- if (e.response != null && e.response.statusCode == 404) {
- showToast(context, "Session expired");
- Navigator.of(context).pushAndRemoveUntil(
- MaterialPageRoute(
- builder: (BuildContext context) {
- return const LoginPage();
- },
- ),
- (route) => route.isFirst,
- );
- } else {
- showErrorDialog(
- context,
- "Incorrect code",
- "Authentication failed, please try again",
- );
- }
- } catch (e) {
- await dialog.hide();
- _logger.severe(e);
- showErrorDialog(
- context,
- "Oops",
- "Authentication failed, please try again",
- );
- }
- }
- Future<void> recoverTwoFactor(BuildContext context, String sessionID) async {
- final dialog = createProgressDialog(context, "Please wait...");
- await dialog.show();
- try {
- final response = await _dio.get(
- _config.getHttpEndpoint() + "/users/two-factor/recover",
- queryParameters: {
- "sessionID": sessionID,
- },
- );
- if (response != null && response.statusCode == 200) {
- Navigator.of(context).pushAndRemoveUntil(
- MaterialPageRoute(
- builder: (BuildContext context) {
- return TwoFactorRecoveryPage(
- sessionID,
- response.data["encryptedSecret"],
- response.data["secretDecryptionNonce"],
- );
- },
- ),
- (route) => route.isFirst,
- );
- }
- } on DioError catch (e) {
- _logger.severe(e);
- if (e.response != null && e.response.statusCode == 404) {
- showToast(context, "Session expired");
- Navigator.of(context).pushAndRemoveUntil(
- MaterialPageRoute(
- builder: (BuildContext context) {
- return const LoginPage();
- },
- ),
- (route) => route.isFirst,
- );
- } else {
- showErrorDialog(
- context,
- "Oops",
- "Something went wrong, please try again",
- );
- }
- } catch (e) {
- _logger.severe(e);
- showErrorDialog(
- context,
- "Oops",
- "Something went wrong, please try again",
- );
- } finally {
- await dialog.hide();
- }
- }
- Future<void> removeTwoFactor(
- BuildContext context,
- String sessionID,
- String recoveryKey,
- String encryptedSecret,
- String secretDecryptionNonce,
- ) async {
- final dialog = createProgressDialog(context, "Please wait...");
- await dialog.show();
- String secret;
- try {
- secret = Sodium.bin2base64(
- await CryptoUtil.decrypt(
- Sodium.base642bin(encryptedSecret),
- Sodium.hex2bin(recoveryKey.trim()),
- Sodium.base642bin(secretDecryptionNonce),
- ),
- );
- } catch (e) {
- await dialog.hide();
- showErrorDialog(
- context,
- "Incorrect recovery key",
- "The recovery key you entered is incorrect",
- );
- return;
- }
- try {
- final response = await _dio.post(
- _config.getHttpEndpoint() + "/users/two-factor/remove",
- data: {
- "sessionID": sessionID,
- "secret": secret,
- },
- );
- if (response != null && response.statusCode == 200) {
- showShortToast(context, "Two-factor authentication successfully reset");
- await _saveConfiguration(response);
- Navigator.of(context).pushAndRemoveUntil(
- MaterialPageRoute(
- builder: (BuildContext context) {
- return const PasswordReentryPage();
- },
- ),
- (route) => route.isFirst,
- );
- }
- } on DioError catch (e) {
- _logger.severe(e);
- if (e.response != null && e.response.statusCode == 404) {
- showToast(context, "Session expired");
- Navigator.of(context).pushAndRemoveUntil(
- MaterialPageRoute(
- builder: (BuildContext context) {
- return const LoginPage();
- },
- ),
- (route) => route.isFirst,
- );
- } else {
- showErrorDialog(
- context,
- "Oops",
- "Something went wrong, please try again",
- );
- }
- } catch (e) {
- _logger.severe(e);
- showErrorDialog(
- context,
- "Oops",
- "Something went wrong, please try again",
- );
- } finally {
- await dialog.hide();
- }
- }
- Future<void> setupTwoFactor(BuildContext context) async {
- final dialog = createProgressDialog(context, "Please wait...");
- await dialog.show();
- try {
- final response = await _dio.post(
- _config.getHttpEndpoint() + "/users/two-factor/setup",
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- await dialog.hide();
- routeToPage(
- context,
- TwoFactorSetupPage(
- response.data["secretCode"],
- response.data["qrCode"],
- ),
- );
- } catch (e, s) {
- await dialog.hide();
- _logger.severe(e, s);
- rethrow;
- }
- }
- Future<bool> enableTwoFactor(
- BuildContext context,
- String secret,
- String code,
- ) async {
- Uint8List recoveryKey;
- try {
- recoveryKey = await getOrCreateRecoveryKey(context);
- } catch (e) {
- showGenericErrorDialog(context);
- return false;
- }
- final dialog = createProgressDialog(context, "Verifying...");
- await dialog.show();
- final encryptionResult =
- CryptoUtil.encryptSync(Sodium.base642bin(secret), recoveryKey);
- try {
- await _dio.post(
- _config.getHttpEndpoint() + "/users/two-factor/enable",
- data: {
- "code": code,
- "encryptedTwoFactorSecret":
- Sodium.bin2base64(encryptionResult.encryptedData),
- "twoFactorSecretDecryptionNonce":
- Sodium.bin2base64(encryptionResult.nonce),
- },
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- await dialog.hide();
- Navigator.pop(context);
- Bus.instance.fire(TwoFactorStatusChangeEvent(true));
- return true;
- } catch (e, s) {
- await dialog.hide();
- _logger.severe(e, s);
- if (e is DioError) {
- if (e.response != null && e.response.statusCode == 401) {
- showErrorDialog(
- context,
- "Incorrect code",
- "Please verify the code you have entered",
- );
- return false;
- }
- }
- showErrorDialog(
- context,
- "Something went wrong",
- "Please contact support if the problem persists",
- );
- }
- return false;
- }
- Future<void> disableTwoFactor(BuildContext context) async {
- final dialog =
- createProgressDialog(context, "Disabling two-factor authentication...");
- await dialog.show();
- try {
- await _dio.post(
- _config.getHttpEndpoint() + "/users/two-factor/disable",
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- Bus.instance.fire(TwoFactorStatusChangeEvent(false));
- await dialog.hide();
- showToast(context, "Two-factor authentication has been disabled");
- } catch (e, s) {
- await dialog.hide();
- _logger.severe(e, s);
- showErrorDialog(
- context,
- "Something went wrong",
- "Please contact support if the problem persists",
- );
- }
- }
- Future<bool> fetchTwoFactorStatus() async {
- try {
- final response = await _dio.get(
- _config.getHttpEndpoint() + "/users/two-factor/status",
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- return response.data["status"];
- } catch (e, s) {
- _logger.severe(e, s);
- rethrow;
- }
- }
- Future<Uint8List> getOrCreateRecoveryKey(BuildContext context) async {
- final encryptedRecoveryKey =
- _config.getKeyAttributes().recoveryKeyEncryptedWithMasterKey;
- if (encryptedRecoveryKey == null || encryptedRecoveryKey.isEmpty) {
- final dialog = createProgressDialog(context, "Please wait...");
- await dialog.show();
- try {
- final keyAttributes = await _config.createNewRecoveryKey();
- await setRecoveryKey(keyAttributes);
- await dialog.hide();
- } catch (e, s) {
- await dialog.hide();
- _logger.severe(e, s);
- rethrow;
- }
- }
- final recoveryKey = _config.getRecoveryKey();
- return recoveryKey;
- }
- Future<String> getPaymentToken() async {
- try {
- var response = await _dio.get(
- "${_config.getHttpEndpoint()}/users/payment-token",
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- if (response != null && response.statusCode == 200) {
- return response.data["paymentToken"];
- } else {
- throw Exception("non 200 ok response");
- }
- } catch (e, s) {
- _logger.severe(e, s);
- return null;
- }
- }
- Future<String> getFamiliesToken() async {
- try {
- var response = await _dio.get(
- "${_config.getHttpEndpoint()}/users/families-token",
- options: Options(
- headers: {
- "X-Auth-Token": _config.getToken(),
- },
- ),
- );
- if (response != null && response.statusCode == 200) {
- return response.data["familiesToken"];
- } else {
- throw Exception("non 200 ok response");
- }
- } catch (e, s) {
- _logger.severe("failed to fetch families token", e, s);
- rethrow;
- }
- }
- Future<void> _saveConfiguration(Response response) async {
- await Configuration.instance.setUserID(response.data["id"]);
- if (response.data["encryptedToken"] != null) {
- await Configuration.instance
- .setEncryptedToken(response.data["encryptedToken"]);
- await Configuration.instance.setKeyAttributes(
- KeyAttributes.fromMap(response.data["keyAttributes"]),
- );
- } else {
- await Configuration.instance.setToken(response.data["token"]);
- }
- }
- }
|