✨ Auth guard
This commit is contained in:
parent
886d362291
commit
de12616109
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