✨ Auth guard
This commit is contained in:
		
							
								
								
									
										185
									
								
								lib/auth.dart
									
									
									
									
									
								
							
							
						
						
									
										185
									
								
								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<void> _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<bool> 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<oauth2.Client> 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<oauth2.Client> 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<void> 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<bool> isAuthorized() async {
 | 
			
		||||
    const storage = FlutterSecureStorage();
 | 
			
		||||
    return await storage.containsKey(key: storageKey);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<dynamic> readProfiles() async {
 | 
			
		||||
    const storage = FlutterSecureStorage();
 | 
			
		||||
    return jsonDecode(await storage.read(key: profileKey) ?? "{}");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  AuthGuard._internal();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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());
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								lib/screens/auth.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								lib/screens/auth.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										101
									
								
								lib/widgets/name_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								lib/widgets/name_card.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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<CircleAvatar> _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<Column> _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<CircleAvatar>(
 | 
			
		||||
                future: _getAvatar(),
 | 
			
		||||
                builder:
 | 
			
		||||
                    (BuildContext context, AsyncSnapshot<Widget> snapshot) {
 | 
			
		||||
                  if (snapshot.hasData) {
 | 
			
		||||
                    return snapshot.data!;
 | 
			
		||||
                  } else {
 | 
			
		||||
                    return const CircularProgressIndicator();
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
              const SizedBox(width: 20),
 | 
			
		||||
              FutureBuilder<Column>(
 | 
			
		||||
                future: _getDescribe(),
 | 
			
		||||
                builder:
 | 
			
		||||
                    (BuildContext context, AsyncSnapshot<Column> snapshot) {
 | 
			
		||||
                  if (snapshot.hasData) {
 | 
			
		||||
                    return snapshot.data!;
 | 
			
		||||
                  } else {
 | 
			
		||||
                    return const Column();
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
              )
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user