import 'dart:io'; import 'dart:convert'; import 'dart:async'; import 'package:git_touch/utils/request_serilizer.dart'; import 'package:gql_http_link/gql_http_link.dart'; import 'package:artemis/artemis.dart'; import 'package:fimber/fimber.dart'; import 'package:http/http.dart' as http; import 'package:uni_links/uni_links.dart'; import 'package:nanoid/nanoid.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../utils/constants.dart'; import '../utils/utils.dart'; import 'account.dart'; import 'gitlab.dart'; class PlatformType { static const github = 'github'; static const gitlab = 'gitlab'; } class AuthModel with ChangeNotifier { static const _apiPrefix = 'https://api.github.com'; List<Account> _accounts; int activeAccountIndex; StreamSubscription<Uri> _sub; bool loading = false; List<Account> get accounts => _accounts; bool get ready => _accounts != null; Account get activeAccount { if (activeAccountIndex == null || _accounts == null) return null; return _accounts[activeAccountIndex]; } String get token => activeAccount.token; _addAccount(Account account) async { // Remove previous if duplicated List<Account> newAccounts = []; for (var a in _accounts) { if (!account.equals(a)) { newAccounts.add(a); } } newAccounts.add(account); _accounts = newAccounts; // Save final prefs = await SharedPreferences.getInstance(); await prefs.setString(StorageKeys.accounts, json.encode(_accounts)); } // https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#web-application-flow Future<void> _onSchemeDetected(Uri uri) async { await closeWebView(); loading = true; notifyListeners(); // Get token by code final res = await http.post( 'https://github.com/login/oauth/access_token', headers: { HttpHeaders.acceptHeader: 'application/json', HttpHeaders.contentTypeHeader: 'application/json', }, body: json.encode({ 'client_id': clientId, 'client_secret': clientSecret, 'code': uri.queryParameters['code'], 'state': _oauthState, }), ); final token = json.decode(res.body)['access_token'] as String; await loginWithToken(token); } Future<void> loginWithToken(String token) async { // Get login and avatar url final queryData = await query(''' { viewer { login avatarUrl } } ''', token); await _addAccount(Account( platform: PlatformType.github, domain: 'https://github.com', token: token, login: queryData['viewer']['login'] as String, avatarUrl: queryData['viewer']['avatarUrl'] as String, )); loading = false; notifyListeners(); } Future<void> loginToGitlab(String domain, String token) async { try { loading = true; notifyListeners(); final res = await http .get('$domain/api/v4/user', headers: {'Private-Token': token}); final info = json.decode(res.body); if (info['message'] != null) { throw info['message']; } final user = GitlabUser.fromJson(info); await _addAccount(Account( platform: PlatformType.gitlab, domain: domain, token: token, login: user.username, avatarUrl: user.avatarUrl, gitlabId: user.id, )); } catch (err) { Fimber.e('loginToGitlab failed', ex: err); // TODO: show errors } finally { loading = false; notifyListeners(); } } Future<String> fetchWithGitlabToken(String p) async { final res = await http.get(p, headers: {'Private-Token': token}); return res.body; } Future fetchGitlab(String p) async { final res = await http.get(activeAccount.domain + '/api/v4' + p, headers: {'Private-Token': token}); final info = json.decode(utf8.decode(res.bodyBytes)); return info; } Future fetchGitea(String p) async { final res = await http.get('https://try.gitea.io' + '/api/v1' + p, headers: {'Authorization': ''}); final info = json.decode(utf8.decode(res.bodyBytes)); return info; } Future<void> init() async { // Listen scheme _sub = getUriLinksStream().listen(_onSchemeDetected, onError: (err) { Fimber.e('getUriLinksStream failed', ex: err); }); var prefs = await SharedPreferences.getInstance(); // Read accounts try { String str = prefs.getString(StorageKeys.accounts); Fimber.d('read accounts: $str'); _accounts = (json.decode(str ?? '[]') as List) .map((item) => Account.fromJson(item)) .toList(); } catch (err) { Fimber.e('prefs getAccount failed', ex: err); _accounts = []; } notifyListeners(); } @override void dispose() { _sub.cancel(); super.dispose(); } void setActiveAccountIndex(int index) { activeAccountIndex = index; notifyListeners(); } Map<String, String> get _headers => {HttpHeaders.authorizationHeader: 'token $token'}; // http timeout var _timeoutDuration = Duration(seconds: 10); // var _timeoutDuration = Duration(seconds: 1); ArtemisClient _gqlClient; ArtemisClient get gqlClient { if (token == null) return null; if (_gqlClient == null) { _gqlClient = ArtemisClient.fromLink( HttpLink( _apiPrefix + '/graphql', defaultHeaders: {HttpHeaders.authorizationHeader: 'token $token'}, serializer: GithubRequestSerializer(), ), ); } return _gqlClient; } Future<dynamic> query(String query, [String _token]) async { if (_token == null) { _token = token; } if (_token == null) { throw 'token is null'; } final res = await http .post(_apiPrefix + '/graphql', headers: { HttpHeaders.authorizationHeader: 'token $_token', HttpHeaders.contentTypeHeader: 'application/json' }, body: json.encode({'query': query})) .timeout(_timeoutDuration); // Fimber.d(res.body); final data = json.decode(res.body); if (data['errors'] != null) { throw data['errors'][0]['message']; } return data['data']; } Future<dynamic> getWithCredentials(String url, {String contentType}) async { var headers = _headers; if (contentType != null) { // https://developer.github.com/v3/repos/contents/#custom-media-types headers[HttpHeaders.contentTypeHeader] = contentType; } final res = await http .get(_apiPrefix + url, headers: headers) .timeout(_timeoutDuration); // Fimber.d(res.body); final data = json.decode(res.body); return data; } Future<void> patchWithCredentials(String url) async { await http .patch(_apiPrefix + url, headers: _headers) .timeout(_timeoutDuration); } Future<http.Response> putWithCredentials( String url, { String contentType = 'application/json', Map<String, dynamic> body = const {}, }) async { return http .put( _apiPrefix + url, headers: {..._headers, HttpHeaders.contentTypeHeader: contentType}, body: json.encode(body), ) .timeout(_timeoutDuration); } String _oauthState; void redirectToGithubOauth() { _oauthState = nanoid(); var scope = Uri.encodeComponent('user,repo,read:org'); launchUrl( 'https://github.com/login/oauth/authorize?client_id=$clientId&redirect_uri=gittouch://login&scope=$scope&state=$_oauthState', ); } }