Browse Source

Merge pull request #1 from ente-io/login_with_email

Login with email
Vishnu Mohandas 4 years ago
parent
commit
93b1fb446d

File diff suppressed because it is too large
+ 0 - 0
assets/email_sent.svg


File diff suppressed because it is too large
+ 0 - 0
assets/vault.svg


+ 8 - 17
lib/core/configuration.dart

@@ -12,10 +12,9 @@ class Configuration {
   static final Configuration instance = Configuration._privateConstructor();
   static final Configuration instance = Configuration._privateConstructor();
 
 
   static const endpointKey = "endpoint";
   static const endpointKey = "endpoint";
-  static const tokenKey = "token";
-  static const usernameKey = "username";
   static const userIDKey = "user_id";
   static const userIDKey = "user_id";
-  static const passwordKey = "password";
+  static const emailKey = "email";
+  static const tokenKey = "token";
   static const hasOptedForE2EKey = "has_opted_for_e2e_encryption";
   static const hasOptedForE2EKey = "has_opted_for_e2e_encryption";
   static const keyKey = "key";
   static const keyKey = "key";
   static const keyEncryptedKey = "encrypted_key";
   static const keyEncryptedKey = "encrypted_key";
@@ -50,7 +49,7 @@ class Configuration {
   }
   }
 
 
   String getEndpoint() {
   String getEndpoint() {
-    return _preferences.getString(endpointKey);
+    return "192.168.0.106";
   }
   }
 
 
   String getHttpEndpoint() {
   String getHttpEndpoint() {
@@ -72,12 +71,12 @@ class Configuration {
     await _preferences.setString(tokenKey, token);
     await _preferences.setString(tokenKey, token);
   }
   }
 
 
-  String getUsername() {
-    return _preferences.getString(usernameKey);
+  String getEmail() {
+    return _preferences.getString(emailKey);
   }
   }
 
 
-  void setUsername(String username) async {
-    await _preferences.setString(usernameKey, username);
+  void setEmail(String email) async {
+    await _preferences.setString(emailKey, email);
   }
   }
 
 
   int getUserID() {
   int getUserID() {
@@ -88,14 +87,6 @@ class Configuration {
     await _preferences.setInt(userIDKey, userID);
     await _preferences.setInt(userIDKey, userID);
   }
   }
 
 
-  String getPassword() {
-    return _preferences.getString(passwordKey);
-  }
-
-  void setPassword(String password) async {
-    await _preferences.setString(passwordKey, password);
-  }
-
   void setOptInForE2E(bool hasOptedForE2E) async {
   void setOptInForE2E(bool hasOptedForE2E) async {
     await _preferences.setBool(hasOptedForE2EKey, hasOptedForE2E);
     await _preferences.setBool(hasOptedForE2EKey, hasOptedForE2E);
   }
   }
@@ -143,6 +134,6 @@ class Configuration {
   }
   }
 
 
   bool hasConfiguredAccount() {
   bool hasConfiguredAccount() {
-    return getEndpoint() != null && getToken() != null;
+    return getToken() != null && getKey() != null;
   }
   }
 }
 }

+ 1 - 1
lib/folder_service.dart

