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,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();
}

View File

@ -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());

View File

@ -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
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();
}
},
)
],
),
),
),
);
}
}