From 0dbb8f132a756d0d0fe9d71a535f39671709ce92 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 28 Jan 2025 19:55:35 +0800 Subject: [PATCH] :sparkles: Factor settings with TOTP, In app notify authenticate method --- assets/translations/en-US.json | 7 +- assets/translations/zh-CN.json | 7 +- ios/Podfile.lock | 12 +-- lib/screens/account/factor_settings.dart | 111 +++++++++++++++++++++-- lib/screens/auth/login.dart | 12 +-- lib/widgets/universal_image.dart | 3 +- macos/Podfile.lock | 12 +-- pubspec.lock | 76 ++++++++-------- 8 files changed, 169 insertions(+), 71 deletions(-) diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 1abd005..50dfecc 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -106,6 +106,8 @@ }, "loginEnterPassword": "Enter the code", "loginSuccess": "Logged in as {}", + "authFactorDelete": "Delete Auth Factor", + "authFactorDeleteDescription": "Are you sure you want delete auth factor {}?", "authFactorPassword": "Password", "authFactorPasswordDescription": "The password you set when you registered.", "authFactorEmail": "Email verification code", @@ -579,5 +581,8 @@ "newsReadingFromReader": "You're reading from HyperNet.Reader", "newsReadingFromOriginal": "You're reading the original article", "newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.", - "newsToday": "Today's News" + "newsToday": "Today's News", + "totpPostSetup": "One More Thing", + "totpPostSetupDescription": "Scan the QR Code below with Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden or any of kind of authenticator app which supports TOTP.", + "totpNeverShare": "Never share this QR Code" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 73e0e0a..23a798f 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -89,6 +89,8 @@ }, "loginEnterPassword": "验证代码", "loginSuccess": "登录为 {}", + "authFactorDelete": "删除验证因子", + "authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?", "authFactorPassword": "密码", "authFactorPasswordDescription": "注册时选择设置的密码。", "authFactorEmail": "电邮一次性验证码", @@ -576,5 +578,8 @@ "newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章", "newsReadingFromOriginal": "你正在阅读原始文章", "newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。", - "newsToday": "快讯" + "newsToday": "快讯", + "totpPostSetup": "还有一件事", + "totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的验证器扫描本 QR Code 来添加。", + "totpNeverShare": "永远不要分享这个 QR Code" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f3575db..804814e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -53,14 +53,14 @@ PODS: - Firebase/Messaging (11.6.0): - Firebase/CoreOnly - FirebaseMessaging (~> 11.6.0) - - firebase_analytics (11.4.0): + - firebase_analytics (11.4.1): - Firebase/Analytics (= 11.6.0) - firebase_core - Flutter - - firebase_core (3.10.0): + - firebase_core (3.10.1): - Firebase/CoreOnly (= 11.6.0) - Flutter - - firebase_messaging (15.2.0): + - firebase_messaging (15.2.1): - Firebase/Messaging (= 11.6.0) - firebase_core - Flutter @@ -382,9 +382,9 @@ SPEC CHECKSUMS: file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 Firebase: 374a441a91ead896215703a674d58cdb3e9d772b - firebase_analytics: 07bd7cfbac54bfcdccf2bb2530f9a65486f7ef3f - firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10 - firebase_messaging: e2f0ba891b1509668c07f5099761518a5af8fe3c + firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e + firebase_core: e2aa06dbd854d961f8ce46c2e20933bee1bf2d2b + firebase_messaging: 96cf6d67121b3f39746b2a4f29a26c0eee4af70e FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 diff --git a/lib/screens/account/factor_settings.dart b/lib/screens/account/factor_settings.dart index 45385ab..d89e31f 100644 --- a/lib/screens/account/factor_settings.dart +++ b/lib/screens/account/factor_settings.dart @@ -1,8 +1,10 @@ import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; +import 'package:qr_flutter/qr_flutter.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/auth.dart'; @@ -10,7 +12,7 @@ import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; -final Map _kFactorTypes = { +final Map kFactorTypes = { 0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password), 1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email), 2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer), @@ -25,10 +27,12 @@ class FactorSettingsScreen extends StatefulWidget { } class _FactorSettingsScreenState extends State { + bool _isBusy = false; List? _factors; Future _fetchFactors() async { try { + setState(() => _isBusy = true); final sn = context.read(); final resp = await sn.client.get('/cgi/id/users/me/factors'); _factors = List.from( @@ -38,7 +42,7 @@ class _FactorSettingsScreenState extends State { if (!mounted) return; context.showErrorDialog(err); } finally { - setState(() {}); + setState(() => _isBusy = false); } } @@ -59,7 +63,7 @@ class _FactorSettingsScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ LoadingIndicator( - isActive: _factors == null, + isActive: _isBusy, ), ListTile( title: Text('authFactorAdd').tr(), @@ -90,10 +94,34 @@ class _FactorSettingsScreenState extends State { itemBuilder: (context, idx) { final ele = _factors![idx]; return ListTile( - title: Text(_kFactorTypes[ele.type]!.$1).tr(), - subtitle: Text(_kFactorTypes[ele.type]!.$2).tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: Icon(_kFactorTypes[ele.type]!.$3), + title: Text(kFactorTypes[ele.type]!.$1).tr(), + subtitle: Text(kFactorTypes[ele.type]!.$2).tr(), + contentPadding: const EdgeInsets.only(left: 24, right: 12), + leading: Icon(kFactorTypes[ele.type]!.$3), + trailing: IconButton( + icon: const Icon(Symbols.close), + onPressed: ele.type > 0 + ? () { + context + .showConfirmDialog( + 'authFactorDelete'.tr(), + 'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]), + ) + .then((val) async { + if (!val) return; + try { + if (!context.mounted) return; + final sn = context.read(); + await sn.client.delete('/cgi/id/users/me/factors/${ele.id}'); + _fetchFactors(); + } catch (err) { + if (!context.mounted) return; + context.showErrorDialog(err); + } + }); + } + : null, + ), ); }, ), @@ -126,7 +154,14 @@ class _FactorNewDialogState extends State<_FactorNewDialog> { final resp = await sn.client.post('/cgi/id/users/me/factors', data: { 'type': _factorType, }); - // TODO show qrcode when creating totp factor + final factor = SnAuthFactor.fromJson(resp.data); + if (!mounted) return; + if (factor.type == 2) { + await showModalBottomSheet( + context: context, + builder: (context) => _FactorTotpFactorDialog(factor: factor), + ); + } if (!mounted) return; Navigator.of(context).pop(true); } catch (err) { @@ -154,7 +189,7 @@ class _FactorNewDialogState extends State<_FactorNewDialog> { overflow: TextOverflow.ellipsis, ), value: _factorType, - items: _kFactorTypes.entries.map( + items: kFactorTypes.entries.map( (ele) { final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key); return DropdownMenuItem( @@ -199,3 +234,61 @@ class _FactorNewDialogState extends State<_FactorNewDialog> { ); } } + +class _FactorTotpFactorDialog extends StatelessWidget { + final SnAuthFactor factor; + + const _FactorTotpFactorDialog({super.key, required this.factor}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: Text( + 'totpPostSetup', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, + ).tr().width(280), + ), + const Gap(4), + Center( + child: Text( + 'totpPostSetupDescription', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall, + ).tr().width(280), + ), + const Gap(16), + QrImageView( + padding: EdgeInsets.zero, + data: factor.config!['url'], + errorCorrectionLevel: QrErrorCorrectLevel.H, + version: QrVersions.auto, + size: 160, + gapless: true, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.circle, + color: Theme.of(context).colorScheme.onSurface, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const Gap(16), + Center( + child: Text( + 'totpNeverShare', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ).tr().bold().width(280), + ), + ], + ), + ); + } +} diff --git a/lib/screens/auth/login.dart b/lib/screens/auth/login.dart index 8ef3048..f805b83 100644 --- a/lib/screens/auth/login.dart +++ b/lib/screens/auth/login.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/userinfo.dart'; +import 'package:surface/screens/account/factor_settings.dart'; import 'package:surface/types/auth.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; @@ -14,11 +15,6 @@ import 'package:url_launcher/url_launcher_string.dart'; import '../../providers/websocket.dart'; -final Map _factorLabelMap = { - 0: ('authFactorPassword'.tr(), Symbols.password, false), - 1: ('authFactorEmail'.tr(), Symbols.email, true), -}; - class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -212,7 +208,7 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> { controller: _passwordController, obscureText: true, autofillHints: [ - (_factorLabelMap[widget.factor!.type]?.$3 ?? true) ? AutofillHints.password : AutofillHints.oneTimeCode + widget.factor!.type == 0 ? AutofillHints.password : AutofillHints.oneTimeCode ], decoration: InputDecoration( isDense: true, @@ -328,10 +324,10 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> { ), ), secondary: Icon( - _factorLabelMap[x.type]?.$2 ?? Symbols.question_mark, + kFactorTypes[x.type]?.$3 ?? Symbols.question_mark, ), title: Text( - _factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(), + kFactorTypes[x.type]?.$1 ?? 'unknown'.tr(), ), enabled: !widget.ticket!.factorTrail.contains(x.id), value: _factorPicked == x.id, diff --git a/lib/widgets/universal_image.dart b/lib/widgets/universal_image.dart index 54b2aaa..0adab33 100644 --- a/lib/widgets/universal_image.dart +++ b/lib/widgets/universal_image.dart @@ -5,10 +5,9 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:flutter_animate/flutter_animate.dart'; - +import 'package:surface/providers/config.dart'; // Keep this import to make the web image render work import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; -import 'package:surface/providers/config.dart'; class UniversalImage extends StatelessWidget { final String url; diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 7652a30..979aec5 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -22,14 +22,14 @@ PODS: - Firebase/Messaging (11.6.0): - Firebase/CoreOnly - FirebaseMessaging (~> 11.6.0) - - firebase_analytics (11.4.0): + - firebase_analytics (11.4.1): - Firebase/Analytics (= 11.6.0) - firebase_core - FlutterMacOS - - firebase_core (3.10.0): + - firebase_core (3.10.1): - Firebase/CoreOnly (~> 11.6.0) - FlutterMacOS - - firebase_messaging (15.2.0): + - firebase_messaging (15.2.1): - Firebase/CoreOnly (~> 11.6.0) - Firebase/Messaging (~> 11.6.0) - firebase_core @@ -296,9 +296,9 @@ SPEC CHECKSUMS: file_saver: 44e6fbf666677faf097302460e214e977fdd977b file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d Firebase: 374a441a91ead896215703a674d58cdb3e9d772b - firebase_analytics: 5249f87da6fed852901581aab2602e0280ec2fdb - firebase_core: 6d9bb8b0ea817e8fe0d928177d42275b45fdba6f - firebase_messaging: ae8e88b586e4d50abc7cac5bacf74d21967fd226 + firebase_analytics: 91efc58e8e37964469fdd59ad11ba36bc97e75d6 + firebase_core: 75e003524565fb5bd80c9960bc5892e8475821cd + firebase_messaging: 082a385eb98b5bb843a566cb30404859c4bd6e25 FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 diff --git a/pubspec.lock b/pubspec.lock index e69ee3c..3eaac7b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "27899c95f9e7ec06c8310e6e0eac967707714b9f1450c4a58fa00ca011a4a8ae" + sha256: e4f2a7ef31b0ab2c89d2bde35ef3e6e6aff1dce5e66069c6540b0e9cfe33ee6b url: "https://pub.dev" source: hosted - version: "1.3.49" + version: "1.3.50" _macros: dependency: transitive description: dart @@ -338,10 +338,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.8" dart_webrtc: dependency: "direct main" description: @@ -418,10 +418,10 @@ packages: dependency: "direct main" description: name: easy_localization - sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 + sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.0.7+1" easy_localization_loader: dependency: "direct main" description: @@ -538,34 +538,34 @@ packages: dependency: "direct main" description: name: firebase_analytics - sha256: "498c6cb8468e348a556709c745d92a52173ab3a9b906aa0593393f0787f201ea" + sha256: eac382bbcd5ae78c1d1ce5619d13f5a7424429f4bf55df9e3ad5110da34d1060 url: "https://pub.dev" source: hosted - version: "11.4.0" + version: "11.4.1" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: ccbb350554e98afdb4b59852689292d194d31232a2647b5012a66622b3711df9 + sha256: a34db46c367265c4c961626e4b128bfb7b7e50958e7add4c27ba103f5f81b9b0 url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.3.1" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: "68e1f18fc16482c211c658e739c25f015b202a260d9ad8249c6d3d7963b8105f" + sha256: b6b4cef08e45e4c7d48476d9fc49fe9577081809a59026fe95b1a1b1eea165fa url: "https://pub.dev" source: hosted - version: "0.5.10+6" + version: "0.5.10+7" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "0307c1fde82e2b8b97e0be2dab93612aff9a72f31ebe9bfac66ed8b37ef7c568" + sha256: d851c1ca98fd5a4c07c747f8c65dacc2edd84a4d9ac055d32a5f0342529069f5 url: "https://pub.dev" source: hosted - version: "3.10.0" + version: "3.10.1" firebase_core_platform_interface: dependency: transitive description: @@ -586,26 +586,26 @@ packages: dependency: "direct main" description: name: firebase_messaging - sha256: "48a8a59197c1c5174060ba9aa1e0036e9b5a0d28a0cc22d19c1fcabc67fafe3c" + sha256: e20ea2a0ecf9b0971575ab3ab42a6e285a94e50092c555b090c1a588a81b4d54 url: "https://pub.dev" source: hosted - version: "15.2.0" + version: "15.2.1" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "9770a8e91f54296829dcaa61ce9b7c2f9ae9abbf99976dd3103a60470d5264dd" + sha256: c57a92b5ae1857ef4fe4ae2e73452b44d32e984e15ab8b53415ea1bb514bdabd url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.6.1" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "329ca4ef45ec616abe6f1d5e58feed0934a50840a65aa327052354ad3c64ed77" + sha256: "83694a990d8525d6b01039240b97757298369622ca0253ad0ebcfed221bf8ee0" url: "https://pub.dev" source: hosted - version: "3.10.0" + version: "3.10.1" fixnum: dependency: transitive description: @@ -830,10 +830,10 @@ packages: dependency: "direct main" description: name: flutter_webrtc - sha256: "188401cc3275bc4f1f965babdff6cac612a4b46572f1e49f49db8af5361d5712" + sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03" url: "https://pub.dev" source: hosted - version: "0.12.6" + version: "0.12.7" freezed: dependency: "direct dev" description: @@ -878,18 +878,18 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" go_router: dependency: "direct main" description: name: go_router - sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d" + sha256: daf3ff5570f55396b2d2c9bf8136d7db3a8acf208ac0cef92a3ae2beb9a81550 url: "https://pub.dev" source: hosted - version: "14.6.3" + version: "14.7.1" google_fonts: dependency: "direct main" description: @@ -950,10 +950,10 @@ packages: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_multi_server: dependency: transitive description: @@ -1030,10 +1030,10 @@ packages: dependency: transitive description: name: image_picker_macos - sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" url: "https://pub.dev" source: hosted - version: "0.2.1+1" + version: "0.2.1+2" image_picker_platform_interface: dependency: transitive description: @@ -1710,18 +1710,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a + sha256: c59819dacc6669a1165d54d2735a9543f136f9b3cec94ca65cea6ab8dffc422e url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.4.0" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5" + sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" shared_preferences_foundation: dependency: transitive description: @@ -2171,10 +2171,10 @@ packages: dependency: "direct main" description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" webrtc_interface: dependency: transitive description: @@ -2187,10 +2187,10 @@ packages: dependency: transitive description: name: win32 - sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e url: "https://pub.dev" source: hosted - version: "5.10.0" + version: "5.10.1" win32_registry: dependency: transitive description: