Last active
July 28, 2022 23:45
-
-
Save HansMuller/3da339b3a6993b45a51dc454e21a97d2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 The Flutter Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
// Based on https://codelabs.developers.google.com/codelabs/flutter-github-client#3 | |
// Must provide values for githubClientId, githubClientSecret, githubScopers in | |
// _AppState per the codelab | |
import 'dart:io'; | |
import 'package:github/github.dart' as github; | |
import 'package:http/http.dart' as http; | |
import 'package:go_router/go_router.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:oauth2/oauth2.dart' as oauth2; | |
import 'package:url_launcher/url_launcher.dart'; | |
void main() { | |
runApp(App()); | |
} | |
class App extends StatefulWidget { | |
const App({ super.key }); | |
static const String title = 'GoRouter GitHub Login'; | |
@override | |
State<App> createState() => _AppState(); | |
} | |
class _AppState extends State<App> { | |
final GithubLoginController loginController = GithubLoginController( | |
githubClientId, | |
githubClientSecret, | |
githubScopes, | |
); | |
late final GoRouter _router = GoRouter( | |
routes: <GoRoute>[ | |
GoRoute( | |
path: '/', | |
builder: (BuildContext context, GoRouterState state) { | |
return Home(loginController: loginController); | |
}, | |
), | |
GoRoute( | |
path: '/login', | |
builder: (BuildContext context, GoRouterState state) { | |
return Login(loginController: loginController); | |
}, | |
), | |
], | |
redirect: (GoRouterState state) { | |
final bool isLoginPageShowing = state.subloc == '/login'; | |
if (!loginController.isLoggedIn) { | |
return isLoginPageShowing ? null : '/login'; | |
} | |
// If the user is logged in but still on the login page then | |
// send them to the home page. | |
if (isLoginPageShowing) { | |
return '/'; | |
} | |
// No need to redirect. | |
return null; | |
}, | |
refreshListenable: loginController, | |
); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp.router( | |
routeInformationProvider: _router.routeInformationProvider, | |
routeInformationParser: _router.routeInformationParser, | |
routerDelegate: _router.routerDelegate, | |
title: 'Framework Team GitHub Dashboard', | |
debugShowCheckedModeBanner: false, | |
); | |
} | |
} | |
class Login extends StatelessWidget { | |
const Login({ required this.loginController, super.key }); | |
final GithubLoginController loginController; | |
@override | |
Widget build(BuildContext context) { | |
return Center( | |
child: ElevatedButton( | |
onPressed: () async { | |
loginController.login(); | |
}, | |
child: const Text('Login to Github'), | |
), | |
); | |
} | |
} | |
class Home extends StatefulWidget { | |
const Home({ super.key, required this.loginController }); | |
final GithubLoginController loginController; | |
@override | |
State<Home> createState() => _HomeState(); | |
} | |
class _HomeState extends State<Home> { | |
late final github.GitHub hub; | |
@override | |
void initState() { | |
super.initState(); | |
assert(widget.loginController.isLoggedIn); | |
hub = github.GitHub(auth: widget.loginController.authentication); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text(App.title), | |
actions: <Widget>[ | |
IconButton( | |
onPressed: () async { | |
widget.loginController.logout(); | |
}, | |
tooltip: 'Logout', | |
icon: const Icon(Icons.logout), | |
) | |
], | |
), | |
body: Center( | |
child: ElevatedButton( | |
onPressed: () async { | |
print('rate limit: ${hub.rateLimitRemaining}'); | |
}, | |
child: const Text('Access Github'), | |
), | |
), | |
); | |
} | |
} | |
class GithubLoginController extends ChangeNotifier { | |
GithubLoginController( | |
this.githubClientId, | |
this.githubClientSecret, | |
this.githubScopes, | |
); | |
final String githubClientId; | |
final String githubClientSecret; | |
final List<String> githubScopes; | |
static final Uri _authorizationEndpoint = Uri.parse('https://github.com/login/oauth/authorize'); | |
static final Uri _tokenEndpoint = Uri.parse('https://github.com/login/oauth/access_token'); | |
oauth2.Client? _client; | |
HttpServer? _redirectServer; | |
github.Authentication? authentication; | |
bool get isLoggedIn => authentication != null; | |
Future<void> login() async { | |
if (isLoggedIn) { | |
return; | |
} | |
await _redirectServer?.close(); | |
_redirectServer = await HttpServer.bind('localhost', 0); | |
_client = await _getOAuth2Client(Uri.parse('http://localhost:${_redirectServer!.port}/auth')); | |
authentication = await github.Authentication.withToken(_client!.credentials.accessToken); | |
notifyListeners(); | |
} | |
Future<void> logout() async { | |
_client?.close(); | |
_client = null; | |
await _redirectServer?.close(); | |
_redirectServer = null; | |
authentication = null; | |
notifyListeners(); | |
} | |
Future<oauth2.Client> _getOAuth2Client(Uri redirectUrl) async { | |
if (githubClientId.isEmpty || githubClientSecret.isEmpty) { | |
throw const GithubLoginException( | |
'githubClientId and githubClientSecret must be not empty. ' | |
'See `lib/github_oauth_credentials.dart` for more detail.'); | |
} | |
final oauth2.AuthorizationCodeGrant grant = oauth2.AuthorizationCodeGrant( | |
githubClientId, | |
_authorizationEndpoint, | |
_tokenEndpoint, | |
secret: githubClientSecret, | |
httpClient: _JsonAcceptingHttpClient(), | |
); | |
final Uri authorizationUrl = grant.getAuthorizationUrl(redirectUrl, scopes: githubScopes); | |
await _redirect(authorizationUrl); | |
final Map<String, String> responseQueryParameters = await _listen(); | |
return await grant.handleAuthorizationResponse(responseQueryParameters); | |
} | |
Future<void> _redirect(Uri authorizationUrl) async { | |
final String url = authorizationUrl.toString(); | |
if (await canLaunch(url)) { | |
await launch(url); | |
} else { | |
throw GithubLoginException('Could not launch $url'); | |
} | |
} | |
Future<Map<String, String>> _listen() async { | |
final HttpRequest request = await _redirectServer!.first; | |
final Map<String,String> params = request.uri.queryParameters; | |
request.response.statusCode = 200; | |
request.response.headers.set('content-type', 'text/plain'); | |
request.response.writeln('Authenticated! You can close this tab.'); | |
await request.response.close(); | |
await _redirectServer!.close(); | |
_redirectServer = null; | |
return params; | |
} | |
} | |
class _JsonAcceptingHttpClient extends http.BaseClient { | |
final _httpClient = http.Client(); | |
@override | |
Future<http.StreamedResponse> send(http.BaseRequest request) { | |
request.headers['Accept'] = 'application/json'; | |
return _httpClient.send(request); | |
} | |
} | |
class GithubLoginException implements Exception { | |
const GithubLoginException(this.message); | |
final String message; | |
@override | |
String toString() => message; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment