diff --git a/lib/auth.dart b/lib/auth.dart index 73aebf9..f2fc2c6 100644 --- a/lib/auth.dart +++ b/lib/auth.dart @@ -1,121 +1,106 @@ +import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:webview_flutter/webview_flutter.dart'; +import 'package:goatagent/screens/auth.dart'; import 'package:oauth2/oauth2.dart' as oauth2; -class LoginPage extends StatelessWidget { - const LoginPage({super.key}); +class AuthGuard { + static final AuthGuard _singleton = AuthGuard._internal(); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Goatworks Login'), - ), - body: Center( - child: ElevatedButton( - onPressed: () async { - await _login(context); - }, - child: const Text('Login with Goatpass'), - ), - ), - ); + final authorizationEndpoint = + Uri.parse('https://id.smartsheep.studio/auth/o/connect'); + final tokenEndpoint = + Uri.parse('https://id.smartsheep.studio/api/auth/token'); + final userinfoEndpoint = + Uri.parse('https://id.smartsheep.studio/api/users/me'); + final redirectUrl = Uri.parse('goatagent://auth'); + + static const clientId = "goatagent"; + static const clientSecret = "_F4%q2Eea3"; + + static const storage = FlutterSecureStorage(); + static const storageKey = "identity"; + static const profileKey = "profiles"; + + factory AuthGuard() { + return _singleton; } - Future _login(BuildContext context) async { - final authorizationEndpoint = - Uri.parse('https://id.smartsheep.studio/auth/o/connect'); - final tokenEndpoint = - Uri.parse('https://id.smartsheep.studio/api/auth/token'); - final userinfoEndpoint = - Uri.parse('https://id.smartsheep.studio/api/users/me'); - final redirectUrl = Uri.parse('goatagent://auth'); + oauth2.Client? client; - const clientId = "goatagent"; - const clientSecret = "_F4%q2Eea3"; + Future pickClient() async { + if (await storage.containsKey(key: storageKey)) { + var credentials = + oauth2.Credentials.fromJson((await storage.read(key: storageKey))!); + client = oauth2.Client(credentials, + identifier: clientId, secret: clientSecret); + print(await storage.readAll()); + return true; + } else { + return false; + } + } - const storage = FlutterSecureStorage(); - const storageKey = "identity"; - - Future createClient() async { - // If logged in - if (await storage.containsKey(key: storageKey)) { - var credentials = oauth2.Credentials.fromJson( - storage.read(key: storageKey) as String); - return oauth2.Client(credentials, - identifier: clientId, secret: clientSecret); - } - - var grant = oauth2.AuthorizationCodeGrant( - clientId, - authorizationEndpoint, - tokenEndpoint, - secret: clientSecret, - basicAuth: false, - ); - - var authorizationUrl = grant.getAuthorizationUrl(redirectUrl); - - if (Platform.isAndroid || Platform.isIOS) { - // Let Goatpass know it is embed in an app - authorizationUrl.replace( - queryParameters: {"embedded": "yes"} - ..addAll(authorizationUrl.queryParameters)); - - // Use WebView to get authorization url - var responseUrl = await Navigator.of(context).push(MaterialPageRoute( - builder: (context) => AuthorizationPage(authorizationUrl), - )); - - var responseUri = Uri.parse(responseUrl); - return await grant - .handleAuthorizationResponse(responseUri.queryParameters); - } else { - // TODO Use system browser to get url - throw UnimplementedError("unsupported platform"); - } + Future createClient(BuildContext context) async { + // If logged in + if (await pickClient()) { + return client!; } - try { - var client = await createClient(); - var response = await client.read(userinfoEndpoint); + var grant = oauth2.AuthorizationCodeGrant( + clientId, + authorizationEndpoint, + tokenEndpoint, + secret: clientSecret, + basicAuth: false, + ); - print('Logged in: $response'); + var authorizationUrl = grant.getAuthorizationUrl(redirectUrl); + + if (Platform.isAndroid || Platform.isIOS) { + // Let Goatpass know it is embed in an app + authorizationUrl = authorizationUrl.replace( + queryParameters: {"embedded": "yes"} + ..addAll(authorizationUrl.queryParameters)); + + // Use WebView to get authorization url + var responseUrl = await Navigator.of(context).push(MaterialPageRoute( + builder: (context) => AuthorizationPage(authorizationUrl), + )); + + var responseUri = Uri.parse(responseUrl); + return await grant + .handleAuthorizationResponse(responseUri.queryParameters); + } else { + throw UnimplementedError("unsupported platform"); + } + } + + Future login(BuildContext context) async { + try { + client = await createClient(context); + var userinfo = await client!.read(userinfoEndpoint); + + storage.write(key: profileKey, value: userinfo); + storage.write(key: storageKey, value: client!.credentials.toJson()); + + print('Logged in: $userinfo'); } catch (e) { print(e); } } -} -class AuthorizationPage extends StatelessWidget { - final Uri authorizationUrl; - - const AuthorizationPage(this.authorizationUrl, {super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Connect with Goatpass'), - ), - body: WebViewWidget( - controller: WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setBackgroundColor(Colors.indigo) - ..setNavigationDelegate(NavigationDelegate( - onNavigationRequest: (NavigationRequest request) { - if (request.url.startsWith('goatagent://auth')) { - Navigator.of(context).pop(request.url); - return NavigationDecision.prevent; - } - return NavigationDecision.navigate; - }, - )) - ..loadRequest(authorizationUrl), - ), - ); + Future isAuthorized() async { + const storage = FlutterSecureStorage(); + return await storage.containsKey(key: storageKey); } + + Future readProfiles() async { + const storage = FlutterSecureStorage(); + return jsonDecode(await storage.read(key: profileKey) ?? "{}"); + } + + AuthGuard._internal(); } diff --git a/lib/main.dart b/lib/main.dart index 637a573..30bb1f0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:goatagent/auth.dart'; +import 'package:goatagent/screens/auth.dart'; import 'package:goatagent/firebase.dart'; import 'package:goatagent/screens/account.dart'; import 'package:goatagent/screens/dashboard.dart'; @@ -10,6 +11,7 @@ import 'layouts/navigation.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + AuthGuard().pickClient(); initializeFirebase(); runApp(GoatAgent()); diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 0d35572..f5cf372 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -1,15 +1,33 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:goatagent/auth.dart'; +import 'package:goatagent/widgets/name_card.dart'; +import 'package:oauth2/oauth2.dart' as oauth2; + +import 'auth.dart'; class AccountScreen extends StatelessWidget { const AccountScreen({super.key}); @override Widget build(BuildContext context) { - return const Scaffold( - body: Center( - child: Text("你好"), + return Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20), + child: Column( + children: [ + NameCard( + onLogin: () async { + await AuthGuard().login(context); + }, + ), + ], + ), + ), ), ); } - -} \ No newline at end of file +} diff --git a/lib/screens/auth.dart b/lib/screens/auth.dart new file mode 100644 index 0000000..c1ed315 --- /dev/null +++ b/lib/screens/auth.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class AuthorizationPage extends StatelessWidget { + final Uri authorizationUrl; + + const AuthorizationPage(this.authorizationUrl, {super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Connect with Goatpass'), + ), + body: WebViewWidget( + controller: WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(Colors.indigo) + ..setNavigationDelegate(NavigationDelegate( + onNavigationRequest: (NavigationRequest request) { + if (request.url.startsWith('goatagent://auth')) { + Navigator.of(context).pop(request.url); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + )) + ..loadRequest(authorizationUrl), + ), + ); + } +} diff --git a/lib/widgets/name_card.dart b/lib/widgets/name_card.dart new file mode 100644 index 0000000..0b93d2a --- /dev/null +++ b/lib/widgets/name_card.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +import '../auth.dart'; + +class NameCard extends StatelessWidget { + const NameCard({super.key, this.onLogin, this.onCheck}); + + final void Function()? onLogin; + final void Function()? onCheck; + + Future _getAvatar() async { + if (await AuthGuard().isAuthorized()) { + final profiles = await AuthGuard().readProfiles(); + return CircleAvatar(backgroundImage: NetworkImage(profiles["picture"])); + } else { + return const CircleAvatar(child: Icon(Icons.account_circle)); + } + } + + Future _getDescribe() async { + if (await AuthGuard().isAuthorized()) { + final profiles = await AuthGuard().readProfiles(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + profiles["nick"], + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Text(profiles["email"]) + ], + ); + } else { + return const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Unauthorized", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Text("Click here to login in.") + ], + ); + } + } + + @override + Widget build(BuildContext context) { + return Card( + child: InkWell( + splashColor: Colors.indigo.withAlpha(30), + onTap: () async { + if (await AuthGuard().isAuthorized() && onCheck != null) { + onCheck!(); + } else if (onLogin != null) { + onLogin!(); + } + }, + child: Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + FutureBuilder( + future: _getAvatar(), + builder: + (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return const CircularProgressIndicator(); + } + }, + ), + const SizedBox(width: 20), + FutureBuilder( + future: _getDescribe(), + builder: + (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return const Column(); + } + }, + ) + ], + ), + ), + ), + ); + } +}