diff --git a/.gitignore b/.gitignore index 0470ce32..e8d80147 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ app.*.map.json cloudotp.db /assets/fonts/** + +/releases/** diff --git a/README.md b/README.md index 8ab8d079..4a314c4f 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ This is an awesome two-factor authenticator based on Flutter for Android and Win ## Screenshots -Light ModeDark ModeAdd Token +Light ModeDark ModeAdd Token -SettingThemeLock +SettingThemeLock -Export and  ImportDropbox \ No newline at end of file +Export and  ImportDropbox \ No newline at end of file diff --git a/README_CN.md b/README_CN.md index 58a17b3f..65b95fe2 100644 --- a/README_CN.md +++ b/README_CN.md @@ -15,8 +15,8 @@ ## Screenshots -Light ModeDark ModeAdd Token +Light ModeDark ModeAdd Token -SettingThemeLock +SettingThemeLock -Export and  ImportDropbox \ No newline at end of file +Export and  ImportDropbox \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 5813fb20..7483464f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -75,4 +75,5 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.interpolator:interpolator:1.0.0" + implementation 'com.google.gms:google-services:4.2.0' } diff --git a/assets/brand/WPS.png b/assets/brand/WPS.png new file mode 100644 index 00000000..20960451 Binary files /dev/null and b/assets/brand/WPS.png differ diff --git a/assets/brand/kingsoft.png b/assets/brand/kingsoft.png new file mode 100644 index 00000000..20960451 Binary files /dev/null and b/assets/brand/kingsoft.png differ diff --git "a/assets/brand/\345\215\216\344\270\272.png" "b/assets/brand/\345\215\216\344\270\272.png" new file mode 100644 index 00000000..d5675f9a Binary files /dev/null and "b/assets/brand/\345\215\216\344\270\272.png" differ diff --git "a/assets/brand/\350\205\276\350\256\257\344\272\221.png" "b/assets/brand/\350\205\276\350\256\257\344\272\221.png" new file mode 100644 index 00000000..fc831156 Binary files /dev/null and "b/assets/brand/\350\205\276\350\256\257\344\272\221.png" differ diff --git "a/assets/brand/\351\207\221\345\261\261.png" "b/assets/brand/\351\207\221\345\261\261.png" new file mode 100644 index 00000000..20960451 Binary files /dev/null and "b/assets/brand/\351\207\221\345\261\261.png" differ diff --git a/devtools_options.yaml b/devtools_options.yaml deleted file mode 100644 index 7e7e7f67..00000000 --- a/devtools_options.yaml +++ /dev/null @@ -1 +0,0 @@ -extensions: diff --git a/lib/Models/cloud_service_config.dart b/lib/Models/cloud_service_config.dart index f6c672a5..c78f0d89 100644 --- a/lib/Models/cloud_service_config.dart +++ b/lib/Models/cloud_service_config.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:cloudotp/TokenUtils/Cloud/cloud_service.dart'; import 'package:cloudotp/TokenUtils/Cloud/googledrive_cloud_service.dart'; -import 'package:cloudotp/Utils/app_provider.dart'; import 'package:cloudotp/Utils/cache_util.dart'; import '../TokenUtils/Cloud/dropbox_cloud_service.dart'; @@ -18,7 +17,8 @@ enum CloudServiceType { OneDrive, GoogleDrive, Dropbox, - S3Cloud,HuaweiCloud; + S3Cloud, + HuaweiCloud; String get label { switch (this) { @@ -40,6 +40,15 @@ enum CloudServiceType { static List toStrings() { return CloudServiceType.values.map((e) => e.label).toList(); } + + static List toEnableStrings() { + return [ + CloudServiceType.OneDrive.label, + CloudServiceType.Dropbox.label, + CloudServiceType.Webdav.label, + CloudServiceType.S3Cloud.label, + ]; + } } extension CloudServiceTypeExtensionOnint on int { @@ -107,13 +116,13 @@ class CloudServiceConfig { case CloudServiceType.Webdav: return WebDavCloudService(this); case CloudServiceType.GoogleDrive: - return GoogleDriveCloudService(rootContext, this); + return GoogleDriveCloudService(this); case CloudServiceType.OneDrive: - return OneDriveCloudService(rootContext, this); + return OneDriveCloudService(this); case CloudServiceType.Dropbox: - return DropboxCloudService(rootContext, this); + return DropboxCloudService(this); case CloudServiceType.HuaweiCloud: - return HuaweiCloudService(rootContext, this); + return HuaweiCloudService(this); case CloudServiceType.S3Cloud: return S3CloudService(this); } diff --git a/lib/Screens/Backup/cloud_service_screen.dart b/lib/Screens/Backup/cloud_service_screen.dart index a5c36997..454200ac 100644 --- a/lib/Screens/Backup/cloud_service_screen.dart +++ b/lib/Screens/Backup/cloud_service_screen.dart @@ -80,12 +80,10 @@ class _CloudServiceScreenState extends State physics: const NeverScrollableScrollPhysics(), controller: pageController, children: const [ - WebDavServiceScreen(), OneDriveServiceScreen(), - GoogleDriveServiceScreen(), DropboxServiceScreen(), + WebDavServiceScreen(), S3CloudServiceScreen(), - HuaweiCloudServiceScreen(), ], ), ), @@ -101,14 +99,13 @@ class _CloudServiceScreenState extends State context: context, topRadius: true, bottomRadius: true, - padding: const EdgeInsets.only(right: 10), child: Column( children: [ ItemBuilder.buildGroupTile( context: context, controller: _typeController, constraintWidth: false, - buttons: CloudServiceType.toStrings(), + buttons: CloudServiceType.toEnableStrings(), onSelected: (value, index, isSelected) { setState(() { _currentType = index.toCloudServiceType; diff --git a/lib/Screens/Backup/dropbox_service_screen.dart b/lib/Screens/Backup/dropbox_service_screen.dart index ffab3218..de41e0fa 100644 --- a/lib/Screens/Backup/dropbox_service_screen.dart +++ b/lib/Screens/Backup/dropbox_service_screen.dart @@ -61,7 +61,6 @@ class _DropboxServiceScreenState extends State _accountController.text = _dropboxCloudServiceConfig!.account ?? ""; _emailController.text = _dropboxCloudServiceConfig!.email ?? ""; _dropboxCloudService = DropboxCloudService( - context, _dropboxCloudServiceConfig!, onConfigChanged: updateConfig, ); @@ -70,7 +69,6 @@ class _DropboxServiceScreenState extends State CloudServiceConfig.init(type: CloudServiceType.Dropbox); await CloudServiceConfigDao.insertConfig(_dropboxCloudServiceConfig!); _dropboxCloudService = DropboxCloudService( - context, _dropboxCloudServiceConfig!, onConfigChanged: updateConfig, ); @@ -87,7 +85,7 @@ class _DropboxServiceScreenState extends State updateConfig(_dropboxCloudServiceConfig!); } inited = true; - setState(() {}); + if (mounted) setState(() {}); } updateConfig(CloudServiceConfig config) { @@ -322,6 +320,8 @@ class _DropboxServiceScreenState extends State text: S.current.webDavLogout, fontSizeDelta: 2, onTap: () async { + CustomLoadingDialog.showLoading( + title: S.current.webDavLoggingOut); await _dropboxCloudService!.signOut(); setState(() { _dropboxCloudServiceConfig!.connected = false; @@ -332,6 +332,8 @@ class _DropboxServiceScreenState extends State _dropboxCloudServiceConfig!.usedSize = -1; updateConfig(_dropboxCloudServiceConfig!); }); + CustomLoadingDialog.dismissLoading(); + IToast.show(S.current.webDavLogoutSuccess); }, ), ), diff --git a/lib/Screens/Backup/googledrive_service_screen.dart b/lib/Screens/Backup/googledrive_service_screen.dart index 8ee5ee6a..4e431102 100644 --- a/lib/Screens/Backup/googledrive_service_screen.dart +++ b/lib/Screens/Backup/googledrive_service_screen.dart @@ -63,7 +63,6 @@ class _GoogleDriveServiceScreenState extends State _accountController.text = _googledriveCloudServiceConfig!.account ?? ""; _emailController.text = _googledriveCloudServiceConfig!.email ?? ""; _googledriveCloudService = GoogleDriveCloudService( - context, _googledriveCloudServiceConfig!, onConfigChanged: updateConfig, ); @@ -72,7 +71,6 @@ class _GoogleDriveServiceScreenState extends State CloudServiceConfig.init(type: CloudServiceType.GoogleDrive); await CloudServiceConfigDao.insertConfig(_googledriveCloudServiceConfig!); _googledriveCloudService = GoogleDriveCloudService( - context, _googledriveCloudServiceConfig!, onConfigChanged: updateConfig, ); diff --git a/lib/Screens/Backup/huawei_service_screen.dart b/lib/Screens/Backup/huawei_service_screen.dart index f573a94c..a089d52c 100644 --- a/lib/Screens/Backup/huawei_service_screen.dart +++ b/lib/Screens/Backup/huawei_service_screen.dart @@ -59,7 +59,6 @@ class _HuaweiCloudServiceScreenState extends State _sizeController.text = _huaweiCloudCloudServiceConfig!.size; _accountController.text = _huaweiCloudCloudServiceConfig!.account ?? ""; _huaweiCloudCloudService = HuaweiCloudService( - context, _huaweiCloudCloudServiceConfig!, onConfigChanged: updateConfig, ); @@ -68,7 +67,6 @@ class _HuaweiCloudServiceScreenState extends State CloudServiceConfig.init(type: CloudServiceType.HuaweiCloud); await CloudServiceConfigDao.insertConfig(_huaweiCloudCloudServiceConfig!); _huaweiCloudCloudService = HuaweiCloudService( - context, _huaweiCloudCloudServiceConfig!, onConfigChanged: updateConfig, ); diff --git a/lib/Screens/Backup/onedrive_service_screen.dart b/lib/Screens/Backup/onedrive_service_screen.dart index 1424af61..066c9329 100644 --- a/lib/Screens/Backup/onedrive_service_screen.dart +++ b/lib/Screens/Backup/onedrive_service_screen.dart @@ -62,7 +62,6 @@ class _OneDriveServiceScreenState extends State _accountController.text = _oneDriveCloudServiceConfig!.account ?? ""; _emailController.text = _oneDriveCloudServiceConfig!.email ?? ""; _oneDriveCloudService = OneDriveCloudService( - context, _oneDriveCloudServiceConfig!, onConfigChanged: updateConfig, ); @@ -71,7 +70,6 @@ class _OneDriveServiceScreenState extends State CloudServiceConfig.init(type: CloudServiceType.OneDrive); await CloudServiceConfigDao.insertConfig(_oneDriveCloudServiceConfig!); _oneDriveCloudService = OneDriveCloudService( - context, _oneDriveCloudServiceConfig!, onConfigChanged: updateConfig, ); @@ -335,6 +333,7 @@ class _OneDriveServiceScreenState extends State text: S.current.webDavLogout, fontSizeDelta: 2, onTap: () async { + CustomLoadingDialog.showLoading(title: S.current.webDavLoggingOut); await _oneDriveCloudService!.signOut(); setState(() { _oneDriveCloudServiceConfig!.connected = false; @@ -345,6 +344,8 @@ class _OneDriveServiceScreenState extends State _oneDriveCloudServiceConfig!.usedSize = -1; updateConfig(_oneDriveCloudServiceConfig!); }); + CustomLoadingDialog.dismissLoading(); + IToast.show(S.current.webDavLogoutSuccess); }, ), ), diff --git a/lib/Screens/Setting/about_setting_screen.dart b/lib/Screens/Setting/about_setting_screen.dart index 8bd41ff4..753c3f86 100644 --- a/lib/Screens/Setting/about_setting_screen.dart +++ b/lib/Screens/Setting/about_setting_screen.dart @@ -203,9 +203,31 @@ class _AboutSettingScreenState extends State UriUtil.launchUrlUri(context, repoUrl); }, showLeading: true, - bottomRadius: true, leading: Icons.commit_outlined, ), + ItemBuilder.buildEntryItem( + context: context, + title: S.current.privacyPolicy, + onTap: () { + Locale locale = Localizations.localeOf(context); + UriUtil.launchUrlUri( + context, privacyPolicyUrl + locale.languageCode); + }, + showLeading: true, + leading: Icons.privacy_tip_outlined, + ), + ItemBuilder.buildEntryItem( + context: context, + title: S.current.serviceTerm, + onTap: () { + Locale locale = Localizations.localeOf(context); + UriUtil.launchUrlUri( + context, serviceTermUrl + locale.languageCode); + }, + showLeading: true, + bottomRadius: true, + leading: Icons.topic_outlined, + ), const SizedBox(height: 10), ItemBuilder.buildEntryItem( topRadius: true, diff --git a/lib/Screens/home_screen.dart b/lib/Screens/home_screen.dart index b627fdc5..f438cc08 100644 --- a/lib/Screens/home_screen.dart +++ b/lib/Screens/home_screen.dart @@ -90,8 +90,8 @@ class HomeScreenState extends State with TickerProviderStateMixin { @override void initState() { super.initState(); - initAppName(); initTab(true); + initAppName(); refresh(true); _searchController.addListener(() { performSearch(_searchController.text); @@ -741,6 +741,19 @@ class HomeScreenState extends State with TickerProviderStateMixin { } _buildTab(TokenCategory? category) { + // { + // bool normalUserBold = false, + // bool sameFontSize = false, + // double fontSizeDelta = 0, + // }) { + // TextStyle normalStyle = Theme.of(context).textTheme.titleLarge!.apply( + // color: Colors.grey, + // fontSizeDelta: fontSizeDelta - (sameFontSize ? 0 : 1), + // fontWeightDelta: normalUserBold ? 0 : -2, + // ); + // TextStyle selectedStyle = Theme.of(context).textTheme.titleLarge!.apply( + // fontSizeDelta: fontSizeDelta + (sameFontSize ? 0 : 1), + // ); return Tab( child: ContextMenuRegion( behavior: ResponsiveUtil.isDesktop() @@ -757,8 +770,19 @@ class HomeScreenState extends State with TickerProviderStateMixin { ); } }, + // child: AnimatedDefaultTextStyle( + // style: (category == null + // ? _currentTabIndex == 0 + // : currentCategoryId == category.id) + // ? selectedStyle + // : normalStyle, + // duration: const Duration(milliseconds: 100), + // child: Container( + // alignment: Alignment.center, child: Text(category?.title ?? S.current.allTokens), ), + // ), + // ), ), ); } diff --git a/lib/TokenUtils/Cloud/dropbox_cloud_service.dart b/lib/TokenUtils/Cloud/dropbox_cloud_service.dart index 0701e83d..9c8d2977 100644 --- a/lib/TokenUtils/Cloud/dropbox_cloud_service.dart +++ b/lib/TokenUtils/Cloud/dropbox_cloud_service.dart @@ -1,13 +1,11 @@ import 'dart:typed_data'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_cloud/dropbox.dart'; import 'package:flutter_cloud/dropbox_response.dart'; import 'package:path/path.dart'; import '../../Models/cloud_service_config.dart'; import '../../Utils/hive_util.dart'; -import '../../generated/l10n.dart'; import '../export_token_util.dart'; import 'cloud_service.dart'; @@ -22,11 +20,9 @@ class DropboxCloudService extends CloudService { static const String _dropboxPath = '/'; final CloudServiceConfig _config; late Dropbox dropbox; - late BuildContext context; Function(CloudServiceConfig)? onConfigChanged; DropboxCloudService( - this.context, this._config, { this.onConfigChanged, }) { @@ -46,10 +42,7 @@ class DropboxCloudService extends CloudService { Future authenticate() async { bool isAuthorized = await dropbox.isConnected(); if (!isAuthorized) { - isAuthorized = await dropbox.connect( - context, - windowName: S.current.cloudTypeDropboxAuthenticateWindowName, - ); + isAuthorized = await dropbox.connect(); } if (isAuthorized) { DropboxUserInfo? info = await fetchInfo(); diff --git a/lib/TokenUtils/Cloud/googledrive_cloud_service.dart b/lib/TokenUtils/Cloud/googledrive_cloud_service.dart index 830eb586..585830ed 100644 --- a/lib/TokenUtils/Cloud/googledrive_cloud_service.dart +++ b/lib/TokenUtils/Cloud/googledrive_cloud_service.dart @@ -1,12 +1,10 @@ import 'dart:typed_data'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_cloud/googledrive.dart'; import 'package:flutter_cloud/googledrive_response.dart'; import '../../Models/cloud_service_config.dart'; import '../../Utils/hive_util.dart'; -import '../../generated/l10n.dart'; import '../export_token_util.dart'; import 'cloud_service.dart'; @@ -17,19 +15,17 @@ class GoogleDriveCloudService extends CloudService { 'https://apps.cloudchewie.com/oauth/cloudotp/googledrive/callback'; static const String _callbackUrl = 'cloudotp://auth/googledrive/callback'; static const String _clientId = - '631913875304-g9qfaoauakvbsuhqiqu85thlbg1ddep5.apps.googleusercontent.com'; - static const String _clientSecret = "XXXXXXXXXXXXXXXXXXXX"; + '547353482361-fi716v2qnfvh3aj515ok1r4cdqqhdqbh.apps.googleusercontent.com'; static const String _googledrivePath = '/CloudOTP'; static const String _googledrivePathName = 'CloudOTP'; final CloudServiceConfig _config; late GoogleDrive googledrive; - late BuildContext context; Function(CloudServiceConfig)? onConfigChanged; - GoogleDriveCloudService(this.context, - this._config, { - this.onConfigChanged, - }) { + GoogleDriveCloudService( + this._config, { + this.onConfigChanged, + }) { init(); } @@ -38,7 +34,6 @@ class GoogleDriveCloudService extends CloudService { googledrive = GoogleDrive( redirectUrl: _callbackUrl, callbackUrl: _callbackUrl, - clientSecret:_clientSecret, clientId: _clientId, ); } @@ -47,10 +42,7 @@ class GoogleDriveCloudService extends CloudService { Future authenticate() async { bool isAuthorized = await googledrive.isConnected(); if (!isAuthorized) { - isAuthorized = await googledrive.connect( - context, - windowName: S.current.cloudTypeGoogleDriveAuthenticateWindowName, - ); + isAuthorized = await googledrive.connect(); } if (isAuthorized) { await fetchInfo(); @@ -104,7 +96,8 @@ class GoogleDriveCloudService extends CloudService { } @override - Future downloadFile(String path, { + Future downloadFile( + String path, { Function(int p1, int p2)? onProgress, }) async { GoogleDriveResponse response = await googledrive.pullById(path); @@ -140,10 +133,11 @@ class GoogleDriveCloudService extends CloudService { } @override - Future uploadFile(String fileName, - Uint8List fileData, { - Function(int p1, int p2)? onProgress, - }) async { + Future uploadFile( + String fileName, + Uint8List fileData, { + Function(int p1, int p2)? onProgress, + }) async { GoogleDriveResponse response = await googledrive.push( fileData, fileName, diff --git a/lib/TokenUtils/Cloud/huawei_cloud_service.dart b/lib/TokenUtils/Cloud/huawei_cloud_service.dart index 46116f9a..0b0d666d 100644 --- a/lib/TokenUtils/Cloud/huawei_cloud_service.dart +++ b/lib/TokenUtils/Cloud/huawei_cloud_service.dart @@ -1,12 +1,10 @@ import 'dart:typed_data'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_cloud/huaweicloud.dart'; import 'package:flutter_cloud/huaweicloud_response.dart'; import '../../Models/cloud_service_config.dart'; import '../../Utils/hive_util.dart'; -import '../../generated/l10n.dart'; import '../export_token_util.dart'; import 'cloud_service.dart'; @@ -17,16 +15,13 @@ class HuaweiCloudService extends CloudService { 'https://apps.cloudchewie.com/oauth/cloudotp/huaweicloud/callback'; static const String _callbackUrl = "cloudotp://auth/huaweicloud/callback"; static const String _clientId = '111829035'; - static const String _clientSecret = 'XXXXXXXXXXXXXXXXXXXXX'; static const String _huaweiCloudEmptyPath = ''; static const String _huaweiCloudPath = 'CloudOTP'; final CloudServiceConfig _config; late HuaweiCloud huaweiCloud; - late BuildContext context; Function(CloudServiceConfig)? onConfigChanged; HuaweiCloudService( - this.context, this._config, { this.onConfigChanged, }) { @@ -39,7 +34,6 @@ class HuaweiCloudService extends CloudService { redirectUrl: _redirectUrl, callbackUrl: _callbackUrl, clientId: _clientId, - clientSecret: _clientSecret, ); } @@ -47,10 +41,7 @@ class HuaweiCloudService extends CloudService { Future authenticate() async { bool isAuthorized = await huaweiCloud.isConnected(); if (!isAuthorized) { - isAuthorized = await huaweiCloud.connect( - context, - windowName: S.current.cloudTypeHuaweiCloudAuthenticateWindowName, - ); + isAuthorized = await huaweiCloud.connect(); } if (isAuthorized) { HuaweiCloudUserInfo? info = await fetchInfo(); diff --git a/lib/TokenUtils/Cloud/onedrive_cloud_service.dart b/lib/TokenUtils/Cloud/onedrive_cloud_service.dart index eec25731..bd68b459 100644 --- a/lib/TokenUtils/Cloud/onedrive_cloud_service.dart +++ b/lib/TokenUtils/Cloud/onedrive_cloud_service.dart @@ -1,13 +1,11 @@ import 'dart:typed_data'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_cloud/onedrive.dart'; import 'package:flutter_cloud/onedrive_response.dart'; import 'package:path/path.dart'; import '../../Models/cloud_service_config.dart'; import '../../Utils/hive_util.dart'; -import '../../generated/l10n.dart'; import '../export_token_util.dart'; import 'cloud_service.dart'; @@ -21,11 +19,9 @@ class OneDriveCloudService extends CloudService { static const String _onedrivePath = '/CloudOTP'; final CloudServiceConfig _config; late OneDrive onedrive; - late BuildContext context; Function(CloudServiceConfig)? onConfigChanged; OneDriveCloudService( - this.context, this._config, { this.onConfigChanged, }) { @@ -45,10 +41,7 @@ class OneDriveCloudService extends CloudService { Future authenticate() async { bool isAuthorized = await onedrive.isConnected(); if (!isAuthorized) { - isAuthorized = await onedrive.connect( - context, - windowName: S.current.cloudTypeOneDriveAuthenticateWindowName, - ); + isAuthorized = await onedrive.connect(); } if (isAuthorized) { await fetchInfo(); diff --git a/lib/Utils/constant.dart b/lib/Utils/constant.dart index 1fefea09..c644de12 100644 --- a/lib/Utils/constant.dart +++ b/lib/Utils/constant.dart @@ -34,6 +34,9 @@ const String repoUrl = "https://github.com/Robert-Stackflow/CloudOTP"; const String releaseUrl = "https://github.com/Robert-Stackflow/CloudOTP/releases"; const String issueUrl = "https://github.com/Robert-Stackflow/CloudOTP/issues"; +const String privacyPolicyUrl = + "https://apps.cloudchewie.com/cloudotp/privacy/"; +const String serviceTermUrl = "https://apps.cloudchewie.com/cloudotp/service/"; AndroidAuthMessages androidAuthMessages = AndroidAuthMessages( cancelButton: S.current.biometricCancelButton, diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index fd27abc7..062aca62 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -243,6 +243,8 @@ "webDavPushBackup": "Push backups", "webDavBackupFiles": "Backup list (total {count} backups)", "webDavLogout": "Logout", + "webDavLoggingOut": "Logging out...", + "webDavLogoutSuccess": "Logout successful", "webDavSignin": "Sign in", "s3Endpoint": "Endpoint", "s3EndpointTip": "S3 cloud service endpoint", @@ -371,6 +373,8 @@ "changeLog": "Change log", "bugReport": "Report BUG", "githubRepo": "GitHub repository", + "privacyPolicy": "Privacy policy", + "serviceTerm": "Terms of service", "rate": "Rate it", "rateTitle": "Rate CloudOTP", "pleaseRate": "Please rate", diff --git a/lib/l10n/intl_zh_CN.arb b/lib/l10n/intl_zh_CN.arb index a6d7dcf0..88d6a62e 100644 --- a/lib/l10n/intl_zh_CN.arb +++ b/lib/l10n/intl_zh_CN.arb @@ -242,6 +242,8 @@ "webDavPullFailed": "拉取失败", "webDavPushBackup": "备份到云端", "webDavLogout": "退出帐户", + "webDavLoggingOut": "退出中...", + "webDavLogoutSuccess": "退出成功", "webDavSignin": "登录", "s3Endpoint": "端点", "s3EndpointTip": "S3云服务端点", @@ -370,6 +372,8 @@ "changeLog": "更新日志", "bugReport": "报告BUG", "githubRepo": "GitHub仓库", + "privacyPolicy": "隐私政策", + "serviceTerm": "服务条款", "rate": "评个分吧", "rateTitle": "为CloudOTP评个分吧", "pleaseRate": "请评分", diff --git a/pubspec.yaml b/pubspec.yaml index 1043e62a..76cced69 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,5 @@ name: cloudotp -version: 2.2.0 -description: An awesome two-factor authenticator which supports cloud storage and multiple platforms. +dddddescription: An awesome two-factor authenticator which supports cloud storage and multiple platforms. publish_to: none environment: diff --git a/third-party/flutter_cloud/lib/dropbox.dart b/third-party/flutter_cloud/lib/dropbox.dart index 64de3246..82a0a475 100644 --- a/third-party/flutter_cloud/lib/dropbox.dart +++ b/third-party/flutter_cloud/lib/dropbox.dart @@ -6,26 +6,27 @@ import 'dart:convert'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_cloud/status.dart'; import 'package:flutter_cloud/token_manager.dart'; -import 'package:hashlib/hashlib.dart'; import 'package:http/http.dart' as http; import 'dropbox_response.dart'; import 'oauth2_helper.dart'; class Dropbox with ChangeNotifier { - static const String authHost = "www.dropbox.com"; - static const String authEndpoint = "/oauth2/authorize"; - static const String tokenEndpoint = "https://$authHost/oauth2/token"; + static const String authEndpoint = "https://www.dropbox.com/oauth2/authorize"; + static const String tokenEndpoint = "https://www.dropbox.com/oauth2/token"; + static const String revokeEndpoint = + "https://api.dropboxapi.com/2/auth/token/revoke"; static const String apiContentEndpoint = "https://content.dropboxapi.com/2"; static const String apiEndpoint = "https://api.dropboxapi.com/2"; - static const String errCANCELED = "CANCELED"; - static const permissionFilesReadWriteAll = + static const permission = "account_info.read files.metadata.write files.metadata.read files.content.write files.content.read file_requests.write file_requests.read"; static const String expireInKey = "__dropbox_tokenExpire"; static const String accessTokenKey = "__dropbox_accessToken"; static const String refreshTokenKey = "__dropbox_refreshToken"; + static const String idTokenKey = "__dropbox_idToken"; late final ITokenManager _tokenManager; late final String redirectUrl; @@ -38,7 +39,7 @@ class Dropbox with ChangeNotifier { required this.clientId, required this.callbackUrl, required this.redirectUrl, - this.scopes = permissionFilesReadWriteAll, + this.scopes = permission, ITokenManager? tokenManager, }) { state = OAuth2Helper.generateStateParameter(); @@ -47,10 +48,12 @@ class Dropbox with ChangeNotifier { tokenEndpoint: tokenEndpoint, clientId: clientId, redirectUrl: redirectUrl, + revokeUrl: revokeEndpoint, scope: scopes, - expireInKey: expireInKey, + expireAtKey: expireInKey, accessTokenKey: accessTokenKey, refreshTokenKey: refreshTokenKey, + idTokenKey: idTokenKey, ); } @@ -64,27 +67,14 @@ class Dropbox with ChangeNotifier { return (accessToken?.isNotEmpty) ?? false; } - String generateCodeVerifier() { - return myBase64Encode(randomBytes(32)); - } - - String myBase64Encode(List input) { - return base64Encode(input) - .replaceAll("+", '-') - .replaceAll("/", '_') - .replaceAll("=", ''); - } - - Future connect( - BuildContext context, { - String? windowName, - }) async { + Future connect() async { try { - String codeVerifier = generateCodeVerifier(); + String codeVerifier = OAuth2Helper.generateCodeVerifier(); - String codeChanllenge = myBase64Encode(sha256.string(codeVerifier).bytes); + String codeChanllenge = OAuth2Helper.generateCodeChanllenge(codeVerifier); - final authUri = Uri.https(authHost, authEndpoint, { + Uri uri = Uri.parse(authEndpoint); + final authUri = Uri.https(uri.authority, uri.path, { 'code_challenge': codeChanllenge, "code_challenge_method": "S256", 'client_id': clientId, @@ -106,7 +96,6 @@ class Dropbox with ChangeNotifier { } http.Response? result = await OAuth2Helper.browserAuthWithVerifier( - context: context, authEndpoint: authUri, tokenEndpoint: Uri.parse(tokenEndpoint), callbackUrl: callbackUrl, @@ -115,7 +104,6 @@ class Dropbox with ChangeNotifier { redirectUrl: redirectUrl, codeVerifier: codeVerifier, scopes: scopes, - windowName: windowName, state: state, ); @@ -127,10 +115,7 @@ class Dropbox with ChangeNotifier { } else { return false; } - } on PlatformException catch (err, trace) { - if (err.code != errCANCELED) { - debugPrint("# Dropbox -> connect: $err\n$trace"); - } + } on PlatformException { return false; } catch (err, trace) { debugPrint("# Dropbox -> connect: $err\n$trace"); @@ -139,85 +124,121 @@ class Dropbox with ChangeNotifier { } Future disconnect() async { - await _tokenManager.clearStoredToken(); - notifyListeners(); + try { + final accessToken = await checkToken(); + Uri uri = Uri.parse(revokeEndpoint); + final resp = await http.post( + uri, + headers: {"Authorization": "Bearer $accessToken"}, + ); + debugPrint( + "# Dropbox -> disconnect: revoke access token: ${resp.statusCode}"); + } catch (err, trace) { + debugPrint("# Dropbox -> disconnect: $err\n$trace"); + } finally { + await _tokenManager.clearStoredToken(); + notifyListeners(); + } } - Future getInfo() async { + Future checkToken() async { final accessToken = await _tokenManager.getAccessToken(); if (accessToken == null) { - return DropboxResponse( - message: "Null access token", bodyBytes: Uint8List(0)); + throw NullAccessTokenException(); + } else { + return accessToken; + } + } + + http.Response processResponse(http.Response response) { + if (response.statusCode == 401) { + disconnect(); } - final url = Uri.parse("$apiEndpoint/users/get_current_account"); - final storageUrl = Uri.parse("$apiEndpoint/users/get_space_usage"); + return response; + } + + Future post( + Uri url, { + dynamic headers, + dynamic body, + }) async { + final accessToken = await checkToken(); + var tmpHeaders = {"Authorization": "Bearer $accessToken"}; + if (headers != null) { + tmpHeaders.addAll(headers); + } + return processResponse(await http.post( + url, + headers: tmpHeaders, + body: body, + )); + } + Future get( + Uri url, { + dynamic headers, + }) async { + final accessToken = await checkToken(); + var tmpHeaders = {"Authorization": "Bearer $accessToken"}; + if (headers != null) { + tmpHeaders.addAll(headers); + } + return processResponse(await http.get( + url, + headers: tmpHeaders, + )); + } + + Future getInfo() async { try { - final resp = await http.post( - url, - headers: {"Authorization": "Bearer $accessToken"}, - ); + final url = Uri.parse("$apiEndpoint/users/get_current_account"); + final storageUrl = Uri.parse("$apiEndpoint/users/get_space_usage"); - debugPrint( - "# Dropbox -> getInfo: ${resp.statusCode}\n# Body: ${resp.body}"); + final resp = await post(url); if (resp.statusCode == 200 || resp.statusCode == 201) { - final usageResp = await http.post( - storageUrl, - headers: {"Authorization": "Bearer $accessToken"}, - ); + debugPrint("# Dropbox -> getInfo success: ${jsonDecode(resp.body)}"); - debugPrint( - "# Dropbox -> getStorageInfo: ${usageResp.statusCode}\n# Body: ${usageResp.body}"); + final usageResp = await post(storageUrl); if (usageResp.statusCode == 200 || usageResp.statusCode == 201) { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - userInfo: DropboxUserInfo.fromJson( - jsonDecode(resp.body), jsonDecode(usageResp.body)), - message: "Get Info successfully.", - bodyBytes: resp.bodyBytes, - isSuccess: true); + debugPrint( + "# Dropbox -> getStorageInfo success: ${jsonDecode(usageResp.body)}"); + + return DropboxResponse.fromResponse( + response: usageResp, + userInfo: DropboxUserInfo.fromJson( + jsonDecode(resp.body), jsonDecode(usageResp.body)), + message: "Get Info successfully.", + ); } else { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Error while get storage info.", - bodyBytes: Uint8List(0)); + debugPrint( + "# Dropbox -> getStorageInfo failed: ${usageResp.statusCode} # Body: ${usageResp.body}"); + return DropboxResponse.fromResponse( + response: usageResp, + message: "Error while get storage info.", + ); } - } else if (resp.statusCode == 404) { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "${url.toString()} not found.", - bodyBytes: Uint8List(0)); } else { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Error while get info.", - bodyBytes: Uint8List(0)); + debugPrint( + "# Dropbox -> getInfo failed: ${resp.statusCode} # Body: ${resp.body}"); + return DropboxResponse.fromResponse( + response: resp, + message: "Error while get info.", + ); } } catch (err) { - debugPrint("# Dropbox -> getInfo: $err"); + debugPrint("# Dropbox -> getInfo error: $err"); return DropboxResponse(message: "Unexpected exception: $err"); } } Future list(String remotePath) async { - final accessToken = await _tokenManager.getAccessToken(); - if (accessToken == null) { - return DropboxResponse(message: "Null access token"); - } - - final url = Uri.parse("$apiEndpoint/files/list_folder"); - try { - final resp = await http.post( + final url = Uri.parse("$apiEndpoint//files/list_folder"); + final resp = await post( url, headers: { - "Authorization": "Bearer $accessToken", "Content-Type": "application/json", }, body: jsonEncode({ @@ -231,8 +252,6 @@ class Dropbox with ChangeNotifier { }), ); - debugPrint("# Dropbox -> list: ${resp.statusCode}\n# Body: ${resp.body}"); - if (resp.statusCode == 200 || resp.statusCode == 201) { Map body = jsonDecode(resp.body); List files = []; @@ -240,26 +259,22 @@ class Dropbox with ChangeNotifier { if (item['.tag'] == "folder") continue; files.add(DropboxFileInfo.fromJson(item)); } - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - files: files, - message: "List files successfully.", - bodyBytes: resp.bodyBytes, - isSuccess: true); - } else if (resp.statusCode == 404) { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Url not found."); + debugPrint("# Dropbox -> list successfully"); + return DropboxResponse.fromResponse( + response: resp, + files: files, + message: "List files successfully.", + ); } else { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Error while listing files."); + debugPrint( + "# Dropbox -> list failed: ${resp.statusCode}\n# Body: ${resp.body}"); + return DropboxResponse.fromResponse( + response: resp, + message: "Error while listing files.", + ); } } catch (err) { - debugPrint("# Dropbox -> list: $err"); + debugPrint("# Dropbox -> list error: $err"); return DropboxResponse(message: "Unexpected exception: $err"); } } @@ -267,46 +282,31 @@ class Dropbox with ChangeNotifier { Future pull( String path, ) async { - final accessToken = await _tokenManager.getAccessToken(); - if (accessToken == null) { - return DropboxResponse( - message: "Null access token", bodyBytes: Uint8List(0)); - } - - final url = Uri.parse("$apiContentEndpoint/files/download"); - try { - final resp = await http.get( + final url = Uri.parse("$apiContentEndpoint/files/download"); + + final resp = await get( url, headers: { - "Authorization": "Bearer $accessToken", "Dropbox-API-Arg": jsonEncode({ "path": path, }), }, ); - debugPrint("# Dropbox -> pull: ${resp.statusCode}\n# Body: ${resp.body}"); - if (resp.statusCode == 200 || resp.statusCode == 201) { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Download successfully.", - bodyBytes: resp.bodyBytes, - isSuccess: true); - } else if (resp.statusCode == 404) { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "File not found.", - bodyBytes: Uint8List(0)); + debugPrint("# Dropbox -> pull successfully"); + return DropboxResponse.fromResponse( + response: resp, + message: "Download successfully.", + ); } else { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Error while downloading file.", - bodyBytes: Uint8List(0)); + debugPrint( + "# Dropbox -> pull failed : ${resp.statusCode}\n# Body: ${resp.body}"); + return DropboxResponse.fromResponse( + response: resp, + message: "Error while downloading file.", + ); } } catch (err) { debugPrint("# Dropbox -> pull: $err"); @@ -315,67 +315,44 @@ class Dropbox with ChangeNotifier { } Future delete(String path) async { - final accessToken = await _tokenManager.getAccessToken(); - if (accessToken == null) { - return DropboxResponse( - message: "Null access token", bodyBytes: Uint8List(0)); - } - - final url = Uri.parse("$apiEndpoint/files/delete_v2"); - try { - final resp = await http.post( + final url = Uri.parse("$apiEndpoint/files/delete_v2"); + + final resp = await post( url, headers: { - "Authorization": "Bearer $accessToken", "Content-Type": "application/json", }, body: jsonEncode({"path": path}), ); - debugPrint( - "# Dropbox -> delete: ${resp.statusCode}\n# Body: ${resp.body}"); - if (resp.statusCode == 200 || resp.statusCode == 201) { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Delete successfully.", - bodyBytes: resp.bodyBytes, - isSuccess: true); - } else if (resp.statusCode == 404) { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "File not found.", - bodyBytes: Uint8List(0)); + debugPrint("# Dropbox -> delete successfully"); + return DropboxResponse.fromResponse( + response: resp, + message: "Delete successfully.", + ); } else { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Error while deleting file.", - bodyBytes: Uint8List(0)); + debugPrint( + "# Dropbox -> delete failed ${resp.statusCode}\n# Body: ${resp.body}"); + return DropboxResponse.fromResponse( + response: resp, + message: "Error while deleting file.", + ); } } catch (err) { - debugPrint("# Dropbox -> delete: $err"); + debugPrint("# Dropbox -> delete error: $err"); return DropboxResponse(message: "Unexpected exception: $err"); } } Future deleteBatch(List paths) async { - final accessToken = await _tokenManager.getAccessToken(); - if (accessToken == null) { - return DropboxResponse( - message: "Null access token", bodyBytes: Uint8List(0)); - } - - final url = Uri.parse("$apiEndpoint/files/delete_batch"); - try { - final resp = await http.post( + final url = Uri.parse("$apiEndpoint/files/delete_batch"); + + final resp = await post( url, headers: { - "Authorization": "Bearer $accessToken", "Content-Type": "application/json", }, body: jsonEncode( @@ -385,31 +362,22 @@ class Dropbox with ChangeNotifier { ), ); - debugPrint( - "# Dropbox -> deleteBatch: ${resp.statusCode}\n# Body: ${resp.body}"); - if (resp.statusCode == 200 || resp.statusCode == 201) { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Delete successfully.", - bodyBytes: resp.bodyBytes, - isSuccess: true); - } else if (resp.statusCode == 404) { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "File not found.", - bodyBytes: Uint8List(0)); + debugPrint("# Dropbox -> deleteBatch successfully"); + return DropboxResponse.fromResponse( + response: resp, + message: "Delete batch successfully.", + ); } else { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Error while deleting file.", - bodyBytes: Uint8List(0)); + debugPrint( + "# Dropbox -> deleteBatch failed: ${resp.statusCode}\n# Body: ${resp.body}"); + return DropboxResponse.fromResponse( + response: resp, + message: "Error while deleting file.", + ); } } catch (err) { - debugPrint("# Dropbox -> deleteBatch: $err"); + debugPrint("# Dropbox -> deleteBatch error: $err"); return DropboxResponse(message: "Unexpected exception: $err"); } } @@ -419,18 +387,12 @@ class Dropbox with ChangeNotifier { String remotePath, { Function(int p1, int p2)? onProgress, }) async { - final accessToken = await _tokenManager.getAccessToken(); - if (accessToken == null) { - return DropboxResponse(message: "Null access token."); - } - try { var url = Uri.parse("$apiContentEndpoint/files/upload"); - var resp = await http.post( + var resp = await post( url, headers: { - "Authorization": "Bearer $accessToken", "Dropbox-API-Arg": jsonEncode({ "autorename": false, "mode": "add", @@ -443,23 +405,23 @@ class Dropbox with ChangeNotifier { body: bytes, ); - debugPrint("# Upload response: ${resp.statusCode}\n# Body: ${resp.body}"); if (resp.statusCode == 200 || resp.statusCode == 201) { onProgress?.call(1, 1); - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Upload finished.", - isSuccess: true); + debugPrint("# Dropbox -> Upload successfully"); + return DropboxResponse.fromResponse( + response: resp, + message: "Upload finished.", + ); } else { - return DropboxResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Upload failed."); + debugPrint("# Dropbox -> Upload failed: ${resp.statusCode}\n# Body: ${resp.body}"); + return DropboxResponse.fromResponse( + response: resp, + message: "Upload failed.", + ); } } catch (err) { - debugPrint("# Upload error: $err"); + debugPrint("# Dropbox -> Upload error: $err"); return DropboxResponse(message: "Unexpected exception: $err"); } } diff --git a/third-party/flutter_cloud/lib/dropbox_response.dart b/third-party/flutter_cloud/lib/dropbox_response.dart index 6f3dc0b2..d362766c 100644 --- a/third-party/flutter_cloud/lib/dropbox_response.dart +++ b/third-party/flutter_cloud/lib/dropbox_response.dart @@ -1,24 +1,47 @@ import 'dart:typed_data'; +import 'package:flutter_cloud/status.dart'; +import 'package:http/http.dart' as http; + class DropboxResponse { + final ResponseStatus status; final int? statusCode; final String? body; final String? message; + final String? accessToken; final bool isSuccess; final Uint8List? bodyBytes; final DropboxUserInfo? userInfo; final List files; DropboxResponse({ + this.status = ResponseStatus.success, this.statusCode, this.body, - this.message, this.bodyBytes, + this.accessToken, this.userInfo, + this.message, this.files = const [], this.isSuccess = false, }); + DropboxResponse.fromResponse({ + required http.Response response, + this.userInfo, + this.message, + this.files = const [], + }) : body = response.body, + accessToken = "", + statusCode = response.statusCode, + bodyBytes = response.bodyBytes, + isSuccess = response.statusCode == 200 || + response.statusCode == 201 || + response.statusCode == 204, + status = ResponseStatus.values.firstWhere( + (element) => element.code == response.statusCode, + orElse: () => ResponseStatus.success); + @override String toString() { return "DropboxResponse(" diff --git a/third-party/flutter_cloud/lib/googledrive.dart b/third-party/flutter_cloud/lib/googledrive.dart index d4ced71b..fe70e370 100644 --- a/third-party/flutter_cloud/lib/googledrive.dart +++ b/third-party/flutter_cloud/lib/googledrive.dart @@ -7,8 +7,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_cloud/token_manager.dart'; +// import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis/drive/v3.dart' as drive; -import 'package:hashlib/hashlib.dart'; import 'package:http/http.dart' as http; import 'googledrive_response.dart'; @@ -28,32 +28,32 @@ class GoogleAuthClient extends http.BaseClient { } class GoogleDrive with ChangeNotifier { - static const String authHost = "accounts.google.com"; - static const String authEndpoint = "/o/oauth2/v2/auth"; + static const String authEndpoint = + "https://accounts.google.com/o/oauth2/v2/auth"; static const String tokenEndpoint = "https://www.googleapis.com/oauth2/v4/token"; + static const String revokeEndpoint = + "https://www.googleapis.com/oauth2/v4/revoke"; static const String apiEndpoint = "https://content.googleapis.com/drive/v3"; - static const String apiUpload = + static const String apiUploadEndpoint = "https://www.googleapis.com/upload/drive/v3/files"; - static const String errCANCELED = "CANCELED"; static const permission = "https://www.googleapis.com/auth/drive.file"; static const String expireInKey = "__googledrive_tokenExpire"; static const String accessTokenKey = "__googledrive_accessToken"; static const String refreshTokenKey = "__googledrive_refreshToken"; + static const String idTokenKey = "__googledrive_idToken"; late final ITokenManager _tokenManager; late final String redirectUrl; late final String callbackUrl; final String scopes; final String clientId; - final String clientSecret; late final String state; GoogleDrive({ required this.clientId, - required this.clientSecret, required this.callbackUrl, required this.redirectUrl, this.scopes = permission, @@ -65,10 +65,12 @@ class GoogleDrive with ChangeNotifier { tokenEndpoint: tokenEndpoint, clientId: clientId, redirectUrl: redirectUrl, + revokeUrl: revokeEndpoint, scope: scopes, - expireInKey: expireInKey, + expireAtKey: expireInKey, accessTokenKey: accessTokenKey, refreshTokenKey: refreshTokenKey, + idTokenKey: idTokenKey, ); } @@ -92,32 +94,38 @@ class GoogleDrive with ChangeNotifier { return (accessToken?.isNotEmpty) ?? false; } - String generateCodeVerifier() { - return myBase64Encode(randomBytes(32)); - } - - String myBase64Encode(List input) { - return base64Encode(input) - .replaceAll("+", '-') - .replaceAll("/", '_') - .replaceAll("=", ''); + Future connect() async { + // GoogleSignIn googleSignIn = GoogleSignIn( + // clientId: clientId, + // scopes: [permission], + // forceCodeForRefreshToken: true, + // signInOption: SignInOption.standard, + // ); + // try { + // GoogleSignInAccount? currentUser = await googleSignIn.signIn(); + // GoogleSignInAuthentication? authentication = + // await currentUser?.authentication; + // print(authentication?.accessToken); + // return true; + // } catch (e, t) { + // print("$e\n$t"); + return false; + // } } - Future connect( - BuildContext context, { - String? windowName, - }) async { + Future connects() async { try { - String codeVerifier = generateCodeVerifier(); + String codeVerifier = OAuth2Helper.generateCodeVerifier(); - String codeChanllenge = myBase64Encode(sha256.string(codeVerifier).bytes); + String codeChanllenge = OAuth2Helper.generateCodeChanllenge(codeVerifier); - final authUri = Uri.https(authHost, authEndpoint, { + Uri uri = Uri.parse(authEndpoint); + final authUri = Uri.https(uri.authority, uri.path, { 'code_challenge': codeChanllenge, "code_challenge_method": "S256", - 'response_type': 'code', 'client_id': clientId, 'redirect_uri': redirectUrl, + 'response_type': 'code', "access_type": "offline", 'scope': scopes, 'state': state, @@ -134,31 +142,25 @@ class GoogleDrive with ChangeNotifier { } http.Response? result = await OAuth2Helper.browserAuthWithVerifier( - context: context, authEndpoint: authUri, tokenEndpoint: Uri.parse(tokenEndpoint), callbackUrl: callbackUrl, callbackUrlScheme: callbackUrlScheme, state: state, clientId: clientId, - clientSecret: clientSecret, codeVerifier: codeVerifier, redirectUrl: redirectUrl, scopes: scopes, - windowName: windowName, ); if (result != null) { notifyListeners(); - bool res = (await _tokenManager.saveTokenResp(result)) as bool; + bool res = (await _tokenManager.saveTokenResp(result)); return res; } else { return false; } - } on PlatformException catch (err) { - if (err.code != errCANCELED) { - debugPrint("# GoogleDrive -> connect: $err"); - } + } on PlatformException { return false; } catch (err) { debugPrint("# GoogleDrive -> connect: $err"); diff --git a/third-party/flutter_cloud/lib/huaweicloud.dart b/third-party/flutter_cloud/lib/huaweicloud.dart index 396e0ab7..f55812a8 100644 --- a/third-party/flutter_cloud/lib/huaweicloud.dart +++ b/third-party/flutter_cloud/lib/huaweicloud.dart @@ -28,19 +28,18 @@ class HuaweiCloud with ChangeNotifier { static const String expireInKey = "__huaweicloud_tokenExpire"; static const String accessTokenKey = "__huaweicloud_accessToken"; static const String refreshTokenKey = "__huaweicloud_refreshToken"; + static const String idTokenKey = "__huaweicloud_idToken"; late final ITokenManager _tokenManager; late final String redirectUrl; late final String callbackUrl; final String scopes; final String clientId; - final String clientSecret; late final String state; HuaweiCloud({ required this.clientId, - required this.clientSecret, required this.redirectUrl, required this.callbackUrl, this.scopes = permission, @@ -49,14 +48,15 @@ class HuaweiCloud with ChangeNotifier { state = OAuth2Helper.generateStateParameter(); _tokenManager = tokenManager ?? DefaultTokenManager( - tokenEndpoint: tokenEndpoint, clientId: clientId, - clientSecret: clientSecret, redirectUrl: redirectUrl, + revokeUrl: revokeEndpoint, + tokenEndpoint: tokenEndpoint, scope: scopes, - expireInKey: expireInKey, + expireAtKey: expireInKey, accessTokenKey: accessTokenKey, refreshTokenKey: refreshTokenKey, + idTokenKey: idTokenKey, ); } @@ -70,18 +70,21 @@ class HuaweiCloud with ChangeNotifier { return (accessToken?.isNotEmpty) ?? false; } - Future connect( - BuildContext context, { - String? windowName, - }) async { + Future connect() async { try { + String codeVerifier = OAuth2Helper.generateCodeVerifier(); + + String codeChanllenge = OAuth2Helper.generateCodeChanllenge(codeVerifier); + final authUri = Uri.https(authHost, authEndpoint, { - 'response_type': 'code', + 'code_challenge': codeChanllenge, + "code_challenge_method": "S256", 'client_id': clientId, 'redirect_uri': redirectUrl, + 'response_type': 'code', + "access_type": "offline", 'scope': scopes, 'state': state, - "access_type": "offline", }); String callbackUrlScheme = ""; @@ -94,18 +97,16 @@ class HuaweiCloud with ChangeNotifier { callbackUrlScheme = callbackUri.toString(); } - http.Response? result = await OAuth2Helper.browserAuth( - context: context, + http.Response? result = await OAuth2Helper.browserAuthWithVerifier( authEndpoint: authUri, tokenEndpoint: Uri.parse(tokenEndpoint), - callbackUrl: callbackUrl, - callbackUrlScheme: callbackUrlScheme, state: state, clientId: clientId, - clientSecret: clientSecret, - redirectUrl: redirectUrl, scopes: scopes, - windowName: windowName, + redirectUrl: redirectUrl, + callbackUrl: callbackUrl, + callbackUrlScheme: callbackUrlScheme, + codeVerifier: codeVerifier, ); if (result != null) { await _tokenManager.saveTokenResp(result); diff --git a/third-party/flutter_cloud/lib/oauth2_helper.dart b/third-party/flutter_cloud/lib/oauth2_helper.dart index 48232ed3..7f91cd34 100644 --- a/third-party/flutter_cloud/lib/oauth2_helper.dart +++ b/third-party/flutter_cloud/lib/oauth2_helper.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; +import 'package:hashlib/hashlib.dart'; import 'package:http/http.dart' as http; class OAuth2Helper { @@ -13,8 +14,22 @@ class OAuth2Helper { return base64Url.encode(values); } + static String generateCodeVerifier() { + return myBase64Encode(randomBytes(32)); + } + + static String myBase64Encode(List input) { + return base64Encode(input) + .replaceAll("+", '-') + .replaceAll("/", '_') + .replaceAll("=", ''); + } + + static String generateCodeChanllenge(String codeVerifier){ + return myBase64Encode(sha256.string(codeVerifier).bytes); + } + static Future browserAuth({ - required BuildContext? context, required Uri authEndpoint, required Uri tokenEndpoint, required String callbackUrl, @@ -22,8 +37,6 @@ class OAuth2Helper { required String clientId, required String redirectUrl, required String state, - String? clientSecret, - String? windowName, String? scopes, }) async { try { @@ -31,9 +44,8 @@ class OAuth2Helper { url: authEndpoint.toString(), callbackUrl: callbackUrl, callbackUrlScheme: callbackUrlScheme, - options: FlutterWebAuth2Options( + options: const FlutterWebAuth2Options( timeout: 60, - windowName: windowName, useWebview: false, ), ); @@ -49,9 +61,6 @@ class OAuth2Helper { 'grant_type': 'authorization_code', 'code': code, }; - if (clientSecret != null && clientSecret.isNotEmpty) { - body['client_secret'] = clientSecret; - } http.Response resp = await http.post(tokenEndpoint, body: body); return resp; } catch (e, t) { @@ -61,7 +70,6 @@ class OAuth2Helper { } static Future browserAuthWithVerifier({ - required BuildContext? context, required Uri authEndpoint, required Uri tokenEndpoint, required String callbackUrl, @@ -70,8 +78,6 @@ class OAuth2Helper { required String redirectUrl, required String codeVerifier, required String state, - String? clientSecret, - String? windowName, String? scopes, }) async { try { @@ -79,9 +85,8 @@ class OAuth2Helper { url: authEndpoint.toString(), callbackUrl: callbackUrl, callbackUrlScheme: callbackUrlScheme, - options: FlutterWebAuth2Options( + options: const FlutterWebAuth2Options( timeout: 60, - windowName: windowName, useWebview: false, ), ); @@ -98,9 +103,6 @@ class OAuth2Helper { 'grant_type': 'authorization_code', 'code': code, }; - if (clientSecret != null && clientSecret.isNotEmpty) { - body['client_secret'] = clientSecret; - } http.Response resp = await http.post(tokenEndpoint, body: body); return resp; } catch (e, t) { diff --git a/third-party/flutter_cloud/lib/onedrive.dart b/third-party/flutter_cloud/lib/onedrive.dart index 28605af7..9c3ee585 100644 --- a/third-party/flutter_cloud/lib/onedrive.dart +++ b/third-party/flutter_cloud/lib/onedrive.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_cloud/status.dart'; import 'package:flutter_cloud/token_manager.dart'; import 'package:http/http.dart' as http; @@ -14,20 +15,21 @@ import 'oauth2_helper.dart'; import 'onedrive_response.dart'; class OneDrive with ChangeNotifier { - static const String authHost = "login.microsoftonline.com"; - static const String authEndpoint = "/consumers/oauth2/v2.0/authorize"; + static const String authEndpoint = + "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"; static const String tokenEndpoint = - "https://$authHost/consumers/oauth2/v2.0/token"; - static const String apiEndpoint = "https://graph.microsoft.com/v1.0/"; - static const String errCANCELED = "CANCELED"; + "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + static const String revokeEndpoint = + "https://login.microsoftonline.com/consumers/oauth2/v2.0/revoke"; + static const String apiEndpoint = "https://graph.microsoft.com/v1.0"; static const _appRootFolder = "special/approot"; static const _defaultRootFolder = "root"; - static const permissionFilesReadWriteAll = "Files.ReadWrite.All"; - static const permissionOfflineAccess = "offline_access"; + static const permission = "Files.ReadWrite.All offline_access"; static const String expireInKey = "__onedrive_tokenExpire"; static const String accessTokenKey = "__onedrive_accessToken"; static const String refreshTokenKey = "__onedrive_refreshToken"; + static const String idTokenKey = "__onedrive_idToken"; late final ITokenManager _tokenManager; late final String redirectUrl; @@ -41,7 +43,7 @@ class OneDrive with ChangeNotifier { required this.clientId, required this.callbackUrl, required this.redirectUrl, - this.scopes = "$permissionFilesReadWriteAll $permissionOfflineAccess", + this.scopes = permission, ITokenManager? tokenManager, }) { state = OAuth2Helper.generateStateParameter(); @@ -50,10 +52,12 @@ class OneDrive with ChangeNotifier { tokenEndpoint: tokenEndpoint, clientId: clientId, redirectUrl: redirectUrl, + revokeUrl: revokeEndpoint, scope: scopes, - expireInKey: expireInKey, + expireAtKey: expireInKey, accessTokenKey: accessTokenKey, refreshTokenKey: refreshTokenKey, + idTokenKey: idTokenKey, ); } @@ -67,12 +71,16 @@ class OneDrive with ChangeNotifier { return (accessToken?.isNotEmpty) ?? false; } - Future connect( - BuildContext context, { - String? windowName, - }) async { + Future connect() async { try { - final authUri = Uri.https(authHost, authEndpoint, { + String codeVerifier = OAuth2Helper.generateCodeVerifier(); + + String codeChanllenge = OAuth2Helper.generateCodeChanllenge(codeVerifier); + + Uri uri = Uri.parse(authEndpoint); + final authUri = Uri.https(uri.authority, uri.path, { + 'code_challenge': codeChanllenge, + "code_challenge_method": "S256", 'response_type': 'code', 'client_id': clientId, 'redirect_uri': redirectUrl, @@ -90,8 +98,7 @@ class OneDrive with ChangeNotifier { callbackUrlScheme = callbackUri.toString(); } - http.Response? result = await OAuth2Helper.browserAuth( - context: context, + http.Response? result = await OAuth2Helper.browserAuthWithVerifier( authEndpoint: authUri, tokenEndpoint: Uri.parse(tokenEndpoint), callbackUrl: callbackUrl, @@ -100,7 +107,7 @@ class OneDrive with ChangeNotifier { redirectUrl: redirectUrl, state: state, scopes: scopes, - windowName: windowName, + codeVerifier: codeVerifier, ); if (result != null) { await _tokenManager.saveTokenResp(result); @@ -109,13 +116,10 @@ class OneDrive with ChangeNotifier { } else { return false; } - } on PlatformException catch (err) { - if (err.code != errCANCELED) { - debugPrint("# OneDrive -> connect: $err"); - } + } on PlatformException { return false; } catch (err) { - debugPrint("# OneDrive -> connect: $err"); + debugPrint("# OneDrive -> connect error: $err"); return false; } } @@ -125,47 +129,94 @@ class OneDrive with ChangeNotifier { notifyListeners(); } - Future getInfo() async { + Future checkToken() async { final accessToken = await _tokenManager.getAccessToken(); if (accessToken == null) { - debugPrint("# OneDrive -> getInfo: Null access token"); - return OneDriveResponse( - message: "Null access token", bodyBytes: Uint8List(0)); + throw NullAccessTokenException(); + } else { + return accessToken; + } + } + + http.Response processResponse(http.Response response) { + if (response.statusCode == 401) { + disconnect(); + } + return response; + } + + Future post( + Uri url, { + dynamic headers, + dynamic body, + }) async { + final accessToken = await checkToken(); + var tmpHeaders = {"Authorization": "Bearer $accessToken"}; + if (headers != null) { + tmpHeaders.addAll(headers); + } + return processResponse(await http.post( + url, + headers: tmpHeaders, + body: body, + )); + } + + Future get( + Uri url, { + dynamic headers, + }) async { + final accessToken = await checkToken(); + var tmpHeaders = {"Authorization": "Bearer $accessToken"}; + if (headers != null) { + tmpHeaders.addAll(headers); } - final url = Uri.parse("$apiEndpoint/drive?select=owner,quota"); + return processResponse(await http.get( + url, + headers: tmpHeaders, + )); + } + Future delete( + Uri url, { + dynamic headers, + }) async { + final accessToken = await checkToken(); + var tmpHeaders = {"Authorization": "Bearer $accessToken"}; + if (headers != null) { + tmpHeaders.addAll(headers); + } + return processResponse(await http.delete( + url, + headers: tmpHeaders, + )); + } + + Future getInfo() async { try { - final resp = await http.get( - url, - headers: {"Authorization": "Bearer $accessToken"}, - ); + final url = Uri.parse("$apiEndpoint/drive?select=owner,quota"); - debugPrint( - "# OneDrive -> getInfo: ${resp.statusCode}\n# Body: ${resp.body}"); + final resp = await get(url); if (resp.statusCode == 200 || resp.statusCode == 201) { - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - userInfo: OneDriveUserInfo.fromJson(jsonDecode(resp.body)), - message: "Get Info successfully.", - bodyBytes: resp.bodyBytes, - isSuccess: true); - } else if (resp.statusCode == 404) { - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "${url.toString()} not found.", - bodyBytes: Uint8List(0)); + OneDriveUserInfo userInfo = + OneDriveUserInfo.fromJson(jsonDecode(resp.body)); + debugPrint("# OneDrive -> get info successfully: $userInfo"); + return OneDriveResponse.fromResponse( + response: resp, + userInfo: userInfo, + message: "Get Info successfully.", + ); } else { - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Error while get info.", - bodyBytes: Uint8List(0)); + debugPrint( + "# OneDrive -> get info failed: ${resp.statusCode}\n# Body: ${resp.body}"); + return OneDriveResponse.fromResponse( + response: resp, + message: "Error while get info.", + ); } } catch (err) { - debugPrint("# OneDrive -> getInfo: $err"); + debugPrint("# OneDrive -> getInfo error: $err"); return OneDriveResponse(message: "Unexpected exception: $err"); } } @@ -174,24 +225,15 @@ class OneDrive with ChangeNotifier { String remotePath, { bool isAppFolder = false, }) async { - final accessToken = await _tokenManager.getAccessToken(); - if (accessToken == null) { - return OneDriveResponse( - message: "Null access token", bodyBytes: Uint8List(0)); - } - - if (isAppFolder) { - await getMetadata(remotePath, isAppFolder: isAppFolder); - } + try { + if (isAppFolder) { + await getMetadata(remotePath, isAppFolder: isAppFolder); + } - final url = Uri.parse( - "${apiEndpoint}me/drive/${_getRootFolder(isAppFolder)}:$remotePath:/children?select=id,name,size,createdDateTime,lastModifiedDateTime,file,description"); + final url = Uri.parse( + "$apiEndpoint/me/drive/${_getRootFolder(isAppFolder)}:$remotePath:/children?select=id,name,size,createdDateTime,lastModifiedDateTime,file,description"); - try { - final resp = await http.get( - url, - headers: {"Authorization": "Bearer $accessToken"}, - ); + final resp = await get(url); if (resp.statusCode == 200 || resp.statusCode == 201) { Map body = jsonDecode(resp.body); @@ -199,25 +241,19 @@ class OneDrive with ChangeNotifier { for (var item in body['value']) { files.add(OneDriveFileInfo.fromJson(item)); } - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - files: files, - message: "List files successfully.", - bodyBytes: resp.bodyBytes, - isSuccess: true); - } else if (resp.statusCode == 404) { - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Url not found.", - bodyBytes: Uint8List(0)); + debugPrint("# OneDrive -> list successfully"); + return OneDriveResponse.fromResponse( + response: resp, + files: files, + message: "List files successfully.", + ); } else { - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Error while listing files.", - bodyBytes: Uint8List(0)); + debugPrint( + "# OneDrive -> list failed: ${resp.statusCode}\n# Body: ${resp.body}"); + return OneDriveResponse.fromResponse( + response: resp, + message: "Error while listing files.", + ); } } catch (err) { debugPrint("# OneDrive -> list: $err"); @@ -229,42 +265,24 @@ class OneDrive with ChangeNotifier { String id, { bool isAppFolder = false, }) async { - final accessToken = await _tokenManager.getAccessToken(); - if (accessToken == null) { - return OneDriveResponse( - message: "Null access token", bodyBytes: Uint8List(0)); - } - - final url = Uri.parse("${apiEndpoint}me/drive/items/$id/content"); - try { - final resp = await http.get( - url, - headers: {"Authorization": "Bearer $accessToken"}, - ); + final url = Uri.parse("$apiEndpoint/me/drive/items/$id/content"); - debugPrint( - "# OneDrive -> pull: ${resp.statusCode}\n# Body: ${resp.body}"); + final resp = await get(url); if (resp.statusCode == 200 || resp.statusCode == 201) { - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Download successfully.", - bodyBytes: resp.bodyBytes, - isSuccess: true); - } else if (resp.statusCode == 404) { - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "File not found.", - bodyBytes: Uint8List(0)); + debugPrint("# OneDrive -> pull successfully"); + return OneDriveResponse.fromResponse( + response: resp, + message: "Download successfully.", + ); } else { - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Error while downloading file.", - bodyBytes: Uint8List(0)); + debugPrint( + "# OneDrive -> pull failed: ${resp.statusCode}\n# Body: ${resp.body}"); + return OneDriveResponse.fromResponse( + response: resp, + message: "Error while downloading file.", + ); } } catch (err) { debugPrint("# OneDrive -> pull: $err"); @@ -276,42 +294,26 @@ class OneDrive with ChangeNotifier { String id, { bool isAppFolder = false, }) async { - final accessToken = await _tokenManager.getAccessToken(); - if (accessToken == null) { - return OneDriveResponse( - message: "Null access token", bodyBytes: Uint8List(0)); - } - - final url = Uri.parse("${apiEndpoint}me/drive/items/$id"); - try { - final resp = await http.delete( - url, - headers: {"Authorization": "Bearer $accessToken"}, - ); + final url = Uri.parse("$apiEndpoint/me/drive/items/$id"); - debugPrint( - "# OneDrive -> delete: ${resp.statusCode}\n# Body: ${resp.body}"); + final resp = await delete(url); - if (resp.statusCode == 200 || resp.statusCode == 201) { - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Delete successfully.", - bodyBytes: resp.bodyBytes, - isSuccess: true); - } else if (resp.statusCode == 404) { - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "File not found.", - bodyBytes: Uint8List(0)); + if (resp.statusCode == 200 || + resp.statusCode == 201 || + resp.statusCode == 204) { + debugPrint("# OneDrive -> delete successfully"); + return OneDriveResponse.fromResponse( + response: resp, + message: "Delete successfully.", + ); } else { - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Error while deleting file.", - bodyBytes: Uint8List(0)); + debugPrint( + "# OneDrive -> delete failed: ${resp.statusCode}\n# Body: ${resp.body}"); + return OneDriveResponse.fromResponse( + response: resp, + message: "Error while deleting file.", + ); } } catch (err) { debugPrint("# OneDrive -> delete: $err"); @@ -325,39 +327,28 @@ class OneDrive with ChangeNotifier { bool isAppFolder = false, Function(int p1, int p2)? onProgress, }) async { - final accessToken = await _tokenManager.getAccessToken(); - if (accessToken == null) { - return OneDriveResponse(message: "Null access token."); - } - try { if (isAppFolder) { await getMetadata(remotePath, isAppFolder: isAppFolder); } - const int pageSize = 1024 * 1024; // page size - final int maxPage = - (bytes.length / pageSize.toDouble()).ceil(); // total pages + const int pageSize = 1024 * 1024; + final int maxPage = (bytes.length / pageSize.toDouble()).ceil(); var now = DateTime.now(); var url = Uri.parse( "$apiEndpoint/me/drive/${_getRootFolder(isAppFolder)}:$remotePath:/createUploadSession"); - var resp = await http.post( - url, - headers: {"Authorization": "Bearer $accessToken"}, - ); + var resp = await post(url); + debugPrint( - "# Create Session: ${DateTime.now().difference(now).inMilliseconds} ms"); + "# OneDrive -> Upload Create Session: ${DateTime.now().difference(now).inMilliseconds} ms"); if (resp.statusCode == 200) { final Map respJson = jsonDecode(resp.body); final String uploadUrl = respJson["uploadUrl"]; url = Uri.parse(uploadUrl); - debugPrint( - "# Upload to: $url\n# Total pages: $maxPage\n# Page size: $pageSize"); - for (var pageIndex = 0; pageIndex < maxPage; pageIndex++) { now = DateTime.now(); final int start = pageIndex * pageSize; @@ -369,42 +360,40 @@ class OneDrive with ChangeNotifier { final pageData = bytes.getRange(start, end).toList(); final contentLength = pageData.length.toString(); - final headers = { - "Content-Length": contentLength, - "Content-Range": range, - }; - resp = await http.put( url, - headers: headers, + headers: { + "Content-Length": contentLength, + "Content-Range": range, + }, body: pageData, ); debugPrint( - "# Upload [${pageIndex + 1}/$maxPage]: ${DateTime.now().difference(now).inMilliseconds} ms, start: $start, end: $end, contentLength: $contentLength, range: $range"); + "# OneDrive -> Upload [${pageIndex + 1}/$maxPage]: ${DateTime.now().difference(now).inMilliseconds} ms, start: $start, end: $end, contentLength: $contentLength, range: $range"); if (resp.statusCode == 202) { onProgress?.call(pageIndex + 1, maxPage); continue; } else if (resp.statusCode == 200 || resp.statusCode == 201) { onProgress?.call(pageIndex + 1, maxPage); - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Upload finished.", - isSuccess: true); + debugPrint("# OneDrive -> Upload finished"); + return OneDriveResponse.fromResponse( + response: resp, + message: "Upload finished.", + ); } else { - return OneDriveResponse( - statusCode: resp.statusCode, - body: resp.body, - message: "Upload failed."); + debugPrint( + "# OneDrive -> Upload failed: ${resp.statusCode}\n# Body: ${resp.body}"); + return OneDriveResponse.fromResponse( + response: resp, + message: "Upload failed.", + ); } } } - - debugPrint("# Upload response: ${resp.statusCode}\n# Body: ${resp.body}"); } catch (err) { - debugPrint("# Upload error: $err"); + debugPrint("# OneDrive -> Upload error: $err"); return OneDriveResponse(message: "Unexpected exception: $err"); } @@ -419,28 +408,21 @@ class OneDrive with ChangeNotifier { String remotePath, { bool isAppFolder = false, }) async { - final accessToken = await _tokenManager.getAccessToken(); - if (accessToken == null) { - return Uint8List(0); - } - - final url = - Uri.parse("${apiEndpoint}me/drive/${_getRootFolder(isAppFolder)}"); - try { - final resp = await http.get( - url, - headers: {"Authorization": "Bearer $accessToken"}, - ); + final url = + Uri.parse("$apiEndpoint/me/drive/${_getRootFolder(isAppFolder)}"); + + final resp = await get(url); if (resp.statusCode == 200 || resp.statusCode == 201) { + debugPrint("# OneDrive -> metadata success: ${resp.body}"); return resp.bodyBytes; } else if (resp.statusCode == 404) { return Uint8List(0); + } else { + debugPrint( + "# OneDrive -> metadata failed: ${resp.statusCode}\n# Body: ${resp.body}"); } - - debugPrint( - "# OneDrive -> metadata: ${resp.statusCode}\n# Body: ${resp.body}"); } catch (err) { debugPrint("# OneDrive -> metadata: $err"); } diff --git a/third-party/flutter_cloud/lib/onedrive_response.dart b/third-party/flutter_cloud/lib/onedrive_response.dart index 4dde134e..ae26220f 100644 --- a/third-party/flutter_cloud/lib/onedrive_response.dart +++ b/third-party/flutter_cloud/lib/onedrive_response.dart @@ -1,24 +1,46 @@ import 'dart:typed_data'; +import 'package:flutter_cloud/status.dart'; +import 'package:http/http.dart' as http; class OneDriveResponse { + final ResponseStatus status; final int? statusCode; final String? body; final String? message; final bool isSuccess; final Uint8List? bodyBytes; + final String? accessToken; final OneDriveUserInfo? userInfo; final List files; OneDriveResponse({ + this.status = ResponseStatus.success, this.statusCode, this.body, this.message, + this.accessToken, this.bodyBytes, this.userInfo, this.files = const [], this.isSuccess = false, }); + OneDriveResponse.fromResponse({ + required http.Response response, + this.userInfo, + this.message, + this.files = const [], + }) : body = response.body, + accessToken = "", + statusCode = response.statusCode, + bodyBytes = response.bodyBytes, + isSuccess = response.statusCode == 200 || + response.statusCode == 201 || + response.statusCode == 204, + status = ResponseStatus.values.firstWhere( + (element) => element.code == response.statusCode, + orElse: () => ResponseStatus.success); + @override String toString() { return "OneDriveResponse(" diff --git a/third-party/flutter_cloud/lib/status.dart b/third-party/flutter_cloud/lib/status.dart new file mode 100644 index 00000000..eed5e09a --- /dev/null +++ b/third-party/flutter_cloud/lib/status.dart @@ -0,0 +1,16 @@ +enum ResponseStatus { + success(200, 'Success'), + connectionError(500, 'Connection Error'), + unauthorized(401, 'Unauthorized'), + nullAccessToken(9001, 'Null Access Token'), + notFound(404, 'Not Found'); + + final int code; + final String message; + + const ResponseStatus(this.code, this.message); +} + +class NullAccessTokenException implements Exception { + NullAccessTokenException(); +} diff --git a/third-party/flutter_cloud/lib/token_manager.dart b/third-party/flutter_cloud/lib/token_manager.dart index 2eaa50d5..9db96576 100644 --- a/third-party/flutter_cloud/lib/token_manager.dart +++ b/third-party/flutter_cloud/lib/token_manager.dart @@ -23,35 +23,38 @@ class DefaultTokenManager extends ITokenManager { final String scope; final String tokenEndpoint; final String clientId; - final String? clientSecret; final String redirectUrl; + final String revokeUrl; - final String expireInKey; + final String expireAtKey; final String accessTokenKey; final String refreshTokenKey; + final String idTokenKey; DefaultTokenManager({ required this.tokenEndpoint, required this.clientId, - this.clientSecret, required this.redirectUrl, + required this.revokeUrl, required this.scope, - required this.expireInKey, + required this.expireAtKey, required this.accessTokenKey, required this.refreshTokenKey, + required this.idTokenKey, }); @override Future saveTokenResp(http.Response resp) async { Map body = jsonDecode(resp.body); try { - String expireIn = + String expireAt = DateTime.now().add(Duration(seconds: body['expires_in'])).toString(); - await secureStorage.write(key: expireInKey, value: expireIn); + await secureStorage.write(key: expireAtKey, value: expireAt); await secureStorage.write( key: accessTokenKey, value: body['access_token']); await secureStorage.write( key: refreshTokenKey, value: body['refresh_token']); + await secureStorage.write(key: idTokenKey, value: body['id_token'] ?? ""); return true; } catch (err) { debugPrint("# DefaultTokenManager -> _saveTokenMap: $err"); @@ -63,7 +66,7 @@ class DefaultTokenManager extends ITokenManager { Future isAuthorized() async { try { final accessToken = await secureStorage.read(key: accessTokenKey); - final accessTokenExpiresAt = await secureStorage.read(key: expireInKey); + final accessTokenExpiresAt = await secureStorage.read(key: expireAtKey); if (((accessToken?.isEmpty) ?? true) && ((accessTokenExpiresAt?.isEmpty) ?? true)) { return false; @@ -79,7 +82,7 @@ class DefaultTokenManager extends ITokenManager { Future clearStoredToken() async { try { await Future.wait([ - secureStorage.delete(key: expireInKey), + secureStorage.delete(key: expireAtKey), secureStorage.delete(key: accessTokenKey), secureStorage.delete(key: refreshTokenKey), ]); @@ -96,22 +99,16 @@ class DefaultTokenManager extends ITokenManager { return null; } - final accessTokenExpiresAt = await secureStorage.read(key: expireInKey); - if ((accessTokenExpiresAt?.isEmpty) ?? true) { + final expiresAt = await secureStorage.read(key: expireAtKey); + if ((expiresAt?.isEmpty) ?? true) { return null; } - final expAt = DateTime.parse(accessTokenExpiresAt!) - .add(const Duration(minutes: -2)); + final expAt = DateTime.parse(expiresAt!).add(const Duration(minutes: -2)); if (DateTime.now().toUtc().isAfter(expAt)) { - // expired, refresh final tokenMap = await _refreshToken(); - if (tokenMap == null) { - // refresh failed - return null; - } - // refresh success + if (tokenMap == null) return null; return tokenMap['access_token']; } @@ -137,13 +134,8 @@ class DefaultTokenManager extends ITokenManager { 'redirect_uri': redirectUrl, }; - if (clientSecret != null && clientSecret!.isNotEmpty) { - body['client_secret'] = clientSecret; - } - final resp = await http.post(Uri.parse(tokenEndpoint), body: body); if (resp.statusCode != 200) { - // refresh failed debugPrint( "# DefaultTokenManager -> _refreshToken: ${resp.statusCode}\n# Body: ${resp.body}"); @@ -153,8 +145,7 @@ class DefaultTokenManager extends ITokenManager { debugPrint("# Refresh token: Success"); final Map tokenMap = jsonDecode(resp.body); - await _saveTokenMap(tokenMap); - + await saveTokenResp(resp); return tokenMap; } catch (err) { debugPrint("# DefaultTokenManager -> _refreshToken: $err"); @@ -165,19 +156,4 @@ class DefaultTokenManager extends ITokenManager { return null; } - - Future _saveTokenMap(Map tokenObj) async { - try { - final expAt = - DateTime.now().toUtc().add(Duration(seconds: tokenObj['expires_in'])); - debugPrint("# Expres at: $expAt"); - - secureStorage.write(key: expireInKey, value: expAt.toString()); - secureStorage.write(key: accessTokenKey, value: tokenObj['access_token']); - secureStorage.write( - key: refreshTokenKey, value: tokenObj['refresh_token']); - } catch (err) { - debugPrint("# DefaultTokenManager -> _saveTokenMap: $err"); - } - } } diff --git a/third-party/flutter_cloud/pubspec.lock b/third-party/flutter_cloud/pubspec.lock index 232c6bea..aeb452bb 100644 --- a/third-party/flutter_cloud/pubspec.lock +++ b/third-party/flutter_cloud/pubspec.lock @@ -49,14 +49,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.18.0" - desktop_webview_window: - dependency: transitive - description: - name: desktop_webview_window - sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.3" fake_async: dependency: transitive description: @@ -158,6 +150,54 @@ packages: description: flutter source: sdk version: "0.0.0" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.1+4" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + sha256: "0b8787cb9c1a68ad398e8010e8c8766bfa33556d2ab97c439fb4137756d7308f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.2.1" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: "5a47ebec9af97daf0822e800e4f101c3340b5ebc3f6898cf860c1a71b53cf077" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.28" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: a058c9880be456f21e2e8571c1126eaacd570bdc5b6c6d9d15aea4bdf22ca9fe + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.7.6" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.5" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: "042805a21127a85b0dc46bba98a37926f17d2439720e8a459d27045d8ef68055" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.4+2" googleapis: dependency: "direct main" description: @@ -334,6 +374,54 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" + protocol_handler: + dependency: transitive + description: + name: protocol_handler + sha256: dc2e2dcb1e0e313c3f43827ec3fa6d98adee6e17edc0c3923ac67efee87479a9 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + protocol_handler_android: + dependency: transitive + description: + name: protocol_handler_android + sha256: "82eb860ca42149e400328f54b85140329a1766d982e94705b68271f6ca73895c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + protocol_handler_ios: + dependency: transitive + description: + name: protocol_handler_ios + sha256: "0d3a56b8c1926002cb1e32b46b56874759f4dcc8183d389b670864ac041b6ec2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + protocol_handler_macos: + dependency: transitive + description: + name: protocol_handler_macos + sha256: "6eb8687a84e7da3afbc5660ce046f29d7ecf7976db45a9dadeae6c87147dd710" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + protocol_handler_platform_interface: + dependency: transitive + description: + name: protocol_handler_platform_interface + sha256: "53776b10526fdc25efdf1abcf68baf57fdfdb75342f4101051db521c9e3f3e5b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + protocol_handler_windows: + dependency: transitive + description: + name: protocol_handler_windows + sha256: d8f3a58938386aca2c76292757392f4d059d09f11439d6d896d876ebe997f2c4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" sky_engine: dependency: transitive description: flutter @@ -483,6 +571,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.5.4" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.4" window_to_front: dependency: transitive description: diff --git a/third-party/flutter_cloud/pubspec.yaml b/third-party/flutter_cloud/pubspec.yaml index 9e8565d4..d0677097 100644 --- a/third-party/flutter_cloud/pubspec.yaml +++ b/third-party/flutter_cloud/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: path: ../flutter_web_auth_2 googleapis: ^13.2.0 hashlib: ^1.19.2 +# google_sign_in: ^6.2.1 dev_dependencies: flutter_test: diff --git a/tools/CloudOTP.iss b/tools/CloudOTP.iss index 20794ac5..6700df70 100644 --- a/tools/CloudOTP.iss +++ b/tools/CloudOTP.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "CloudOTP" -#define MyAppVersion "2.2.0" +#define MyAppVersion "2.3.0" #define MyAppPublisher "Cloudchewie" #define MyAppURL "https://apps.cloudchewie.com/cloudotp" #define MyAppExeName "CloudOTP.exe" diff --git a/art/addtoken.jpg b/tools/art/addtoken.jpg similarity index 100% rename from art/addtoken.jpg rename to tools/art/addtoken.jpg diff --git a/art/darkmode.jpg b/tools/art/darkmode.jpg similarity index 100% rename from art/darkmode.jpg rename to tools/art/darkmode.jpg diff --git a/art/dropbox.jpg b/tools/art/dropbox.jpg similarity index 100% rename from art/dropbox.jpg rename to tools/art/dropbox.jpg diff --git a/art/export_import.jpg b/tools/art/export_import.jpg similarity index 100% rename from art/export_import.jpg rename to tools/art/export_import.jpg diff --git a/art/lightmode.jpg b/tools/art/lightmode.jpg similarity index 100% rename from art/lightmode.jpg rename to tools/art/lightmode.jpg diff --git a/art/lock.jpg b/tools/art/lock.jpg similarity index 100% rename from art/lock.jpg rename to tools/art/lock.jpg diff --git a/art/setting.jpg b/tools/art/setting.jpg similarity index 100% rename from art/setting.jpg rename to tools/art/setting.jpg diff --git a/art/theme.jpg b/tools/art/theme.jpg similarity index 100% rename from art/theme.jpg rename to tools/art/theme.jpg diff --git a/dll/msvcp140.dll b/tools/dll/msvcp140.dll similarity index 100% rename from dll/msvcp140.dll rename to tools/dll/msvcp140.dll diff --git a/dll/sqlcipher.dll b/tools/dll/sqlcipher.dll similarity index 100% rename from dll/sqlcipher.dll rename to tools/dll/sqlcipher.dll diff --git a/dll/sqlite3-unencrypt.dll b/tools/dll/sqlite3-unencrypt.dll similarity index 100% rename from dll/sqlite3-unencrypt.dll rename to tools/dll/sqlite3-unencrypt.dll diff --git a/dll/sqlite3.dll b/tools/dll/sqlite3.dll similarity index 100% rename from dll/sqlite3.dll rename to tools/dll/sqlite3.dll diff --git a/dll/vcruntime140.dll b/tools/dll/vcruntime140.dll similarity index 100% rename from dll/vcruntime140.dll rename to tools/dll/vcruntime140.dll diff --git a/dll/vcruntime140_1.dll b/tools/dll/vcruntime140_1.dll similarity index 100% rename from dll/vcruntime140_1.dll rename to tools/dll/vcruntime140_1.dll diff --git a/tools/generate.py b/tools/generate.py index e45b4e0d..d3af06ea 100644 --- a/tools/generate.py +++ b/tools/generate.py @@ -9,7 +9,7 @@ "D:\\Repositories\\CloudOTP\\build\\windows\\x64\\runner\\Release" ) downloads_path = "D:\\Ruida\\Downloads" -dll_path = "D:\\Repositories\\CloudOTP\\dll\\sqlite3.dll" +dll_path = "D:\\Repositories\\CloudOTP\\tools\\dll\\sqlite3.dll" iss_path = "D:\\Repositories\\CloudOTP\\tools\\CloudOTP.iss" iscc_path = "D:\\Program Files\\Inno Setup 6\\ISCC.exe" @@ -64,7 +64,6 @@ def zip_windows(version): # generate the installer def generate_installer(version): print("start generate installer...") - # 打开iss文件,修改版本号,即替换#define MyAppVersion "2.1.0"中的2.1.0为指定的版本号 with open(iss_path, "r") as f: lines = f.readlines() with open(iss_path, "w") as f: @@ -98,11 +97,6 @@ def release_windows(version): print("release windows runner done.") -# 使用argparse处理命令行参数, -# -v或--version参数指定版本号, -# -a或--android参数指定是否生成apk, -# -w或--windows参数指定是否生成windows, -# -s或--split参数指定是否生成abi分包apk if __name__ == "__main__": import argparse