@@ -136,7 +136,7 @@ class FolderSharingService {
       try {
       try {
         return Folder(
         return Folder(
           null,
           null,
-          Configuration.instance.getUsername() + "s " + deviceFolder,
+          Configuration.instance.getEmail() + "s " + deviceFolder,
           Configuration.instance.getUserID(),
           Configuration.instance.getUserID(),
           deviceFolder,
           deviceFolder,
           Set<int>(),
           Set<int>(),

+ 5 - 30
lib/main.dart

@@ -2,7 +2,6 @@ import 'dart:async';
 
 
 import 'package:computer/computer.dart';
 import 'package:computer/computer.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
@@ -14,7 +13,6 @@ import 'package:photos/ui/home_widget.dart';
 import 'package:sentry/sentry.dart';
 import 'package:sentry/sentry.dart';
 import 'package:super_logging/super_logging.dart';
 import 'package:super_logging/super_logging.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
-import 'package:uni_links/uni_links.dart';
 
 
 final _logger = Logger("main");
 final _logger = Logger("main");
 
 
@@ -36,7 +34,6 @@ void _main() async {
   await PhotoSyncManager.instance.init();
   await PhotoSyncManager.instance.init();
   await MemoriesService.instance.init();
   await MemoriesService.instance.init();
   await FavoriteFilesRepository.instance.init();
   await FavoriteFilesRepository.instance.init();
-  await initDeepLinks();
   _sync();
   _sync();
 
 
   final SentryClient sentry = new SentryClient(dsn: SENTRY_DSN);
   final SentryClient sentry = new SentryClient(dsn: SENTRY_DSN);
@@ -76,31 +73,6 @@ void _sendErrorToSentry(SentryClient sentry, Object error, StackTrace stack) {
   }
   }
 }
 }
 
 
-Future<void> initDeepLinks() async {
-  // Platform messages may fail, so we use a try/catch PlatformException.
-  try {
-    String initialLink = await getInitialLink();
-    // Parse the link and warn the user, if it is not correct,
-    // but keep in mind it could be `null`.
-    if (initialLink != null) {
-      _logger.info("Initial link received: " + initialLink);
-    } else {
-      _logger.info("No initial link received.");
-    }
-  } on PlatformException {
-    // Handle exception by warning the user their action did not succeed
-    // return?
-    _logger.severe("PlatformException thrown while getting initial link");
-  }
-
-  // Attach a listener to the stream
-  getLinksStream().listen((String link) {
-    _logger.info("Link received: " + link);
-  }, onError: (err) {
-    _logger.severe(err);
-  });
-}
-
 class MyApp extends StatelessWidget with WidgetsBindingObserver {
 class MyApp extends StatelessWidget with WidgetsBindingObserver {
   final _title = 'ente';
   final _title = 'ente';
   @override
   @override
@@ -109,8 +81,11 @@ class MyApp extends StatelessWidget with WidgetsBindingObserver {
 
 
     return MaterialApp(
     return MaterialApp(
       title: _title,
       title: _title,
-      theme: ThemeData.dark()
-          .copyWith(hintColor: Colors.grey, accentColor: Colors.pink[400]),
+      theme: ThemeData.dark().copyWith(
+        hintColor: Colors.grey,
+        accentColor: Colors.pink[400],
+        buttonColor: Colors.pink,
+      ),
       home: HomeWidget(_title),
       home: HomeWidget(_title),
     );
     );
   }
   }

+ 71 - 0
lib/ui/email_entry_page.dart

@@ -0,0 +1,71 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/user_authenticator.dart';
+
+class EmailEntryPage extends StatefulWidget {
+  EmailEntryPage({Key key}) : super(key: key);
+
+  @override
+  _EmailEntryPageState createState() => _EmailEntryPageState();
+}
+
+class _EmailEntryPageState extends State<EmailEntryPage> {
+  TextEditingController _emailController;
+
+  @override
+  void initState() {
+    _emailController =
+        TextEditingController(text: Configuration.instance.getEmail());
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text("Preserve Memories"),
+      ),
+      body: _getBody(),
+    );
+  }
+
+  Widget _getBody() {
+    return SingleChildScrollView(
+      child: Container(
+        padding: EdgeInsets.all(8),
+        child: Column(
+          children: [
+            TextFormField(
+              decoration: InputDecoration(
+                hintText: 'email@domain.com',
+                contentPadding: EdgeInsets.all(20),
+              ),
+              controller: _emailController,
+              autofocus: true,
+              autocorrect: false,
+              keyboardType: TextInputType.emailAddress,
+            ),
+            Padding(padding: EdgeInsets.all(8)),
+            SizedBox(
+                width: double.infinity,
+                child: RaisedButton(
+                  onPressed: () {
+                    final email = _emailController.text;
+                    Configuration.instance.setEmail(email);
+                    UserAuthenticator.instance.getOtt(context, email);
+                  },
+                  padding: const EdgeInsets.fromLTRB(8, 12, 8, 12),
+                  child: Text("Sign In"),
+                  color: Theme.of(context).buttonColor,
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(18.0),
+                  ),
+                )),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 38 - 32
lib/ui/gallery_app_bar_widget.dart

@@ -3,10 +3,13 @@ import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
-import 'package:photos/events/remote_sync_event.dart';
+import 'package:photos/events/user_authenticated_event.dart';
 import 'package:photos/file_repository.dart';
 import 'package:photos/file_repository.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/models/selected_files.dart';
-import 'package:photos/ui/setup_page.dart';
+import 'package:photos/ui/email_entry_page.dart';
+import 'package:photos/ui/ott_verification_page.dart';
+import 'package:photos/ui/passphrase_entry_page.dart';
+import 'package:photos/ui/passphrase_reentry_page.dart';
 import 'package:photos/ui/share_folder_widget.dart';
 import 'package:photos/ui/share_folder_widget.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/file_util.dart';
 import 'package:photos/utils/file_util.dart';
@@ -41,22 +44,25 @@ class GalleryAppBarWidget extends StatefulWidget
 }
 }
 
 
 class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
 class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
-  bool _hasSyncErrors = false;
-  StreamSubscription<RemoteSyncEvent> _subscription;
-
+  StreamSubscription _userAuthEventSubscription;
   @override
   @override
   void initState() {
   void initState() {
-    _subscription = Bus.instance.on<RemoteSyncEvent>().listen((event) {
-      setState(() {
-        _hasSyncErrors = !event.success;
-      });
-    });
     widget.selectedFiles.addListener(() {
     widget.selectedFiles.addListener(() {
       setState(() {});
       setState(() {});
     });
     });
+    _userAuthEventSubscription =
+        Bus.instance.on<UserAuthenticatedEvent>().listen((event) {
+      setState(() {});
+    });
     super.initState();
     super.initState();
   }
   }
 
 
+  @override
+  void dispose() {
+    _userAuthEventSubscription.cancel();
+    super.dispose();
+  }
+
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     if (widget.selectedFiles.files.isEmpty) {
     if (widget.selectedFiles.files.isEmpty) {
@@ -80,13 +86,30 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
 
 
   List<Widget> _getDefaultActions(BuildContext context) {
   List<Widget> _getDefaultActions(BuildContext context) {
     List<Widget> actions = List<Widget>();
     List<Widget> actions = List<Widget>();
-    if (_hasSyncErrors || !Configuration.instance.hasConfiguredAccount()) {
+    if (!Configuration.instance.hasConfiguredAccount()) {
       actions.add(IconButton(
       actions.add(IconButton(
-        icon: Icon(Configuration.instance.hasConfiguredAccount()
-            ? Icons.sync_problem
-            : Icons.sync_disabled),
+        icon: Icon(Icons.sync_disabled),
         onPressed: () {
         onPressed: () {
-          _openSyncConfiguration(context);
+          var page;
+          if (Configuration.instance.getToken() == null) {
+            page = EmailEntryPage();
+          } else {
+            // No key
+            if (Configuration.instance.getEncryptedKey() != null) {
+              // Yet to decrypt the key
+              page = PassphraseReentryPage();
+            } else {
+              // Never had a key
+              page = PassphraseEntryPage();
+            }
+          }
+          Navigator.of(context).push(
+            MaterialPageRoute(
+              builder: (BuildContext context) {
+                return page;
+              },
+            ),
+          );
         },
         },
       ));
       ));
     } else if (widget.type == GalleryAppBarType.local_folder &&
     } else if (widget.type == GalleryAppBarType.local_folder &&
@@ -178,21 +201,4 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
   void _clearSelectedFiles() {
   void _clearSelectedFiles() {
     widget.selectedFiles.clearAll();
     widget.selectedFiles.clearAll();
   }
   }
-
-  void _openSyncConfiguration(BuildContext context) {
-    final page = SetupPage();
-    Navigator.of(context).push(
-      MaterialPageRoute(
-        settings: RouteSettings(name: "/setup"),
-        builder: (BuildContext context) {
-          return page;
-        },
-      ),
-    );
-  }
-
-  void dispose() {
-    _subscription.cancel();
-    super.dispose();
-  }
 }
 }

+ 43 - 0
lib/ui/home_widget.dart

@@ -2,7 +2,9 @@ import 'dart:async';
 
 
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter/widgets.dart';
+import 'package:photos/core/configuration.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/models/filters/important_items_filter.dart';
 import 'package:photos/models/filters/important_items_filter.dart';
@@ -18,9 +20,11 @@ import 'package:photos/ui/loading_widget.dart';
 import 'package:photos/ui/memories_widget.dart';
 import 'package:photos/ui/memories_widget.dart';
 import 'package:photos/ui/remote_folder_gallery_widget.dart';
 import 'package:photos/ui/remote_folder_gallery_widget.dart';
 import 'package:photos/ui/search_page.dart';
 import 'package:photos/ui/search_page.dart';
+import 'package:photos/user_authenticator.dart';
 import 'package:photos/utils/logging_util.dart';
 import 'package:photos/utils/logging_util.dart';
 import 'package:shake/shake.dart';
 import 'package:shake/shake.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
+import 'package:uni_links/uni_links.dart';
 
 
 class HomeWidget extends StatefulWidget {
 class HomeWidget extends StatefulWidget {
   final String title;
   final String title;
@@ -56,6 +60,7 @@ class _HomeWidgetState extends State<HomeWidget> {
         Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
         Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
       setState(() {});
       setState(() {});
     });
     });
+    _initDeepLinks();
     super.initState();
     super.initState();
   }
   }
 
 
@@ -100,6 +105,44 @@ class _HomeWidgetState extends State<HomeWidget> {
     );
     );
   }
   }
 
 
+  Future<bool> _initDeepLinks() async {
+    // Platform messages may fail, so we use a try/catch PlatformException.
+    try {
+      String initialLink = await getInitialLink();
+      // Parse the link and warn the user, if it is not correct,
+      // but keep in mind it could be `null`.
+      if (initialLink != null) {
+        _logger.info("Initial link received: " + initialLink);
+        _getCredentials(context, initialLink);
+        return true;
+      } else {
+        _logger.info("No initial link received.");
+      }
+    } on PlatformException {
+      // Handle exception by warning the user their action did not succeed
+      // return?
+      _logger.severe("PlatformException thrown while getting initial link");
+    }
+
+    // Attach a listener to the stream
+    getLinksStream().listen((String link) {
+      _logger.info("Link received: " + link);
+      _getCredentials(context, link);
+    }, onError: (err) {
+      _logger.severe(err);
+    });
+    return false;
+  }
+
+  void _getCredentials(BuildContext context, String link) {
+    if (Configuration.instance.hasConfiguredAccount()) {
+      return;
+    }
+    final ott = Uri.parse(link).queryParameters["ott"];
+    _logger.info("Ott: " + ott);
+    UserAuthenticator.instance.getCredentials(context, ott);
+  }
+
   Widget _getMainGalleryWidget() {
   Widget _getMainGalleryWidget() {
     return FutureBuilder(
     return FutureBuilder(
       future: FileRepository.instance.loadFiles().then((files) {
       future: FileRepository.instance.loadFiles().then((files) {

+ 123 - 0
lib/ui/ott_verification_page.dart

@@ -0,0 +1,123 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/ui/email_entry_page.dart';
+import 'package:photos/user_authenticator.dart';
+
+class OTTVerificationPage extends StatefulWidget {
+  OTTVerificationPage({Key key}) : super(key: key);
+
+  @override
+  _OTTVerificationPageState createState() => _OTTVerificationPageState();
+}
+
+class _OTTVerificationPageState extends State<OTTVerificationPage> {
+  final _verificationCodeController = TextEditingController();
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text("Verify Email"),
+      ),
+      body: _getBody(),
+    );
+  }
+
+  Widget _getBody() {
+    return SingleChildScrollView(
+      child: Container(
+        padding: EdgeInsets.fromLTRB(8, 40, 8, 8),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.center,
+          mainAxisSize: MainAxisSize.max,
+          mainAxisAlignment: MainAxisAlignment.end,
+          children: [
+            SvgPicture.asset(
+              "assets/email_sent.svg",
+              width: 256,
+              height: 256,
+            ),
+            Padding(padding: EdgeInsets.all(12)),
+            Text.rich(
+              TextSpan(
+                style: TextStyle(fontSize: 18),
+                children: <TextSpan>[
+                  TextSpan(text: "We've sent a mail to "),
+                  TextSpan(
+                      text: Configuration.instance.getEmail(),
+                      style: TextStyle(
+                        color: Theme.of(context).accentColor,
+                      )),
+                  TextSpan(text: "."),
+                ],
+              ),
+              textAlign: TextAlign.center,
+            ),
+            Padding(padding: EdgeInsets.all(12)),
+            Text(
+              "Please check your inbox (and spam) to complete verification.",
+              textAlign: TextAlign.center,
+            ),
+            Padding(padding: EdgeInsets.all(12)),
+            TextFormField(
+              decoration: InputDecoration(
+                hintText: 'Tap to enter verification code',
+                contentPadding: EdgeInsets.all(20),
+              ),
+              controller: _verificationCodeController,
+              autofocus: false,
+              autocorrect: false,
+              keyboardType: TextInputType.visiblePassword,
+              textAlign: TextAlign.center,
+              onChanged: (_) {
+                setState(() {});
+              },
+            ),
+            Padding(padding: EdgeInsets.all(8)),
+            SizedBox(
+                width: double.infinity,
+                child: RaisedButton(
+                  onPressed: _verificationCodeController.text == null ||
+                          _verificationCodeController.text.isEmpty
+                      ? null
+                      : () {
+                          UserAuthenticator.instance.getCredentials(
+                              context, _verificationCodeController.text);
+                        },
+                  padding: const EdgeInsets.fromLTRB(8, 12, 8, 12),
+                  child: Text(
+                    "Verify",
+                  ),
+                  color: Colors.pink,
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(18.0),
+                  ),
+                )),
+            Padding(padding: EdgeInsets.all(8)),
+            TextButton(
+                onPressed: () {
+                  Navigator.of(context).push(
+                    MaterialPageRoute(
+                      builder: (BuildContext context) {
+                        return EmailEntryPage();
+                      },
+                    ),
+                  );
+                },
+                child: Text(
+                  "Did not get email?",
+                  style: TextStyle(
+                    decoration: TextDecoration.underline,
+                    fontSize: 12,
+                  ),
+                )),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 160 - 0
lib/ui/passphrase_entry_page.dart

@@ -0,0 +1,160 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:photos/user_authenticator.dart';
+import 'package:photos/utils/dialog_util.dart';
+
+class PassphraseEntryPage extends StatefulWidget {
+  PassphraseEntryPage({Key key}) : super(key: key);
+
+  @override
+  _PassphraseEntryPageState createState() => _PassphraseEntryPageState();
+}
+
+class _PassphraseEntryPageState extends State<PassphraseEntryPage> {
+  final _passphraseController1 = TextEditingController(),
+      _passphraseController2 = TextEditingController();
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        leading: Icon(Icons.lock),
+        title: Text("Encryption Passphrase"),
+      ),
+      body: _getBody(),
+    );
+  }
+
+  Widget _getBody() {
+    return SingleChildScrollView(
+      child: Container(
+        padding: EdgeInsets.fromLTRB(16, 40, 16, 16),
+        child: Column(
+          children: [
+            SvgPicture.asset(
+              "assets/vault.svg",
+              width: 196,
+              height: 196,
+            ),
+            Padding(padding: EdgeInsets.all(12)),
+            Text(
+              "Please enter a passphrase that we can use to encrypt your data.",
+              textAlign: TextAlign.center,
+            ),
+            Padding(padding: EdgeInsets.all(4)),
+            Text.rich(
+              TextSpan(
+                children: <TextSpan>[
+                  TextSpan(
+                      text:
+                          "We don't store your passphrase, so if you forget, "),
+                  TextSpan(
+                      text: "we will not be able to help you",
+                      style: TextStyle(
+                        decoration: TextDecoration.underline,
+                        fontWeight: FontWeight.bold,
+                      )),
+                  TextSpan(text: " recover your data."),
+                ],
+              ),
+              textAlign: TextAlign.center,
+            ),
+            Padding(padding: EdgeInsets.all(12)),
+            TextFormField(
+              decoration: InputDecoration(
+                hintText: "something you'll never forget",
+                contentPadding: EdgeInsets.all(20),
+              ),
+              controller: _passphraseController1,
+              autofocus: false,
+              autocorrect: false,
+              keyboardType: TextInputType.visiblePassword,
+              onChanged: (_) {
+                setState(() {});
+              },
+            ),
+            Padding(padding: EdgeInsets.all(8)),
+            TextFormField(
+              decoration: InputDecoration(
+                hintText: "something you'll never ever forget",
+                contentPadding: EdgeInsets.all(20),
+              ),
+              controller: _passphraseController2,
+              autofocus: false,
+              autocorrect: false,
+              obscureText: true,
+              keyboardType: TextInputType.visiblePassword,
+              onChanged: (_) {
+                setState(() {});
+              },
+            ),
+            Padding(padding: EdgeInsets.all(8)),
+            SizedBox(
+                width: double.infinity,
+                child: RaisedButton(
+                  onPressed: _passphraseController1.text.isNotEmpty &&
+                          _passphraseController2.text.isNotEmpty
+                      ? () {
+                          if (_passphraseController1.text !=
+                              _passphraseController2.text) {
+                            showErrorDialog(context, "Uhm...",
+                                "The passphrases you entered don't match.");
+                          } else {
+                            _showPassphraseConfirmationDialog();
+                          }
+                        }
+                      : null,
+                  padding: const EdgeInsets.fromLTRB(8, 12, 8, 12),
+                  child: Text("Set Passphrase"),
+                  color: Theme.of(context).buttonColor,
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(18.0),
+                  ),
+                )),
+          ],
+        ),
+      ),
+    );
+  }
+
+  void _showPassphraseConfirmationDialog() {
+    AlertDialog alert = AlertDialog(
+      title: Text("Confirmation"),
+      content: SingleChildScrollView(
+        child: Column(children: [
+          Text("The passphrase you are promising to never forget is"),
+          Padding(padding: EdgeInsets.all(8)),
+          Text(_passphraseController1.text,
+              style: TextStyle(
+                fontWeight: FontWeight.bold,
+                fontSize: 28,
+              )),
+        ]),
+      ),
+      actions: [
+        FlatButton(
+          child: Text("Change"),
+          onPressed: () {
+            Navigator.of(context).pop();
+          },
+        ),
+        FlatButton(
+          child: Text("Confirm"),
+          onPressed: () {
+            Navigator.of(context).pop();
+            UserAuthenticator.instance
+                .setPassphrase(context, _passphraseController1.text);
+          },
+        ),
+      ],
+    );
+
+    showDialog(
+      context: context,
+      builder: (BuildContext context) {
+        return alert;
+      },
+    );
+  }
+}

+ 94 - 0
lib/ui/passphrase_reentry_page.dart

@@ -0,0 +1,94 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/core/event_bus.dart';
+import 'package:photos/events/user_authenticated_event.dart';
+import 'package:photos/utils/dialog_util.dart';
+
+class PassphraseReentryPage extends StatefulWidget {
+  PassphraseReentryPage({Key key}) : super(key: key);
+
+  @override
+  _PassphraseReentryPageState createState() => _PassphraseReentryPageState();
+}
+
+class _PassphraseReentryPageState extends State<PassphraseReentryPage> {
+  final _passphraseController = TextEditingController();
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        leading: Icon(Icons.lock),
+        title: Text(
+          "Encryption Passphrase",
+        ),
+      ),
+      body: _getBody(),
+    );
+  }
+
+  Widget _getBody() {
+    return SingleChildScrollView(
+      child: Container(
+        padding: EdgeInsets.fromLTRB(16, 40, 16, 16),
+        child: Column(
+          children: [
+            SvgPicture.asset(
+              "assets/vault.svg",
+              width: 196,
+              height: 196,
+            ),
+            Padding(padding: EdgeInsets.all(20)),
+            Text(
+              "Please enter your passphrase.",
+              textAlign: TextAlign.center,
+              style: TextStyle(
+                fontSize: 16,
+              ),
+            ),
+            Padding(padding: EdgeInsets.all(12)),
+            TextFormField(
+              decoration: InputDecoration(
+                hintText: "that thing you promised to never forget",
+                contentPadding: EdgeInsets.all(20),
+              ),
+              controller: _passphraseController,
+              autofocus: false,
+              autocorrect: false,
+              keyboardType: TextInputType.visiblePassword,
+              onChanged: (_) {
+                setState(() {});
+              },
+            ),
+            Padding(padding: EdgeInsets.all(12)),
+            SizedBox(
+                width: double.infinity,
+                child: RaisedButton(
+                  onPressed: _passphraseController.text.isNotEmpty
+                      ? () async {
+                          final dialog =
+                              createProgressDialog(context, "Please wait...");
+                          await dialog.show();
+                          await Configuration.instance
+                              .decryptEncryptedKey(_passphraseController.text);
+                          await dialog.hide();
+                          Bus.instance.fire(UserAuthenticatedEvent());
+                          Navigator.of(context)
+                              .popUntil((route) => route.isFirst);
+                        }
+                      : null,
+                  padding: const EdgeInsets.fromLTRB(8, 12, 8, 12),
+                  child: Text("Set Passphrase"),
+                  color: Theme.of(context).buttonColor,
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(18.0),
+                  ),
+                )),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 110 - 5
lib/user_authenticator.dart

@@ -1,9 +1,16 @@
 import 'package:dio/dio.dart';
 import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
 
 
 import 'package:photos/events/user_authenticated_event.dart';
 import 'package:photos/events/user_authenticated_event.dart';
+import 'package:photos/ui/ott_verification_page.dart';
+import 'package:photos/ui/passphrase_entry_page.dart';
+import 'package:photos/ui/passphrase_reentry_page.dart';
+import 'package:photos/utils/dialog_util.dart';
+import 'package:photos/utils/toast_util.dart';
 
 
 class UserAuthenticator {
 class UserAuthenticator {
   final _dio = Dio();
   final _dio = Dio();
@@ -14,6 +21,105 @@ class UserAuthenticator {
   static final UserAuthenticator instance =
   static final UserAuthenticator instance =
       UserAuthenticator._privateConstructor();
       UserAuthenticator._privateConstructor();
 
 
+  Future<void> getOtt(BuildContext context, String email) async {
+    final dialog = createProgressDialog(context, "Please wait...");
+    await dialog.show();
+    await Dio().get(
+      Configuration.instance.getHttpEndpoint() + "/users/ott",
+      queryParameters: {
+        "email": email,
+      },
+    ).catchError((e) async {
+      _logger.severe(e);
+    }).then((response) async {
+      await dialog.hide();
+      if (response != null && response.statusCode == 200) {
+        Navigator.of(context).push(
+          MaterialPageRoute(
+            builder: (BuildContext context) {
+              return OTTVerificationPage();
+            },
+          ),
+        );
+      } else {
+        showGenericErrorDialog(context);
+      }
+    });
+  }
+
+  Future<void> getCredentials(BuildContext context, String ott) async {
+    final dialog = createProgressDialog(context, "Please wait...");
+    await dialog.show();
+    await Dio().get(
+      Configuration.instance.getHttpEndpoint() + "/users/credentials",
+      queryParameters: {
+        "email": Configuration.instance.getEmail(),
+        "ott": ott,
+      },
+    ).catchError((e) async {
+      _logger.severe(e);
+    }).then((response) async {
+      await dialog.hide();
+      if (response != null && response.statusCode == 200) {
+        _saveConfiguration(response);
+        showToast("Email verification successful!");
+        var page;
+        if (Configuration.instance.getEncryptedKey() != null) {
+          page = PassphraseReentryPage();
+        } else {
+          page = PassphraseEntryPage();
+        }
+        Navigator.of(context).pushAndRemoveUntil(
+          MaterialPageRoute(
+            builder: (BuildContext context) {
+              return page;
+            },
+          ),
+          (route) => route.isFirst,
+        );
+      } else {
+        showErrorDialog(
+            context, "Oops.", "Verification failed, please try again.");
+      }
+    });
+  }
+
+  Future<void> setPassphrase(BuildContext context, String passphrase) async {
+    final dialog = createProgressDialog(context, "Please wait...");
+    await dialog.show();
+    await Configuration.instance.generateAndSaveKey(passphrase);
+    await _dio
+        .put(
+      Configuration.instance.getHttpEndpoint() + "/users/encrypted-key",
+      data: {
+        "encryptedKey": Configuration.instance.getEncryptedKey(),
+      },
+      options: Options(
+        headers: {
+          "X-Auth-Token": Configuration.instance.getToken(),
+        },
+      ),
+    )
+        .catchError((e) async {
+      await dialog.hide();
+      Configuration.instance.setKey(null);
+      Configuration.instance.setEncryptedKey(null);
+      _logger.severe(e);
+      showGenericErrorDialog(context);
+    }).then((response) async {
+      await dialog.hide();
+      if (response != null && response.statusCode == 200) {
+        Bus.instance.fire(UserAuthenticatedEvent());
+        Navigator.of(context).popUntil((route) => route.isFirst);
+      } else {
+        Configuration.instance.setKey(null);
+        Configuration.instance.setEncryptedKey(null);
+        showGenericErrorDialog(context);
+      }
+    });
+  }
+
+  @deprecated
   Future<bool> login(String username, String password) {
   Future<bool> login(String username, String password) {
     return _dio.post(
     return _dio.post(
         Configuration.instance.getHttpEndpoint() + "/users/authenticate",
         Configuration.instance.getHttpEndpoint() + "/users/authenticate",
@@ -22,7 +128,7 @@ class UserAuthenticator {
           "password": password,
           "password": password,
         }).then((response) {
         }).then((response) {
       if (response.statusCode == 200 && response.data != null) {
       if (response.statusCode == 200 && response.data != null) {
-        _saveConfiguration(username, password, response);
+        _saveConfiguration(response);
         Bus.instance.fire(UserAuthenticatedEvent());
         Bus.instance.fire(UserAuthenticatedEvent());
         return true;
         return true;
       } else {
       } else {
@@ -34,6 +140,7 @@ class UserAuthenticator {
     });
     });
   }
   }
 
 
+  @deprecated
   Future<bool> create(String username, String password) {
   Future<bool> create(String username, String password) {
     return _dio
     return _dio
         .post(Configuration.instance.getHttpEndpoint() + "/users", data: {
         .post(Configuration.instance.getHttpEndpoint() + "/users", data: {
@@ -41,7 +148,7 @@ class UserAuthenticator {
       "password": password,
       "password": password,
     }).then((response) {
     }).then((response) {
       if (response.statusCode == 200 && response.data != null) {
       if (response.statusCode == 200 && response.data != null) {
-        _saveConfiguration(username, password, response);
+        _saveConfiguration(response);
         return true;
         return true;
       } else {
       } else {
         if (response.data != null && response.data["message"] != null) {
         if (response.data != null && response.data["message"] != null) {
@@ -68,9 +175,7 @@ class UserAuthenticator {
     );
     );
   }
   }
 
 
-  void _saveConfiguration(String username, String password, Response response) {
-    Configuration.instance.setUsername(username);
-    Configuration.instance.setPassword(password);
+  void _saveConfiguration(Response response) {
     Configuration.instance.setUserID(response.data["id"]);
     Configuration.instance.setUserID(response.data["id"]);
     Configuration.instance.setToken(response.data["token"]);
     Configuration.instance.setToken(response.data["token"]);
     final String encryptedKey = response.data["encryptedKey"];
     final String encryptedKey = response.data["encryptedKey"];

+ 27 - 0
lib/utils/dialog_util.dart

@@ -20,3 +20,30 @@ ProgressDialog createProgressDialog(BuildContext context, String message) {
   );
   );
   return dialog;
   return dialog;
 }
 }
+
+void showErrorDialog(BuildContext context, String title, String content) {
+  AlertDialog alert = AlertDialog(
+    title: Text(title),
+    content: Text(content),
+    actions: [
+      FlatButton(
+        child: Text("OK"),
+        onPressed: () {
+          Navigator.of(context).pop();
+        },
+      ),
+    ],
+  );
+
+  showDialog(
+    context: context,
+    builder: (BuildContext context) {
+      return alert;
+    },
+  );
+}
+
+void showGenericErrorDialog(BuildContext context) {
+  showErrorDialog(
+      context, "Oops.", "Sorry, something went wrong. Please try again.");
+}

+ 21 - 0
pubspec.lock

@@ -272,6 +272,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "3.3.3"
     version: "3.3.3"
+  flutter_svg:
+    dependency: "direct main"
+    description:
+      name: flutter_svg
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.18.1"
   flutter_test:
   flutter_test:
     dependency: "direct dev"
     dependency: "direct dev"
     description: flutter
     description: flutter
@@ -408,6 +415,20 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "1.7.0"
     version: "1.7.0"
+  path_drawing:
+    dependency: transitive
+    description:
+      name: path_drawing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.4.1+1"
+  path_parsing:
+    dependency: transitive
+    description:
+      name: path_parsing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.4"
   path_provider:
   path_provider:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:

+ 3 - 0
pubspec.yaml

@@ -62,6 +62,7 @@ dependencies:
   computer: ^1.0.2
   computer: ^1.0.2
   flutter_secure_storage: ^3.3.3
   flutter_secure_storage: ^3.3.3
   uni_links: ^0.4.0
   uni_links: ^0.4.0
+  flutter_svg: ^0.18.1
 
 
 dev_dependencies:
 dev_dependencies:
   flutter_test:
   flutter_test:
@@ -78,6 +79,8 @@ flutter_icons:
 
 
 # The following section is specific to Flutter.
 # The following section is specific to Flutter.
 flutter:
 flutter:
+  assets:
+    - assets/
 
 
   # The following line ensures that the Material Icons font is
   # The following line ensures that the Material Icons font is
   # included with your application, so that you can use the icons in
   # included with your application, so that you can use the icons in

Some files were not shown because too many files changed in this diff