Factor settings with TOTP, In app notify authenticate method

This commit is contained in:
LittleSheep 2025-01-28 19:55:35 +08:00
parent 3395f3dbd0
commit 0dbb8f132a
8 changed files with 169 additions and 71 deletions

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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

View File

@ -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<int, (String, String, IconData)> _kFactorTypes = {
final Map<int, (String, String, IconData)> 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<FactorSettingsScreen> {
bool _isBusy = false;
List<SnAuthFactor>? _factors;
Future<void> _fetchFactors() async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/me/factors');
_factors = List<SnAuthFactor>.from(
@ -38,7 +42,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() {});
setState(() => _isBusy = false);
}
}
@ -59,7 +63,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LoadingIndicator(
isActive: _factors == null,
isActive: _isBusy,
),
ListTile(
title: Text('authFactorAdd').tr(),
@ -90,10 +94,34 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
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<SnNetworkProvider>();
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<int>(
@ -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),
),
],
),
);
}
}

View File

@ -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<int, (String label, IconData icon, bool isOtp)> _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,

View File

@ -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;

View File

@ -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

View File

@ -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: