commit
93b1fb446d
15 changed files with 706 additions and 85 deletions
1
assets/email_sent.svg
Normal file
1
assets/email_sent.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 18 KiB |
1
assets/vault.svg
Normal file
1
assets/vault.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.7 KiB |
|
@ -12,10 +12,9 @@ class Configuration {
|
|||
static final Configuration instance = Configuration._privateConstructor();
|
||||
|
||||
static const endpointKey = "endpoint";
|
||||
static const tokenKey = "token";
|
||||
static const usernameKey = "username";
|
||||
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 keyKey = "key";
|
||||
static const keyEncryptedKey = "encrypted_key";
|
||||
|
@ -50,7 +49,7 @@ class Configuration {
|
|||
}
|
||||
|
||||
String getEndpoint() {
|
||||
return _preferences.getString(endpointKey);
|
||||
return "192.168.0.106";
|
||||
}
|
||||
|
||||
String getHttpEndpoint() {
|
||||
|
@ -72,12 +71,12 @@ class Configuration {
|
|||
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() {
|
||||
|
@ -88,14 +87,6 @@ class Configuration {
|
|||
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 {
|
||||
await _preferences.setBool(hasOptedForE2EKey, hasOptedForE2E);
|
||||
}
|
||||
|
@ -143,6 +134,6 @@ class Configuration {
|
|||
}
|
||||
|
||||
bool hasConfiguredAccount() {
|
||||
return getEndpoint() != null && getToken() != null;
|
||||
return getToken() != null && getKey() != null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -136,7 +136,7 @@ class FolderSharingService {
|
|||
try {
|
||||
return Folder(
|
||||
null,
|
||||
Configuration.instance.getUsername() + "s " + deviceFolder,
|
||||
Configuration.instance.getEmail() + "s " + deviceFolder,
|
||||
Configuration.instance.getUserID(),
|
||||
deviceFolder,
|
||||
Set<int>(),
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||
|
||||
import 'package:computer/computer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:photos/core/constants.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:super_logging/super_logging.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
|
||||
final _logger = Logger("main");
|
||||
|
||||
|
@ -36,7 +34,6 @@ void _main() async {
|
|||
await PhotoSyncManager.instance.init();
|
||||
await MemoriesService.instance.init();
|
||||
await FavoriteFilesRepository.instance.init();
|
||||
await initDeepLinks();
|
||||
_sync();
|
||||
|
||||
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 {
|
||||
final _title = 'ente';
|
||||
@override
|
||||
|
@ -109,8 +81,11 @@ class MyApp extends StatelessWidget with WidgetsBindingObserver {
|
|||
|
||||
return MaterialApp(
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
|
71
lib/ui/email_entry_page.dart
Normal file
71
lib/ui/email_entry_page.dart
Normal file
|
@ -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),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,10 +3,13 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/core/configuration.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/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/utils/dialog_util.dart';
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
|
@ -41,22 +44,25 @@ class GalleryAppBarWidget extends StatefulWidget
|
|||
}
|
||||
|
||||
class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
||||
bool _hasSyncErrors = false;
|
||||
StreamSubscription<RemoteSyncEvent> _subscription;
|
||||
|
||||
StreamSubscription _userAuthEventSubscription;
|
||||
@override
|
||||
void initState() {
|
||||
_subscription = Bus.instance.on<RemoteSyncEvent>().listen((event) {
|
||||
setState(() {
|
||||
_hasSyncErrors = !event.success;
|
||||
});
|
||||
});
|
||||
widget.selectedFiles.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
_userAuthEventSubscription =
|
||||
Bus.instance.on<UserAuthenticatedEvent>().listen((event) {
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_userAuthEventSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.selectedFiles.files.isEmpty) {
|
||||
|
@ -80,13 +86,30 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
|
||||
List<Widget> _getDefaultActions(BuildContext context) {
|
||||
List<Widget> actions = List<Widget>();
|
||||
if (_hasSyncErrors || !Configuration.instance.hasConfiguredAccount()) {
|
||||
if (!Configuration.instance.hasConfiguredAccount()) {
|
||||
actions.add(IconButton(
|
||||
icon: Icon(Configuration.instance.hasConfiguredAccount()
|
||||
? Icons.sync_problem
|
||||
: Icons.sync_disabled),
|
||||
icon: Icon(Icons.sync_disabled),
|
||||
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 &&
|
||||
|
@ -178,21 +201,4 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
void _clearSelectedFiles() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/events/local_photos_updated_event.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/remote_folder_gallery_widget.dart';
|
||||
import 'package:photos/ui/search_page.dart';
|
||||
import 'package:photos/user_authenticator.dart';
|
||||
import 'package:photos/utils/logging_util.dart';
|
||||
import 'package:shake/shake.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
|
||||
class HomeWidget extends StatefulWidget {
|
||||
final String title;
|
||||
|
@ -56,6 +60,7 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
|
||||
setState(() {});
|
||||
});
|
||||
_initDeepLinks();
|
||||
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() {
|
||||
return FutureBuilder(
|
||||
future: FileRepository.instance.loadFiles().then((files) {
|
||||
|
|
123
lib/ui/ott_verification_page.dart
Normal file
123
lib/ui/ott_verification_page.dart
Normal file
|
@ -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
lib/ui/passphrase_entry_page.dart
Normal file
160
lib/ui/passphrase_entry_page.dart
Normal file
|
@ -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
lib/ui/passphrase_reentry_page.dart
Normal file
94
lib/ui/passphrase_reentry_page.dart
Normal file
|
@ -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),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,16 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.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/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 {
|
||||
final _dio = Dio();
|
||||
|
@ -14,6 +21,105 @@ class UserAuthenticator {
|
|||
static final UserAuthenticator instance =
|
||||
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) {
|
||||
return _dio.post(
|
||||
Configuration.instance.getHttpEndpoint() + "/users/authenticate",
|
||||
|
@ -22,7 +128,7 @@ class UserAuthenticator {
|
|||
"password": password,
|
||||
}).then((response) {
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
_saveConfiguration(username, password, response);
|
||||
_saveConfiguration(response);
|
||||
Bus.instance.fire(UserAuthenticatedEvent());
|
||||
return true;
|
||||
} else {
|
||||
|
@ -34,6 +140,7 @@ class UserAuthenticator {
|
|||
});
|
||||
}
|
||||
|
||||
@deprecated
|
||||
Future<bool> create(String username, String password) {
|
||||
return _dio
|
||||
.post(Configuration.instance.getHttpEndpoint() + "/users", data: {
|
||||
|
@ -41,7 +148,7 @@ class UserAuthenticator {
|
|||
"password": password,
|
||||
}).then((response) {
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
_saveConfiguration(username, password, response);
|
||||
_saveConfiguration(response);
|
||||
return true;
|
||||
} else {
|
||||
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.setToken(response.data["token"]);
|
||||
final String encryptedKey = response.data["encryptedKey"];
|
||||
|
|
|
@ -20,3 +20,30 @@ ProgressDialog createProgressDialog(BuildContext context, String message) {
|
|||
);
|
||||
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
pubspec.lock
21
pubspec.lock
|
@ -272,6 +272,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -408,6 +415,20 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -62,6 +62,7 @@ dependencies:
|
|||
computer: ^1.0.2
|
||||
flutter_secure_storage: ^3.3.3
|
||||
uni_links: ^0.4.0
|
||||
flutter_svg: ^0.18.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -78,6 +79,8 @@ flutter_icons:
|
|||
|
||||
# The following section is specific to Flutter.
|
||||
flutter:
|
||||
assets:
|
||||
- assets/
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
|
|
Loading…
Add table
Reference in a new issue