diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 1c6a2a0..1f3ac03 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -859,5 +859,12 @@ "notificationTopicPostSubscription": "Post Subscriptions", "notificationTopicMessaging": "New Messages", "notificationTopicMessagingCall": "Incoming Calls", - "notificationTopicGeneral": "General" + "notificationTopicGeneral": "General", + "authMaximumAuthSteps": "Maximum Authenticate Steps", + "authMaximumAuthStepsDescription": { + "one": "Maximum ask for {} step authenticate", + "other": "Maximum ask for {} steps authenticate" + }, + "authAlwaysRisky": "Always Risky", + "authAlwaysRiskyDescription": "Always ask for the highest steps count of authentication when logging in." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 8833be9..1212020 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -857,5 +857,12 @@ "notificationTopicPostSubscription": "帖子订阅", "notificationTopicMessaging": "消息", "notificationTopicMessagingCall": "通话", - "notificationTopicGeneral": "杂项" + "notificationTopicGeneral": "杂项", + "authMaximumAuthSteps": "最大验证步骤", + "authMaximumAuthStepsDescription": { + "one": "登入时最多要求 {} 步验证", + "other": "登入时最多要求 {} 步验证" + }, + "authAlwaysRisky": "总是风险", + "authAlwaysRiskyDescription": "在登入时始终按最高标准要求验证。" } diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index 13ab3ad..c9eaa67 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -846,5 +846,23 @@ "translated": "已翻譯", "settingsAutoTranslate": "自動翻譯", "settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。", - "trayMenuHide": "隱藏" + "trayMenuHide": "隱藏", + "accountSettingsNotify": "通知設置", + "accountSettingsNotifyDescription": "調整你所收到的通知種類。", + "accountSettingsSecurity": "安全設置", + "accountSettingsSecurityDescription": "調整你的帳户安全設置。", + "save": "保存", + "notificationTopicPostFeedback": "帖子數據反饋", + "notificationTopicPostReply": "帖子回覆", + "notificationTopicPostSubscription": "帖子訂閲", + "notificationTopicMessaging": "消息", + "notificationTopicMessagingCall": "通話", + "notificationTopicGeneral": "雜項", + "authMaximumAuthSteps": "最大驗證步驟", + "authMaximumAuthStepsDescription": { + "one": "登入時最多要求 {} 步驗證", + "other": "登入時最多要求 {} 步驗證" + }, + "authAlwaysRisky": "總是風險", + "authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。" } diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index b85aefa..c64ab23 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -846,5 +846,23 @@ "translated": "已翻譯", "settingsAutoTranslate": "自動翻譯", "settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。", - "trayMenuHide": "隱藏" + "trayMenuHide": "隱藏", + "accountSettingsNotify": "通知設置", + "accountSettingsNotifyDescription": "調整你所收到的通知種類。", + "accountSettingsSecurity": "安全設置", + "accountSettingsSecurityDescription": "調整你的帳戶安全設置。", + "save": "保存", + "notificationTopicPostFeedback": "帖子數據反饋", + "notificationTopicPostReply": "帖子回覆", + "notificationTopicPostSubscription": "帖子訂閱", + "notificationTopicMessaging": "消息", + "notificationTopicMessagingCall": "通話", + "notificationTopicGeneral": "雜項", + "authMaximumAuthSteps": "最大驗證步驟", + "authMaximumAuthStepsDescription": { + "one": "登入時最多要求 {} 步驗證", + "other": "登入時最多要求 {} 步驗證" + }, + "authAlwaysRisky": "總是風險", + "authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。" } diff --git a/lib/router.dart b/lib/router.dart index f42f916..4ca24de 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -10,6 +10,7 @@ import 'package:surface/screens/account/contact_methods.dart'; import 'package:surface/screens/account/factor_settings.dart'; import 'package:surface/screens/account/keypairs.dart'; import 'package:surface/screens/account/prefs/notify.dart'; +import 'package:surface/screens/account/prefs/security.dart'; import 'package:surface/screens/account/profile_page.dart'; import 'package:surface/screens/account/profile_edit.dart'; import 'package:surface/screens/account/publishers/publisher_edit.dart'; @@ -168,6 +169,11 @@ final _appRoutes = [ name: 'accountSettingsNotify', builder: (context, state) => const AccountNotifyPrefsScreen(), ), + GoRoute( + path: '/auth', + name: 'accountSettingsSecurity', + builder: (context, state) => const AccountSecurityPrefsScreen(), + ), ], ), GoRoute( diff --git a/lib/screens/account/account_settings.dart b/lib/screens/account/account_settings.dart index 2460b90..6d6900d 100644 --- a/lib/screens/account/account_settings.dart +++ b/lib/screens/account/account_settings.dart @@ -107,6 +107,16 @@ class AccountSettingsScreen extends StatelessWidget { GoRouter.of(context).pushNamed('accountSettingsNotify'); }, ), + ListTile( + title: Text('accountSettingsSecurity').tr(), + subtitle: Text('accountSettingsSecurityDescription').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.shield), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + GoRouter.of(context).pushNamed('accountSettingsSecurity'); + }, + ), ListTile( title: Text('accountProfileEdit').tr(), subtitle: Text('accountProfileEditSubtitle').tr(), diff --git a/lib/screens/account/prefs/security.dart b/lib/screens/account/prefs/security.dart new file mode 100644 index 0000000..9d6c90c --- /dev/null +++ b/lib/screens/account/prefs/security.dart @@ -0,0 +1,147 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:provider/provider.dart'; +import 'package:surface/providers/sn_network.dart'; +import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; + +class AccountSecurityPrefsScreen extends StatefulWidget { + const AccountSecurityPrefsScreen({super.key}); + + @override + State createState() => + _AccountSecurityPrefsScreenState(); +} + +class _AccountSecurityPrefsScreenState + extends State { + bool _isBusy = true; + + Map _config = { + 'maximum_auth_steps': 2, + 'always_risky': false, + }; + + Future _getPreferences() async { + setState(() => _isBusy = true); + + final sn = context.read(); + + try { + final resp = await sn.client.get('/cgi/id/preferences/auth'); + _config = resp.data['config'] + .map((k, v) => MapEntry(k, v as bool)) + .cast(); + } finally { + setState(() => _isBusy = false); + } + } + + Future _savePreferences() async { + setState(() => _isBusy = true); + + final sn = context.read(); + + try { + await sn.client.put( + '/cgi/id/preferences/auth', + data: { + 'config': _config, + }, + ); + if (!mounted) return; + context.showSnackbar('accountSettingsApplied'.tr()); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + @override + void initState() { + super.initState(); + _getPreferences(); + } + + @override + Widget build(BuildContext context) { + return AppScaffold( + appBar: AppBar( + leading: const PageBackButton(), + title: Text('accountSettingsSecurity').tr(), + ), + body: Column( + children: [ + LoadingIndicator(isActive: _isBusy), + ListTile( + tileColor: Theme.of(context).colorScheme.surfaceContainer, + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Icons.save), + title: Text('save').tr(), + enabled: !_isBusy, + onTap: () { + _savePreferences(); + }, + ), + Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + ListTile( + title: Text('authMaximumAuthSteps').tr(), + subtitle: Text('authMaximumAuthStepsDescription') + .plural(_config['maximum_auth_steps'] ?? 2), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + padding: EdgeInsets.zero, + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + icon: const Icon(Symbols.remove), + onPressed: () { + if (_config['maximum_auth_steps'] > 1) { + setState(() => _config['maximum_auth_steps']--); + } + }, + ), + IconButton( + padding: EdgeInsets.zero, + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + icon: const Icon(Symbols.add), + onPressed: () { + if (_config['maximum_auth_steps'] < 99) { + setState(() => _config['maximum_auth_steps']++); + } + }, + ), + ], + ), + ), + CheckboxListTile( + title: Text('authAlwaysRisky').tr(), + subtitle: Text('authAlwaysRiskyDescription').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + value: _config['always_risky'] ?? false, + onChanged: (value) { + setState(() => _config['always_risky'] = value); + }, + ), + ], + ), + ), + ], + ), + ); + } +}