user_service.dart 24 KB

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