✨ Auth guard
This commit is contained in:
parent
886d362291
commit
de12616109
109
lib/auth.dart
109
lib/auth.dart
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> isAuthorized() async {
|
||||||
|
const storage = FlutterSecureStorage();
|
||||||
|
return await storage.containsKey(key: storageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthorizationPage extends StatelessWidget {
|
Future<dynamic> readProfiles() async {
|
||||||
final Uri authorizationUrl;
|
const storage = FlutterSecureStorage();
|
||||||
|
return jsonDecode(await storage.read(key: profileKey) ?? "{}");
|
||||||
|
}
|
||||||
|
|
||||||
const AuthorizationPage(this.authorizationUrl, {super.key});
|
AuthGuard._internal();
|
||||||
|
|
||||||
@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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
@ -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
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