user_service.dart 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  1. // @dart=2.9
  2. import 'dart:async';
  3. import 'dart:typed_data';
  4. import 'package:bip39/bip39.dart' as bip39;
  5. import 'package:dio/dio.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter_sodium/flutter_sodium.dart';
  8. import 'package:logging/logging.dart';
  9. import 'package:photos/core/configuration.dart';
  10. import 'package:photos/core/constants.dart';
  11. import 'package:photos/core/event_bus.dart';
  12. import 'package:photos/core/network.dart';
  13. import 'package:photos/db/public_keys_db.dart';
  14. import 'package:photos/events/two_factor_status_change_event.dart';
  15. import 'package:photos/events/user_details_changed_event.dart';
  16. import 'package:photos/models/delete_account.dart';
  17. import 'package:photos/models/key_attributes.dart';
  18. import 'package:photos/models/key_gen_result.dart';
  19. import 'package:photos/models/public_key.dart';
  20. import 'package:photos/models/sessions.dart';
  21. import 'package:photos/models/set_keys_request.dart';
  22. import 'package:photos/models/set_recovery_key_request.dart';
  23. import 'package:photos/models/user_details.dart';
  24. import 'package:photos/ui/account/login_page.dart';
  25. import 'package:photos/ui/account/ott_verification_page.dart';
  26. import 'package:photos/ui/account/password_entry_page.dart';
  27. import 'package:photos/ui/account/password_reentry_page.dart';
  28. import 'package:photos/ui/account/two_factor_authentication_page.dart';
  29. import 'package:photos/ui/account/two_factor_recovery_page.dart';
  30. import 'package:photos/ui/account/two_factor_setup_page.dart';
  31. import 'package:photos/ui/components/dialog_widget.dart';
  32. import 'package:photos/utils/crypto_util.dart';
  33. import 'package:photos/utils/dialog_util.dart';
  34. import 'package:photos/utils/navigation_util.dart';
  35. import 'package:photos/utils/toast_util.dart';
  36. import 'package:shared_preferences/shared_preferences.dart';
  37. class UserService {
  38. static const keyHasEnabledTwoFactor = "has_enabled_two_factor";
  39. final _dio = Network.instance.getDio();
  40. final _enteDio = Network.instance.enteDio;
  41. final _logger = Logger((UserService).toString());
  42. final _config = Configuration.instance;
  43. SharedPreferences _preferences;
  44. ValueNotifier<String> emailValueNotifier;
  45. UserService._privateConstructor();
  46. static final UserService instance = UserService._privateConstructor();
  47. Future<void> init() async {
  48. emailValueNotifier =
  49. ValueNotifier<String>(Configuration.instance.getEmail());
  50. _preferences = await SharedPreferences.getInstance();
  51. if (Configuration.instance.isLoggedIn()) {
  52. // add artificial delay in refreshing 2FA status
  53. Future.delayed(
  54. const Duration(seconds: 5),
  55. () => {setTwoFactor(fetchTwoFactorStatus: true).ignore()},
  56. );
  57. }
  58. Bus.instance.on<TwoFactorStatusChangeEvent>().listen((event) {
  59. setTwoFactor(value: event.status);
  60. });
  61. }
  62. Future<void> sendOtt(
  63. BuildContext context,
  64. String email, {
  65. bool isChangeEmail = false,
  66. bool isCreateAccountScreen = false,
  67. }) async {
  68. final dialog = createProgressDialog(context, "Please wait...");
  69. await dialog.show();
  70. try {
  71. final response = await _dio.post(
  72. _config.getHttpEndpoint() + "/users/ott",
  73. data: {"email": email, "purpose": isChangeEmail ? "change" : ""},
  74. );
  75. await dialog.hide();
  76. if (response != null && response.statusCode == 200) {
  77. unawaited(
  78. Navigator.of(context).push(
  79. MaterialPageRoute(
  80. builder: (BuildContext context) {
  81. return OTTVerificationPage(
  82. email,
  83. isChangeEmail: isChangeEmail,
  84. isCreateAccountScreen: isCreateAccountScreen,
  85. );
  86. },
  87. ),
  88. ),
  89. );
  90. return;
  91. }
  92. unawaited(showGenericErrorDialog(context: context));
  93. } on DioError catch (e) {
  94. await dialog.hide();
  95. _logger.info(e);
  96. if (e.response != null && e.response.statusCode == 403) {
  97. unawaited(
  98. showErrorDialog(
  99. context,
  100. "Oops",
  101. "This email is already in use",
  102. ),
  103. );
  104. } else {
  105. unawaited(showGenericErrorDialog(context: context));
  106. }
  107. } catch (e) {
  108. await dialog.hide();
  109. _logger.severe(e);
  110. unawaited(showGenericErrorDialog(context: context));
  111. }
  112. }
  113. Future<String> getPublicKey(String email) async {
  114. try {
  115. final response = await _enteDio.get(
  116. "/users/public-key",
  117. queryParameters: {"email": email},
  118. );
  119. final publicKey = response.data["publicKey"];
  120. await PublicKeysDB.instance.setKey(PublicKey(email, publicKey));
  121. return publicKey;
  122. } on DioError catch (e) {
  123. _logger.info(e);
  124. return null;
  125. }
  126. }
  127. Future<UserDetails> getUserDetailsV2({bool memoryCount = true}) async {
  128. try {
  129. final response = await _enteDio.get(
  130. "/users/details/v2",
  131. queryParameters: {
  132. "memoryCount": memoryCount,
  133. },
  134. );
  135. return UserDetails.fromMap(response.data);
  136. } on DioError catch (e) {
  137. _logger.info(e);
  138. rethrow;
  139. }
  140. }
  141. Future<Sessions> getActiveSessions() async {
  142. try {
  143. final response = await _enteDio.get("/users/sessions");
  144. return Sessions.fromMap(response.data);
  145. } on DioError catch (e) {
  146. _logger.info(e);
  147. rethrow;
  148. }
  149. }
  150. Future<void> terminateSession(String token) async {
  151. try {
  152. await _enteDio.delete(
  153. "/users/session",
  154. queryParameters: {
  155. "token": token,
  156. },
  157. );
  158. } on DioError catch (e) {
  159. _logger.info(e);
  160. rethrow;
  161. }
  162. }
  163. Future<void> leaveFamilyPlan() async {
  164. try {
  165. await _enteDio.delete("/family/leave");
  166. } on DioError catch (e) {
  167. _logger.warning('failed to leave family plan', e);
  168. rethrow;
  169. }
  170. }
  171. Future<void> logout(BuildContext context) async {
  172. final dialog = createProgressDialog(context, "Logging out...");
  173. await dialog.show();
  174. try {
  175. final response = await _enteDio.post("/users/logout");
  176. if (response != null && response.statusCode == 200) {
  177. await Configuration.instance.logout();
  178. await dialog.hide();
  179. Navigator.of(context).popUntil((route) => route.isFirst);
  180. } else {
  181. throw Exception("Log out action failed");
  182. }
  183. } catch (e) {
  184. _logger.severe(e);
  185. await dialog.hide();
  186. showGenericErrorDialog(context: context);
  187. }
  188. }
  189. Future<DeleteChallengeResponse> getDeleteChallenge(
  190. BuildContext context,
  191. ) async {
  192. final dialog = createProgressDialog(context, "Please wait...");
  193. await dialog.show();
  194. try {
  195. final response = await _enteDio.get("/users/delete-challenge");
  196. if (response != null && response.statusCode == 200) {
  197. // clear data
  198. await dialog.hide();
  199. return DeleteChallengeResponse(
  200. allowDelete: response.data["allowDelete"] as bool,
  201. encryptedChallenge: response.data["encryptedChallenge"],
  202. );
  203. } else {
  204. throw Exception("delete action failed");
  205. }
  206. } catch (e) {
  207. _logger.severe(e);
  208. await dialog.hide();
  209. await showGenericErrorDialog(context: context);
  210. return null;
  211. }
  212. }
  213. Future<void> deleteAccount(
  214. BuildContext context,
  215. String challengeResponse,
  216. ) async {
  217. try {
  218. final response = await _enteDio.delete(
  219. "/users/delete",
  220. data: {
  221. "challenge": challengeResponse,
  222. },
  223. );
  224. if (response != null && response.statusCode == 200) {
  225. // clear data
  226. await Configuration.instance.logout();
  227. } else {
  228. throw Exception("delete action failed");
  229. }
  230. } catch (e) {
  231. _logger.severe(e);
  232. rethrow;
  233. }
  234. }
  235. Future<void> verifyEmail(BuildContext context, String ott) async {
  236. final dialog = createProgressDialog(context, "Please wait...");
  237. await dialog.show();
  238. try {
  239. final response = await _dio.post(
  240. _config.getHttpEndpoint() + "/users/verify-email",
  241. data: {
  242. "email": _config.getEmail(),
  243. "ott": ott,
  244. },
  245. );
  246. await dialog.hide();
  247. if (response != null && response.statusCode == 200) {
  248. Widget page;
  249. final String twoFASessionID = response.data["twoFactorSessionID"];
  250. if (twoFASessionID != null && twoFASessionID.isNotEmpty) {
  251. page = TwoFactorAuthenticationPage(twoFASessionID);
  252. } else {
  253. await _saveConfiguration(response);
  254. if (Configuration.instance.getEncryptedToken() != null) {
  255. page = const PasswordReentryPage();
  256. } else {
  257. page = const PasswordEntryPage();
  258. }
  259. }
  260. Navigator.of(context).pushAndRemoveUntil(
  261. MaterialPageRoute(
  262. builder: (BuildContext context) {
  263. return page;
  264. },
  265. ),
  266. (route) => route.isFirst,
  267. );
  268. } else {
  269. // should never reach here
  270. throw Exception("unexpected response during email verification");
  271. }
  272. } on DioError catch (e) {
  273. _logger.info(e);
  274. await dialog.hide();
  275. if (e.response != null && e.response.statusCode == 410) {
  276. await showErrorDialog(
  277. context,
  278. "Oops",
  279. "Your verification code has expired",
  280. );
  281. Navigator.of(context).pop();
  282. } else {
  283. showErrorDialog(
  284. context,
  285. "Incorrect code",
  286. "Sorry, the code you've entered is incorrect",
  287. );
  288. }
  289. } catch (e) {
  290. await dialog.hide();
  291. _logger.severe(e);
  292. showErrorDialog(context, "Oops", "Verification failed, please try again");
  293. }
  294. }
  295. Future<void> setEmail(String email) async {
  296. await _config.setEmail(email);
  297. emailValueNotifier.value = email ?? "";
  298. }
  299. Future<void> changeEmail(
  300. BuildContext context,
  301. String email,
  302. String ott,
  303. ) async {
  304. final dialog = createProgressDialog(context, "Please wait...");
  305. await dialog.show();
  306. try {
  307. final response = await _enteDio.post(
  308. "/users/change-email",
  309. data: {
  310. "email": email,
  311. "ott": ott,
  312. },
  313. );
  314. await dialog.hide();
  315. if (response != null && response.statusCode == 200) {
  316. showShortToast(context, "Email changed to " + email);
  317. await setEmail(email);
  318. Navigator.of(context).popUntil((route) => route.isFirst);
  319. Bus.instance.fire(UserDetailsChangedEvent());
  320. return;
  321. }
  322. showErrorDialog(context, "Oops", "Verification failed, please try again");
  323. } on DioError catch (e) {
  324. await dialog.hide();
  325. if (e.response != null && e.response.statusCode == 403) {
  326. showErrorDialog(context, "Oops", "This email is already in use");
  327. } else {
  328. showErrorDialog(
  329. context,
  330. "Incorrect code",
  331. "Authentication failed, please try again",
  332. );
  333. }
  334. } catch (e) {
  335. await dialog.hide();
  336. _logger.severe(e);
  337. showErrorDialog(context, "Oops", "Verification failed, please try again");
  338. }
  339. }
  340. Future<void> setAttributes(KeyGenResult result) async {
  341. try {
  342. final name = _config.getName();
  343. await _enteDio.put(
  344. "/users/attributes",
  345. data: {
  346. "name": name,
  347. "keyAttributes": result.keyAttributes.toMap(),
  348. },
  349. );
  350. await _config.setKey(result.privateKeyAttributes.key);
  351. await _config.setSecretKey(result.privateKeyAttributes.secretKey);
  352. await _config.setKeyAttributes(result.keyAttributes);
  353. } catch (e) {
  354. _logger.severe(e);
  355. rethrow;
  356. }
  357. }
  358. Future<void> updateKeyAttributes(KeyAttributes keyAttributes) async {
  359. try {
  360. final setKeyRequest = SetKeysRequest(
  361. kekSalt: keyAttributes.kekSalt,
  362. encryptedKey: keyAttributes.encryptedKey,
  363. keyDecryptionNonce: keyAttributes.keyDecryptionNonce,
  364. memLimit: keyAttributes.memLimit,
  365. opsLimit: keyAttributes.opsLimit,
  366. );
  367. await _enteDio.put(
  368. "/users/keys",
  369. data: setKeyRequest.toMap(),
  370. );
  371. await _config.setKeyAttributes(keyAttributes);
  372. } catch (e) {
  373. _logger.severe(e);
  374. rethrow;
  375. }
  376. }
  377. Future<void> setRecoveryKey(KeyAttributes keyAttributes) async {
  378. try {
  379. final setRecoveryKeyRequest = SetRecoveryKeyRequest(
  380. keyAttributes.masterKeyEncryptedWithRecoveryKey,
  381. keyAttributes.masterKeyDecryptionNonce,
  382. keyAttributes.recoveryKeyEncryptedWithMasterKey,
  383. keyAttributes.recoveryKeyDecryptionNonce,
  384. );
  385. await _enteDio.put(
  386. "/users/recovery-key",
  387. data: setRecoveryKeyRequest.toMap(),
  388. );
  389. await _config.setKeyAttributes(keyAttributes);
  390. } catch (e) {
  391. _logger.severe(e);
  392. rethrow;
  393. }
  394. }
  395. Future<void> verifyTwoFactor(
  396. BuildContext context,
  397. String sessionID,
  398. String code,
  399. ) async {
  400. final dialog = createProgressDialog(context, "Authenticating...");
  401. await dialog.show();
  402. try {
  403. final response = await _dio.post(
  404. _config.getHttpEndpoint() + "/users/two-factor/verify",
  405. data: {
  406. "sessionID": sessionID,
  407. "code": code,
  408. },
  409. );
  410. await dialog.hide();
  411. if (response != null && response.statusCode == 200) {
  412. showShortToast(context, "Authentication successful!");
  413. await _saveConfiguration(response);
  414. Navigator.of(context).pushAndRemoveUntil(
  415. MaterialPageRoute(
  416. builder: (BuildContext context) {
  417. return const PasswordReentryPage();
  418. },
  419. ),
  420. (route) => route.isFirst,
  421. );
  422. }
  423. } on DioError catch (e) {
  424. await dialog.hide();
  425. _logger.severe(e);
  426. if (e.response != null && e.response.statusCode == 404) {
  427. showToast(context, "Session expired");
  428. Navigator.of(context).pushAndRemoveUntil(
  429. MaterialPageRoute(
  430. builder: (BuildContext context) {
  431. return const LoginPage();
  432. },
  433. ),
  434. (route) => route.isFirst,
  435. );
  436. } else {
  437. showErrorDialog(
  438. context,
  439. "Incorrect code",
  440. "Authentication failed, please try again",
  441. );
  442. }
  443. } catch (e) {
  444. await dialog.hide();
  445. _logger.severe(e);
  446. showErrorDialog(
  447. context,
  448. "Oops",
  449. "Authentication failed, please try again",
  450. );
  451. }
  452. }
  453. Future<void> recoverTwoFactor(BuildContext context, String sessionID) async {
  454. final dialog = createProgressDialog(context, "Please wait...");
  455. await dialog.show();
  456. try {
  457. final response = await _dio.get(
  458. _config.getHttpEndpoint() + "/users/two-factor/recover",
  459. queryParameters: {
  460. "sessionID": sessionID,
  461. },
  462. );
  463. if (response != null && response.statusCode == 200) {
  464. Navigator.of(context).pushAndRemoveUntil(
  465. MaterialPageRoute(
  466. builder: (BuildContext context) {
  467. return TwoFactorRecoveryPage(
  468. sessionID,
  469. response.data["encryptedSecret"],
  470. response.data["secretDecryptionNonce"],
  471. );
  472. },
  473. ),
  474. (route) => route.isFirst,
  475. );
  476. }
  477. } on DioError catch (e) {
  478. _logger.severe(e);
  479. if (e.response != null && e.response.statusCode == 404) {
  480. showToast(context, "Session expired");
  481. Navigator.of(context).pushAndRemoveUntil(
  482. MaterialPageRoute(
  483. builder: (BuildContext context) {
  484. return const LoginPage();
  485. },
  486. ),
  487. (route) => route.isFirst,
  488. );
  489. } else {
  490. showErrorDialog(
  491. context,
  492. "Oops",
  493. "Something went wrong, please try again",
  494. );
  495. }
  496. } catch (e) {
  497. _logger.severe(e);
  498. showErrorDialog(
  499. context,
  500. "Oops",
  501. "Something went wrong, please try again",
  502. );
  503. } finally {
  504. await dialog.hide();
  505. }
  506. }
  507. Future<void> removeTwoFactor(
  508. BuildContext context,
  509. String sessionID,
  510. String recoveryKey,
  511. String encryptedSecret,
  512. String secretDecryptionNonce,
  513. ) async {
  514. final dialog = createProgressDialog(context, "Please wait...");
  515. await dialog.show();
  516. String secret;
  517. try {
  518. if (recoveryKey.contains(' ')) {
  519. if (recoveryKey.split(' ').length != mnemonicKeyWordCount) {
  520. throw AssertionError(
  521. 'recovery code should have $mnemonicKeyWordCount words',
  522. );
  523. }
  524. recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
  525. }
  526. secret = Sodium.bin2base64(
  527. await CryptoUtil.decrypt(
  528. Sodium.base642bin(encryptedSecret),
  529. Sodium.hex2bin(recoveryKey.trim()),
  530. Sodium.base642bin(secretDecryptionNonce),
  531. ),
  532. );
  533. } catch (e) {
  534. await dialog.hide();
  535. await showErrorDialog(
  536. context,
  537. "Incorrect recovery key",
  538. "The recovery key you entered is incorrect",
  539. );
  540. return;
  541. }
  542. try {
  543. final response = await _dio.post(
  544. _config.getHttpEndpoint() + "/users/two-factor/remove",
  545. data: {
  546. "sessionID": sessionID,
  547. "secret": secret,
  548. },
  549. );
  550. if (response != null && response.statusCode == 200) {
  551. showShortToast(context, "Two-factor authentication successfully reset");
  552. await _saveConfiguration(response);
  553. Navigator.of(context).pushAndRemoveUntil(
  554. MaterialPageRoute(
  555. builder: (BuildContext context) {
  556. return const PasswordReentryPage();
  557. },
  558. ),
  559. (route) => route.isFirst,
  560. );
  561. }
  562. } on DioError catch (e) {
  563. _logger.severe(e);
  564. if (e.response != null && e.response.statusCode == 404) {
  565. showToast(context, "Session expired");
  566. Navigator.of(context).pushAndRemoveUntil(
  567. MaterialPageRoute(
  568. builder: (BuildContext context) {
  569. return const LoginPage();
  570. },
  571. ),
  572. (route) => route.isFirst,
  573. );
  574. } else {
  575. showErrorDialog(
  576. context,
  577. "Oops",
  578. "Something went wrong, please try again",
  579. );
  580. }
  581. } catch (e) {
  582. _logger.severe(e);
  583. showErrorDialog(
  584. context,
  585. "Oops",
  586. "Something went wrong, please try again",
  587. );
  588. } finally {
  589. await dialog.hide();
  590. }
  591. }
  592. Future<void> setupTwoFactor(BuildContext context, Completer completer) async {
  593. final dialog = createProgressDialog(context, "Please wait...");
  594. await dialog.show();
  595. try {
  596. final response = await _enteDio.post("/users/two-factor/setup");
  597. await dialog.hide();
  598. unawaited(
  599. routeToPage(
  600. context,
  601. TwoFactorSetupPage(
  602. response.data["secretCode"],
  603. response.data["qrCode"],
  604. completer,
  605. ),
  606. ),
  607. );
  608. } catch (e) {
  609. await dialog.hide();
  610. _logger.severe("Failed to setup tfa", e);
  611. completer.complete();
  612. rethrow;
  613. }
  614. }
  615. Future<bool> enableTwoFactor(
  616. BuildContext context,
  617. String secret,
  618. String code,
  619. ) async {
  620. Uint8List recoveryKey;
  621. try {
  622. recoveryKey = await getOrCreateRecoveryKey(context);
  623. } catch (e) {
  624. showGenericErrorDialog(context: context);
  625. return false;
  626. }
  627. final dialog = createProgressDialog(context, "Verifying...");
  628. await dialog.show();
  629. final encryptionResult =
  630. CryptoUtil.encryptSync(Sodium.base642bin(secret), recoveryKey);
  631. try {
  632. await _enteDio.post(
  633. "/users/two-factor/enable",
  634. data: {
  635. "code": code,
  636. "encryptedTwoFactorSecret":
  637. Sodium.bin2base64(encryptionResult.encryptedData),
  638. "twoFactorSecretDecryptionNonce":
  639. Sodium.bin2base64(encryptionResult.nonce),
  640. },
  641. );
  642. await dialog.hide();
  643. Navigator.pop(context);
  644. Bus.instance.fire(TwoFactorStatusChangeEvent(true));
  645. return true;
  646. } catch (e, s) {
  647. await dialog.hide();
  648. _logger.severe(e, s);
  649. if (e is DioError) {
  650. if (e.response != null && e.response.statusCode == 401) {
  651. showErrorDialog(
  652. context,
  653. "Incorrect code",
  654. "Please verify the code you have entered",
  655. );
  656. return false;
  657. }
  658. }
  659. showErrorDialog(
  660. context,
  661. "Something went wrong",
  662. "Please contact support if the problem persists",
  663. );
  664. }
  665. return false;
  666. }
  667. Future<void> disableTwoFactor(BuildContext context) async {
  668. final dialog =
  669. createProgressDialog(context, "Disabling two-factor authentication...");
  670. await dialog.show();
  671. try {
  672. await _enteDio.post(
  673. "/users/two-factor/disable",
  674. );
  675. await dialog.hide();
  676. Bus.instance.fire(TwoFactorStatusChangeEvent(false));
  677. unawaited(
  678. showShortToast(
  679. context,
  680. "Two-factor authentication has been disabled",
  681. ),
  682. );
  683. } catch (e) {
  684. await dialog.hide();
  685. _logger.severe("Failed to disabled 2FA", e);
  686. await showErrorDialog(
  687. context,
  688. "Something went wrong",
  689. "Please contact support if the problem persists",
  690. );
  691. }
  692. }
  693. Future<bool> fetchTwoFactorStatus() async {
  694. try {
  695. final response = await _enteDio.get("/users/two-factor/status");
  696. setTwoFactor(value: response.data["status"]);
  697. return response.data["status"];
  698. } catch (e) {
  699. _logger.severe("Failed to fetch 2FA status", e);
  700. rethrow;
  701. }
  702. }
  703. Future<Uint8List> getOrCreateRecoveryKey(BuildContext context) async {
  704. final encryptedRecoveryKey =
  705. _config.getKeyAttributes().recoveryKeyEncryptedWithMasterKey;
  706. if (encryptedRecoveryKey == null || encryptedRecoveryKey.isEmpty) {
  707. final dialog = createProgressDialog(context, "Please wait...");
  708. await dialog.show();
  709. try {
  710. final keyAttributes = await _config.createNewRecoveryKey();
  711. await setRecoveryKey(keyAttributes);
  712. await dialog.hide();
  713. } catch (e, s) {
  714. await dialog.hide();
  715. _logger.severe(e, s);
  716. rethrow;
  717. }
  718. }
  719. final recoveryKey = _config.getRecoveryKey();
  720. return recoveryKey;
  721. }
  722. Future<String> getPaymentToken() async {
  723. try {
  724. final response = await _enteDio.get("/users/payment-token");
  725. if (response != null && response.statusCode == 200) {
  726. return response.data["paymentToken"];
  727. } else {
  728. throw Exception("non 200 ok response");
  729. }
  730. } catch (e) {
  731. _logger.severe("Failed to get payment token", e);
  732. return null;
  733. }
  734. }
  735. Future<String> getFamiliesToken() async {
  736. try {
  737. final response = await _enteDio.get("/users/families-token");
  738. if (response != null && response.statusCode == 200) {
  739. return response.data["familiesToken"];
  740. } else {
  741. throw Exception("non 200 ok response");
  742. }
  743. } catch (e, s) {
  744. _logger.severe("failed to fetch families token", e, s);
  745. rethrow;
  746. }
  747. }
  748. Future<void> _saveConfiguration(Response response) async {
  749. await Configuration.instance.setUserID(response.data["id"]);
  750. if (response.data["encryptedToken"] != null) {
  751. await Configuration.instance
  752. .setEncryptedToken(response.data["encryptedToken"]);
  753. await Configuration.instance.setKeyAttributes(
  754. KeyAttributes.fromMap(response.data["keyAttributes"]),
  755. );
  756. } else {
  757. await Configuration.instance.setToken(response.data["token"]);
  758. }
  759. }
  760. Future<void> setTwoFactor({
  761. bool value = false,
  762. bool fetchTwoFactorStatus = false,
  763. }) async {
  764. if (fetchTwoFactorStatus) {
  765. value = await UserService.instance.fetchTwoFactorStatus();
  766. }
  767. _preferences.setBool(keyHasEnabledTwoFactor, value);
  768. }
  769. bool hasEnabledTwoFactor() {
  770. return _preferences.getBool(keyHasEnabledTwoFactor);
  771. }
  772. }