Skip to content

Instantly share code, notes, and snippets.

@HansMuller
Last active July 28, 2022 23:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save HansMuller/3da339b3a6993b45a51dc454e21a97d2 to your computer and use it in GitHub Desktop.
Save HansMuller/3da339b3a6993b45a51dc454e21a97d2 to your computer and use it in GitHub Desktop.
// 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