Auth guard

This commit is contained in:
LittleSheep 2024-02-08 04:40:56 +08:00
parent 886d362291
commit de12616109
5 changed files with 243 additions and 105 deletions

View File

@ -1,31 +1,14 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.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; import 'package:oauth2/oauth2.dart' as oauth2;
class LoginPage extends StatelessWidget { class AuthGuard {
const LoginPage({super.key}); 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'),
),
),
);
}
Future<void> _login(BuildContext context) async {
final authorizationEndpoint = final authorizationEndpoint =
Uri.parse('https://id.smartsheep.studio/auth/o/connect'); Uri.parse('https://id.smartsheep.studio/auth/o/connect');
final tokenEndpoint = final tokenEndpoint =
@ -34,19 +17,36 @@ class LoginPage extends StatelessWidget {
Uri.parse('https://id.smartsheep.studio/api/users/me'); Uri.parse('https://id.smartsheep.studio/api/users/me');
final redirectUrl = Uri.parse('goatagent://auth'); final redirectUrl = Uri.parse('goatagent://auth');
const clientId = "goatagent"; static const clientId = "goatagent";
const clientSecret = "_F4%q2Eea3"; static const clientSecret = "_F4%q2Eea3";
const storage = FlutterSecureStorage(); static const storage = FlutterSecureStorage();
const storageKey = "identity"; static const storageKey = "identity";
static const profileKey = "profiles";
Future<oauth2.Client> createClient() async { factory AuthGuard() {
// If logged in return _singleton;
}
oauth2.Client? client;
Future<bool> pickClient() async {
if (await storage.containsKey(key: storageKey)) { if (await storage.containsKey(key: storageKey)) {
var credentials = oauth2.Credentials.fromJson( var credentials =
storage.read(key: storageKey) as String); oauth2.Credentials.fromJson((await storage.read(key: storageKey))!);
return oauth2.Client(credentials, client = oauth2.Client(credentials,
identifier: clientId, secret: clientSecret); identifier: clientId, secret: clientSecret);
print(await storage.readAll());
return true;
} else {
return false;
}
}
Future<oauth2.Client> createClient(BuildContext context) async {
// If logged in
if (await pickClient()) {
return client!;
} }
var grant = oauth2.AuthorizationCodeGrant( var grant = oauth2.AuthorizationCodeGrant(
@ -61,7 +61,7 @@ class LoginPage extends StatelessWidget {
if (Platform.isAndroid || Platform.isIOS) { if (Platform.isAndroid || Platform.isIOS) {
// Let Goatpass know it is embed in an app // Let Goatpass know it is embed in an app
authorizationUrl.replace( authorizationUrl = authorizationUrl.replace(
queryParameters: {"embedded": "yes"} queryParameters: {"embedded": "yes"}
..addAll(authorizationUrl.queryParameters)); ..addAll(authorizationUrl.queryParameters));
@ -74,48 +74,33 @@ class LoginPage extends StatelessWidget {
return await grant return await grant
.handleAuthorizationResponse(responseUri.queryParameters); .handleAuthorizationResponse(responseUri.queryParameters);
} else { } else {
// TODO Use system browser to get url
throw UnimplementedError("unsupported platform"); throw UnimplementedError("unsupported platform");
} }
} }
Future<void> login(BuildContext context) async {
try { try {
var client = await createClient(); client = await createClient(context);
var response = await client.read(userinfoEndpoint); var userinfo = await client!.read(userinfoEndpoint);
print('Logged in: $response'); storage.write(key: profileKey, value: userinfo);
storage.write(key: storageKey, value: client!.credentials.toJson());
print('Logged in: $userinfo');
} catch (e) { } catch (e) {
print(e); print(e);
} }
} }
}
class AuthorizationPage extends StatelessWidget { Future<bool> isAuthorized() async {
final Uri authorizationUrl; const storage = FlutterSecureStorage();
return await storage.containsKey(key: storageKey);
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;
}, Future<dynamic> readProfiles() async {
)) const storage = FlutterSecureStorage();
..loadRequest(authorizationUrl), return jsonDecode(await storage.read(key: profileKey) ?? "{}");
),
);
} }
AuthGuard._internal();
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:goatagent/auth.dart'; import 'package:goatagent/auth.dart';
import 'package:goatagent/screens/auth.dart';
import 'package:goatagent/firebase.dart'; import 'package:goatagent/firebase.dart';
import 'package:goatagent/screens/account.dart'; import 'package:goatagent/screens/account.dart';
import 'package:goatagent/screens/dashboard.dart'; import 'package:goatagent/screens/dashboard.dart';
@ -10,6 +11,7 @@ import 'layouts/navigation.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
AuthGuard().pickClient();
initializeFirebase(); initializeFirebase();
runApp(GoatAgent()); runApp(GoatAgent());

View File

@ -1,15 +1,33 @@
import 'dart:io';
import 'package:flutter/material.dart'; 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 { class AccountScreen extends StatelessWidget {
const AccountScreen({super.key}); const AccountScreen({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Scaffold( return Scaffold(
body: Center( body: SafeArea(
child: Text("你好"), 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
View 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
View 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();
}
},
)
],
),
),
),
);
}
}