From dfe117d04f78e755f20d227d9f289d9f606f5702 Mon Sep 17 00:00:00 2001
From: LittleSheep <littlesheep.code@hotmail.com>
Date: Sat, 22 Mar 2025 00:14:49 +0800
Subject: [PATCH] :sparkles: Auth preference screen

---
 assets/translations/en-US.json            |   9 +-
 assets/translations/zh-CN.json            |   9 +-
 assets/translations/zh-HK.json            |  20 ++-
 assets/translations/zh-TW.json            |  20 ++-
 lib/router.dart                           |   6 +
 lib/screens/account/account_settings.dart |  10 ++
 lib/screens/account/prefs/security.dart   | 147 ++++++++++++++++++++++
 7 files changed, 217 insertions(+), 4 deletions(-)
 create mode 100644 lib/screens/account/prefs/security.dart

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<AccountSecurityPrefsScreen> createState() =>
+      _AccountSecurityPrefsScreenState();
+}
+
+class _AccountSecurityPrefsScreenState
+    extends State<AccountSecurityPrefsScreen> {
+  bool _isBusy = true;
+
+  Map<String, dynamic> _config = {
+    'maximum_auth_steps': 2,
+    'always_risky': false,
+  };
+
+  Future<void> _getPreferences() async {
+    setState(() => _isBusy = true);
+
+    final sn = context.read<SnNetworkProvider>();
+
+    try {
+      final resp = await sn.client.get('/cgi/id/preferences/auth');
+      _config = resp.data['config']
+          .map((k, v) => MapEntry(k, v as bool))
+          .cast<String, bool>();
+    } finally {
+      setState(() => _isBusy = false);
+    }
+  }
+
+  Future<void> _savePreferences() async {
+    setState(() => _isBusy = true);
+
+    final sn = context.read<SnNetworkProvider>();
+
+    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);
+                  },
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}