2019-01-31 07:20:48 +01:00
|
|
|
import 'dart:io';
|
2019-02-07 07:35:19 +01:00
|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:async';
|
2019-12-07 06:18:44 +01:00
|
|
|
import 'package:git_touch/utils/request_serilizer.dart';
|
2019-12-06 14:51:33 +01:00
|
|
|
import 'package:gql_http_link/gql_http_link.dart';
|
|
|
|
import 'package:artemis/artemis.dart';
|
2019-10-13 05:01:29 +02:00
|
|
|
import 'package:fimber/fimber.dart';
|
2019-02-07 07:35:19 +01:00
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'package:uni_links/uni_links.dart';
|
|
|
|
import 'package:nanoid/nanoid.dart';
|
2019-02-11 17:22:58 +01:00
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
2019-01-31 07:20:48 +01:00
|
|
|
import 'package:flutter/material.dart';
|
2019-03-10 14:53:35 +01:00
|
|
|
import 'package:flutter/cupertino.dart';
|
2019-02-07 07:35:19 +01:00
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
2019-02-09 11:20:14 +01:00
|
|
|
import '../utils/constants.dart';
|
2019-03-10 09:09:26 +01:00
|
|
|
import '../utils/utils.dart';
|
2019-09-08 14:07:35 +02:00
|
|
|
import 'account.dart';
|
2019-12-30 13:50:31 +01:00
|
|
|
import 'gitlab.dart';
|
2019-02-21 11:43:41 +01:00
|
|
|
|
2019-02-21 14:21:16 +01:00
|
|
|
class PlatformType {
|
2019-02-21 15:36:19 +01:00
|
|
|
static const github = 'github';
|
|
|
|
static const gitlab = 'gitlab';
|
|
|
|
}
|
|
|
|
|
2019-09-27 14:52:38 +02:00
|
|
|
class AuthModel with ChangeNotifier {
|
2019-09-26 16:14:14 +02:00
|
|
|
static const _apiPrefix = 'https://api.github.com';
|
2019-02-21 14:21:16 +01:00
|
|
|
|
2019-09-27 14:52:38 +02:00
|
|
|
List<Account> _accounts;
|
2019-09-26 16:14:14 +02:00
|
|
|
int activeAccountIndex;
|
2019-02-07 07:35:19 +01:00
|
|
|
StreamSubscription<Uri> _sub;
|
2019-02-10 06:17:25 +01:00
|
|
|
bool loading = false;
|
2019-02-07 07:35:19 +01:00
|
|
|
|
2019-09-27 14:52:38 +02:00
|
|
|
List<Account> get accounts => _accounts;
|
2019-09-26 16:14:14 +02:00
|
|
|
bool get ready => _accounts != null;
|
2019-09-27 14:52:38 +02:00
|
|
|
Account get activeAccount {
|
2019-09-26 16:14:14 +02:00
|
|
|
if (activeAccountIndex == null || _accounts == null) return null;
|
|
|
|
return _accounts[activeAccountIndex];
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
2019-01-30 07:46:18 +01:00
|
|
|
|
2019-09-26 16:14:14 +02:00
|
|
|
String get token => activeAccount.token;
|
|
|
|
|
2019-09-27 14:52:38 +02:00
|
|
|
_addAccount(Account account) async {
|
2019-09-26 16:28:40 +02:00
|
|
|
// Remove previous if duplicated
|
2019-09-27 14:52:38 +02:00
|
|
|
List<Account> newAccounts = [];
|
2019-09-26 16:28:40 +02:00
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
2019-02-07 07:35:19 +01:00
|
|
|
// https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#web-application-flow
|
2019-02-21 11:43:41 +01:00
|
|
|
Future<void> _onSchemeDetected(Uri uri) async {
|
2019-03-13 14:31:36 +01:00
|
|
|
await closeWebView();
|
2019-02-11 17:22:58 +01:00
|
|
|
|
2019-09-08 14:07:35 +02:00
|
|
|
loading = true;
|
|
|
|
notifyListeners();
|
2019-02-10 06:17:25 +01:00
|
|
|
|
2019-09-26 16:14:14 +02:00
|
|
|
// Get token by code
|
|
|
|
final res = await http.post(
|
2019-02-07 07:35:19 +01:00
|
|
|
'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,
|
2019-09-26 16:14:14 +02:00
|
|
|
'code': uri.queryParameters['code'],
|
2019-09-02 15:52:32 +02:00
|
|
|
'state': _oauthState,
|
2019-02-07 07:35:19 +01:00
|
|
|
}),
|
|
|
|
);
|
2019-09-26 16:14:14 +02:00
|
|
|
final token = json.decode(res.body)['access_token'] as String;
|
2019-10-03 06:55:17 +02:00
|
|
|
await loginWithToken(token);
|
2019-02-21 11:43:41 +01:00
|
|
|
}
|
2019-02-07 07:35:19 +01:00
|
|
|
|
2019-10-03 06:55:17 +02:00
|
|
|
Future<void> loginWithToken(String token) async {
|
2019-09-26 16:14:14 +02:00
|
|
|
// Get login and avatar url
|
|
|
|
final queryData = await query('''
|
2019-02-07 07:35:19 +01:00
|
|
|
{
|
|
|
|
viewer {
|
|
|
|
login
|
|
|
|
avatarUrl
|
|
|
|
}
|
|
|
|
}
|
2019-02-21 11:43:41 +01:00
|
|
|
''', token);
|
2019-02-07 07:35:19 +01:00
|
|
|
|
2019-09-27 14:52:38 +02:00
|
|
|
await _addAccount(Account(
|
2019-09-26 16:14:14 +02:00
|
|
|
platform: PlatformType.github,
|
2019-09-26 16:40:53 +02:00
|
|
|
domain: 'https://github.com',
|
2019-09-26 16:14:14 +02:00
|
|
|
token: token,
|
|
|
|
login: queryData['viewer']['login'] as String,
|
|
|
|
avatarUrl: queryData['viewer']['avatarUrl'] as String,
|
2019-09-26 16:28:40 +02:00
|
|
|
));
|
2019-02-07 07:35:19 +01:00
|
|
|
|
2019-09-08 14:07:35 +02:00
|
|
|
loading = false;
|
|
|
|
notifyListeners();
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
|
|
|
|
2019-02-21 14:21:16 +01:00
|
|
|
Future<void> loginToGitlab(String domain, String token) async {
|
|
|
|
try {
|
2019-09-26 16:14:14 +02:00
|
|
|
loading = true;
|
|
|
|
notifyListeners();
|
|
|
|
|
|
|
|
final res = await http
|
2019-02-21 14:21:16 +01:00
|
|
|
.get('$domain/api/v4/user', headers: {'Private-Token': token});
|
2019-09-26 16:14:14 +02:00
|
|
|
final info = json.decode(res.body);
|
2019-02-21 14:21:16 +01:00
|
|
|
if (info['message'] != null) {
|
|
|
|
throw info['message'];
|
|
|
|
}
|
2019-12-30 13:50:31 +01:00
|
|
|
final user = GitlabUser.fromJson(info);
|
2019-02-21 14:21:16 +01:00
|
|
|
|
2019-09-27 14:52:38 +02:00
|
|
|
await _addAccount(Account(
|
2019-09-26 16:14:14 +02:00
|
|
|
platform: PlatformType.gitlab,
|
|
|
|
domain: domain,
|
|
|
|
token: token,
|
2019-12-30 13:50:31 +01:00
|
|
|
login: user.username,
|
|
|
|
avatarUrl: user.avatarUrl,
|
|
|
|
gitlabId: user.id,
|
2019-09-26 16:28:40 +02:00
|
|
|
));
|
2019-02-21 14:21:16 +01:00
|
|
|
} catch (err) {
|
2019-10-13 05:01:29 +02:00
|
|
|
Fimber.e('loginToGitlab failed', ex: err);
|
2019-02-21 14:21:16 +01:00
|
|
|
// TODO: show errors
|
|
|
|
} finally {
|
2019-09-08 14:07:35 +02:00
|
|
|
loading = false;
|
|
|
|
notifyListeners();
|
2019-02-21 14:21:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-11 16:37:29 +01:00
|
|
|
Future<String> fetchWithGitlabToken(String p) async {
|
|
|
|
final res = await http.get(p, headers: {'Private-Token': token});
|
|
|
|
return res.body;
|
|
|
|
}
|
|
|
|
|
2019-11-01 18:12:17 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-12-04 15:02:22 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-11-05 08:09:54 +01:00
|
|
|
Future<void> init() async {
|
2019-09-26 16:14:14 +02:00
|
|
|
// Listen scheme
|
2019-09-08 14:07:35 +02:00
|
|
|
_sub = getUriLinksStream().listen(_onSchemeDetected, onError: (err) {
|
2019-10-13 05:01:29 +02:00
|
|
|
Fimber.e('getUriLinksStream failed', ex: err);
|
2019-09-08 14:07:35 +02:00
|
|
|
});
|
|
|
|
|
2019-09-02 15:52:32 +02:00
|
|
|
var prefs = await SharedPreferences.getInstance();
|
2019-02-07 07:35:19 +01:00
|
|
|
|
2019-09-26 16:14:14 +02:00
|
|
|
// Read accounts
|
2019-02-21 14:21:16 +01:00
|
|
|
try {
|
2019-09-26 16:14:14 +02:00
|
|
|
String str = prefs.getString(StorageKeys.accounts);
|
2019-10-13 05:01:29 +02:00
|
|
|
Fimber.d('read accounts: $str');
|
2019-09-26 16:14:14 +02:00
|
|
|
_accounts = (json.decode(str ?? '[]') as List)
|
2019-09-27 14:52:38 +02:00
|
|
|
.map((item) => Account.fromJson(item))
|
2019-09-26 16:14:14 +02:00
|
|
|
.toList();
|
2019-02-21 14:21:16 +01:00
|
|
|
} catch (err) {
|
2019-10-13 05:01:29 +02:00
|
|
|
Fimber.e('prefs getAccount failed', ex: err);
|
2019-09-26 16:14:14 +02:00
|
|
|
_accounts = [];
|
2019-02-21 14:21:16 +01:00
|
|
|
}
|
|
|
|
|
2019-09-08 14:07:35 +02:00
|
|
|
notifyListeners();
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
|
|
|
|
2019-10-03 06:24:09 +02:00
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
_sub.cancel();
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
2019-09-26 16:14:14 +02:00
|
|
|
void setActiveAccountIndex(int index) {
|
|
|
|
activeAccountIndex = index;
|
2019-09-08 14:07:35 +02:00
|
|
|
notifyListeners();
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
|
|
|
|
2019-09-02 15:52:32 +02:00
|
|
|
Map<String, String> get _headers =>
|
|
|
|
{HttpHeaders.authorizationHeader: 'token $token'};
|
|
|
|
|
2019-02-08 13:52:10 +01:00
|
|
|
// http timeout
|
|
|
|
var _timeoutDuration = Duration(seconds: 10);
|
|
|
|
// var _timeoutDuration = Duration(seconds: 1);
|
|
|
|
|
2019-12-06 14:51:33 +01:00
|
|
|
ArtemisClient _gqlClient;
|
|
|
|
ArtemisClient get gqlClient {
|
|
|
|
if (token == null) return null;
|
2019-11-06 14:27:37 +01:00
|
|
|
|
2019-12-06 14:51:33 +01:00
|
|
|
if (_gqlClient == null) {
|
|
|
|
_gqlClient = ArtemisClient.fromLink(
|
2019-12-07 06:18:44 +01:00
|
|
|
HttpLink(
|
|
|
|
_apiPrefix + '/graphql',
|
|
|
|
defaultHeaders: {HttpHeaders.authorizationHeader: 'token $token'},
|
|
|
|
serializer: GithubRequestSerializer(),
|
|
|
|
),
|
2019-12-06 14:51:33 +01:00
|
|
|
);
|
|
|
|
}
|
2019-11-06 14:27:37 +01:00
|
|
|
|
2019-12-06 14:51:33 +01:00
|
|
|
return _gqlClient;
|
|
|
|
}
|
2019-11-06 14:27:37 +01:00
|
|
|
|
2019-02-07 07:35:19 +01:00
|
|
|
Future<dynamic> query(String query, [String _token]) async {
|
|
|
|
if (_token == null) {
|
|
|
|
_token = token;
|
|
|
|
}
|
|
|
|
if (_token == null) {
|
2019-09-26 16:14:14 +02:00
|
|
|
throw 'token is null';
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
|
|
|
|
2019-02-08 13:52:10 +01:00
|
|
|
final res = await http
|
2019-09-08 14:07:35 +02:00
|
|
|
.post(_apiPrefix + '/graphql',
|
2019-02-08 13:52:10 +01:00
|
|
|
headers: {
|
|
|
|
HttpHeaders.authorizationHeader: 'token $_token',
|
|
|
|
HttpHeaders.contentTypeHeader: 'application/json'
|
|
|
|
},
|
|
|
|
body: json.encode({'query': query}))
|
|
|
|
.timeout(_timeoutDuration);
|
|
|
|
|
2019-10-13 05:01:29 +02:00
|
|
|
// Fimber.d(res.body);
|
2019-02-07 07:35:19 +01:00
|
|
|
final data = json.decode(res.body);
|
|
|
|
|
|
|
|
if (data['errors'] != null) {
|
2019-09-26 16:14:14 +02:00
|
|
|
throw data['errors'][0]['message'];
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
2019-02-10 12:15:50 +01:00
|
|
|
|
2019-02-07 07:35:19 +01:00
|
|
|
return data['data'];
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<dynamic> getWithCredentials(String url, {String contentType}) async {
|
2019-09-02 15:52:32 +02:00
|
|
|
var headers = _headers;
|
2019-02-07 07:35:19 +01:00
|
|
|
if (contentType != null) {
|
|
|
|
// https://developer.github.com/v3/repos/contents/#custom-media-types
|
|
|
|
headers[HttpHeaders.contentTypeHeader] = contentType;
|
|
|
|
}
|
2019-02-08 13:52:10 +01:00
|
|
|
final res = await http
|
2019-09-08 14:07:35 +02:00
|
|
|
.get(_apiPrefix + url, headers: headers)
|
2019-02-08 13:52:10 +01:00
|
|
|
.timeout(_timeoutDuration);
|
2019-10-13 05:01:29 +02:00
|
|
|
// Fimber.d(res.body);
|
2019-02-07 07:35:19 +01:00
|
|
|
final data = json.decode(res.body);
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2019-09-02 15:52:32 +02:00
|
|
|
Future<void> patchWithCredentials(String url) async {
|
2019-09-08 14:07:35 +02:00
|
|
|
await http
|
|
|
|
.patch(_apiPrefix + url, headers: _headers)
|
|
|
|
.timeout(_timeoutDuration);
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
|
|
|
|
2019-10-03 06:16:44 +02:00
|
|
|
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),
|
|
|
|
)
|
2019-02-08 13:52:10 +01:00
|
|
|
.timeout(_timeoutDuration);
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
|
|
|
|
2019-10-03 06:16:44 +02:00
|
|
|
Future<http.Response> postWithCredentials(
|
|
|
|
String url, {
|
|
|
|
String contentType = 'application/json',
|
|
|
|
Map<String, dynamic> body = const {},
|
|
|
|
}) async {
|
|
|
|
return http
|
|
|
|
.post(
|
|
|
|
_apiPrefix + url,
|
|
|
|
headers: {..._headers, HttpHeaders.contentTypeHeader: contentType},
|
|
|
|
body: json.encode(body),
|
|
|
|
)
|
2019-04-05 15:19:00 +02:00
|
|
|
.timeout(_timeoutDuration);
|
|
|
|
}
|
|
|
|
|
2019-09-02 15:52:32 +02:00
|
|
|
Future<void> deleteWithCredentials(String url) async {
|
|
|
|
await http
|
2019-09-08 14:07:35 +02:00
|
|
|
.delete(_apiPrefix + url, headers: _headers)
|
2019-02-10 11:50:40 +01:00
|
|
|
.timeout(_timeoutDuration);
|
|
|
|
}
|
|
|
|
|
2019-09-02 15:52:32 +02:00
|
|
|
String _oauthState;
|
|
|
|
void redirectToGithubOauth() {
|
|
|
|
_oauthState = nanoid();
|
2019-09-21 19:02:14 +02:00
|
|
|
var scope = Uri.encodeComponent('user,repo,read:org');
|
2019-09-29 07:32:53 +02:00
|
|
|
launchUrl(
|
2019-09-21 19:02:14 +02:00
|
|
|
'https://github.com/login/oauth/authorize?client_id=$clientId&redirect_uri=gittouch://login&scope=$scope&state=$_oauthState',
|
2019-09-02 15:52:32 +02:00
|
|
|
);
|
2019-01-31 07:20:48 +01:00
|
|
|
}
|
2019-01-30 07:46:18 +01:00
|
|
|
}
|