From df1df10c99514b57351a5efda8b609a175d54a3d Mon Sep 17 00:00:00 2001 From: Abhishek Shroff Date: Sat, 10 Aug 2024 22:23:12 +0530 Subject: [PATCH] [client] Basic login page --- client/lib/login_page.dart | 167 +++++++++++++++++++++++++++++++++++ client/lib/main.dart | 112 ++--------------------- client/lib/util/dialogs.dart | 75 ++++++++++++++++ client/pubspec.lock | 91 +++++++++++++++++-- client/pubspec.yaml | 83 ++--------------- 5 files changed, 342 insertions(+), 186 deletions(-) create mode 100644 client/lib/login_page.dart create mode 100644 client/lib/util/dialogs.dart diff --git a/client/lib/login_page.dart b/client/lib/login_page.dart new file mode 100644 index 00000000..d67cfa6d --- /dev/null +++ b/client/lib/login_page.dart @@ -0,0 +1,167 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; +import 'package:phylum/util/dialogs.dart'; +import 'package:uri/uri.dart'; + +typedef AuthRequestBuilder = Future Function(BuildContext, UriBuilder); + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final Client _client = Client(); + String _email = ''; + String _password = ''; + Uri? apiBase; + String? urlError; + String? emailError; + + bool get apiBaseValid => kIsWeb || (apiBase?.hasScheme == true && apiBase?.hasAuthority == true); + + @override + void initState() { + super.initState(); + } + + void _performLogin(BuildContext context, String email, String password) async { + final apiBaseUrl = apiBase; + if (!apiBaseValid) return; + + showProgressDialog( + context, + barrierDismissible: false, + title: 'Logging in...', + ); + final baseUri = apiBaseUrl == null ? UriBuilder() : UriBuilder.fromUri(apiBaseUrl); + final path = baseUri.path.split('/').where((s) => s.isNotEmpty).join('/'); + baseUri.path = '$path/api/v1/auth/login'; + baseUri.queryParameters['username'] = email; + baseUri.queryParameters['password'] = password; + final request = Request('post', baseUri.build()); + + try { + final response = await _client.send(request); + final responseString = await response.stream.bytesToString(); + final json = jsonDecode(responseString) as Map; + + if (context.mounted) { + Navigator.of(context).pop(); + if (response.statusCode != 200) { + showAlertDialog( + context, + barrierDismissible: false, + title: 'Login Error', + message: json['msg'], + ); + } else { + showAlertDialog( + context, + barrierDismissible: false, + title: 'Login Successful', + message: responseString, + ); + } + } + } on SocketException { + if (context.mounted) { + Navigator.of(context).pop(); + showAlertDialog( + context, + barrierDismissible: false, + title: 'Login Error', + message: 'Unable to reach server', + ); + } + } catch (e) { + if (context.mounted) { + Navigator.of(context).pop(); + showAlertDialog( + context, + barrierDismissible: false, + title: 'Login Error', + message: e.toString(), + ); + } + rethrow; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 360), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!kIsWeb) + TextField( + decoration: InputDecoration( + label: const Text('Server URL'), + hintText: 'http://localhost:1234/', + errorText: urlError, + ), + keyboardType: TextInputType.url, + onChanged: (value) { + setState(() { + apiBase = Uri.tryParse(value); + urlError = apiBase == null ? 'Invalid URL' : null; + }); + }, + ), + TextField( + decoration: const InputDecoration( + label: Text('Username'), + ), + keyboardType: TextInputType.emailAddress, + onChanged: (value) { + setState(() { + _email = value.trim(); + }); + }, + ), + TextField( + decoration: const InputDecoration( + label: Text('Password'), + ), + obscureText: true, + keyboardType: TextInputType.visiblePassword, + onChanged: (value) { + setState(() { + _password = value.trim(); + }); + }, + onSubmitted: (value) { + _performLogin(context, _email, _password); + }, + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: ElevatedButton( + onPressed: apiBaseValid && _email.isNotEmpty + ? () async { + _performLogin(context, _email, _password); + } + : null, + child: const Text('Login'), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/client/lib/main.dart b/client/lib/main.dart index 8e940891..c8272fb3 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:phylum/login_page.dart'; void main() { runApp(const MyApp()); @@ -7,119 +8,16 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'Phylum', + debugShowCheckedModeBanner: false, theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange), useMaterial3: true, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + home: const LoginPage(), ); } } diff --git a/client/lib/util/dialogs.dart b/client/lib/util/dialogs.dart new file mode 100644 index 00000000..13a65b60 --- /dev/null +++ b/client/lib/util/dialogs.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; + +Future showProgressDialog( + BuildContext context, { + bool barrierDismissible = false, + String? title, + String? message, +}) { + return showDialog( + context: context, + barrierDismissible: barrierDismissible, + builder: (context) => AlertDialog( + title: (title == null) ? null : Text(title), + content: SizedBox( + width: 360, + child: Row( + children: [ + const Padding( + padding: EdgeInsets.only(right: 16), + child: CircularProgressIndicator(), + ), + if (message != null) + Text( + message, + softWrap: true, + overflow: TextOverflow.fade, + ), + ], + ), + ), + ), + ); +} + +Future showAlertDialog( + BuildContext context, { + bool barrierDismissible = false, + String? title, + String? message, + String? negativeText, + String positiveText = 'OK', +}) { + return showDialog( + context: context, + barrierDismissible: barrierDismissible, + builder: (context) => AlertDialog( + title: (title == null) ? null : Text(title), + scrollable: (message?.length ?? 0) > 300, + content: (message == null) + ? null + : ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 360), + child: Text( + message, + softWrap: true, + ), + ), + actions: [ + if (negativeText != null) + TextButton( + child: Text(negativeText), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + ElevatedButton( + child: Text(positiveText), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ], + ), + ); +} diff --git a/client/pubspec.lock b/client/pubspec.lock index af2ac16b..fe335c60 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -41,14 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" - cupertino_icons: - dependency: "direct main" + crypto: + dependency: transitive description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "3.0.3" fake_async: dependency: transitive description: @@ -75,6 +75,30 @@ packages: description: flutter source: sdk version: "0.0.0" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + http: + dependency: "direct main" + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" leak_tracker: dependency: transitive description: @@ -107,6 +131,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + logger: + dependency: transitive + description: + name: logger + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" + url: "https://pub.dev" + source: hosted + version: "2.4.0" matcher: dependency: transitive description: @@ -131,6 +163,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + offtheline: + dependency: "direct main" + description: + path: "." + ref: "4d7cc0438b25e5c97a7b8bd6b77f6e34121c43aa" + resolved-ref: "4d7cc0438b25e5c97a7b8bd6b77f6e34121c43aa" + url: "https://github.com/shroff/offtheline.git" + source: git + version: "0.12.0" path: dependency: transitive description: @@ -139,6 +180,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" sky_engine: dependency: transitive description: flutter @@ -160,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: @@ -192,6 +249,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uri: + dependency: "direct main" + description: + name: uri + sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" + url: "https://pub.dev" + source: hosted + version: "1.0.0" vector_math: dependency: transitive description: @@ -208,6 +281,14 @@ packages: url: "https://pub.dev" source: hosted version: "14.2.4" + web: + dependency: transitive + description: + name: web + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + url: "https://pub.dev" + source: hosted + version: "1.0.0" sdks: dart: ">=3.5.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 260f800c..47773db0 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -1,90 +1,25 @@ name: phylum description: "Self-hosted file storage" -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +publish_to: 'none' +version: 0.0.1+1 environment: sdk: ^3.5.0 -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter - - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 + http: + offtheline: + git: + url: https://github.com/shroff/offtheline.git + ref: 4d7cc0438b25e5c97a7b8bd6b77f6e34121c43aa + uri: dev_dependencies: flutter_test: sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^4.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package + uses-material-design: true \ No newline at end of file