From dc38b46b2c48358b5713aebdc6fab776aa37588f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 22 Mar 2025 20:24:05 +0800 Subject: [PATCH] :sparkles: Support captcha --- assets/translations/en-US.json | 3 +- assets/translations/zh-CN.json | 3 +- assets/translations/zh-HK.json | 3 +- assets/translations/zh-TW.json | 3 +- lib/screens/auth/register.dart | 50 ++++++++++++++----- lib/screens/captcha.dart | 38 ++++++++++++++ lib/screens/home.dart | 12 ++++- .../navigation/app_drawer_navigation.dart | 10 ++++ 8 files changed, 105 insertions(+), 17 deletions(-) create mode 100644 lib/screens/captcha.dart diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 5b002eb..ec90d08 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -889,5 +889,6 @@ "other": "Total {} posts" }, "settingsHideBottomNav": "Hide Bottom Navigation", - "settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer." + "settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.", + "reCaptcha": "reCaptcha" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index ab5209a..612b5e0 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -887,5 +887,6 @@ "one": "共 {} 条帖子" }, "settingsHideBottomNav": "隐藏底部导航栏", - "settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。" + "settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。", + "reCaptcha": "人机验证" } diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index f070db4..3044e36 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -887,5 +887,6 @@ "one": "共 {} 條帖子" }, "settingsHideBottomNav": "隱藏底部導航欄", - "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。" + "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。", + "reCaptcha": "人機驗證" } diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index c5753be..57eb7ad 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -887,5 +887,6 @@ "one": "共 {} 條帖子" }, "settingsHideBottomNav": "隱藏底部導航欄", - "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。" + "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。", + "reCaptcha": "人機驗證" } diff --git a/lib/screens/auth/register.dart b/lib/screens/auth/register.dart index 15fe156..4c4dbe3 100644 --- a/lib/screens/auth/register.dart +++ b/lib/screens/auth/register.dart @@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/screens/captcha.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -33,10 +34,20 @@ class _RegisterScreenState extends State { final username = _usernameController.value.text; final nickname = _nicknameController.value.text; final password = _passwordController.value.text; - if (email.isEmpty || username.isEmpty || nickname.isEmpty || password.isEmpty) { + if (email.isEmpty || + username.isEmpty || + nickname.isEmpty || + password.isEmpty) { return; } + final captchaTk = await Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute( + builder: (context) => TurnstileScreen(), + ), + ); + if (captchaTk == null) return; + try { final sn = context.read(); await sn.client.post('/cgi/id/users', data: { @@ -45,6 +56,7 @@ class _RegisterScreenState extends State { 'email': email, 'password': password, 'language': EasyLocalization.of(context)!.currentLocale.toString(), + 'captcha_token': captchaTk, }); if (!context.mounted) return; @@ -91,8 +103,11 @@ class _RegisterScreenState extends State { children: [ TextFormField( validator: (value) { - if (value == null || value.length < 4 || value.length > 32) { - return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]); + if (value == null || + value.length < 4 || + value.length > 32) { + return 'fieldUsernameLengthLimit' + .tr(args: [4.toString(), 32.toString()]); } if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) { return 'fieldUsernameAlphanumOnly'.tr(); @@ -108,13 +123,17 @@ class _RegisterScreenState extends State { border: const UnderlineInputBorder(), labelText: 'fieldUsername'.tr(), ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(12), TextFormField( validator: (value) { - if (value == null || value.length < 4 || value.length > 32) { - return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]); + if (value == null || + value.length < 4 || + value.length > 32) { + return 'fieldNicknameLengthLimit' + .tr(args: [4.toString(), 32.toString()]); } return null; }, @@ -127,7 +146,8 @@ class _RegisterScreenState extends State { border: const UnderlineInputBorder(), labelText: 'fieldNickname'.tr(), ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(12), TextFormField( @@ -149,7 +169,8 @@ class _RegisterScreenState extends State { border: const UnderlineInputBorder(), labelText: 'fieldEmail'.tr(), ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(12), TextFormField( @@ -169,7 +190,8 @@ class _RegisterScreenState extends State { border: const UnderlineInputBorder(), labelText: 'fieldPassword'.tr(), ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ], ).padding(horizontal: 7), @@ -186,9 +208,13 @@ class _RegisterScreenState extends State { Text( 'termAcceptNextWithAgree'.tr(), textAlign: TextAlign.end, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()), - ), + style: + Theme.of(context).textTheme.bodySmall!.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withAlpha((255 * 0.75).round()), + ), ), Material( color: Colors.transparent, diff --git a/lib/screens/captcha.dart b/lib/screens/captcha.dart new file mode 100644 index 0000000..266df22 --- /dev/null +++ b/lib/screens/captcha.dart @@ -0,0 +1,38 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:provider/provider.dart'; +import 'package:surface/providers/config.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; + +class TurnstileScreen extends StatefulWidget { + const TurnstileScreen({ + super.key, + }); + + @override + State createState() => _TurnstileScreenState(); +} + +class _TurnstileScreenState extends State { + @override + Widget build(BuildContext context) { + final cfg = context.read(); + return AppScaffold( + appBar: AppBar(title: Text("reCaptcha").tr()), + body: InAppWebView( + initialUrlRequest: URLRequest( + url: WebUri('${cfg.serverUrl}/captcha?redirect_uri=solink://captcha'), + ), + shouldOverrideUrlLoading: (controller, navigationAction) async { + Uri? url = navigationAction.request.url; + if (url != null && url.queryParameters.containsKey('captcha_tk')) { + Navigator.pop(context, url.queryParameters['captcha_tk']!); + return NavigationActionPolicy.CANCEL; + } + return NavigationActionPolicy.ALLOW; + }, + ), + ); + } +} diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 0997e10..4f37fcc 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/special_day.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/widget.dart'; +import 'package:surface/screens/captcha.dart'; import 'package:surface/types/check_in.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/app_bar_leading.dart'; @@ -508,11 +509,20 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> { } Future _doCheckIn() async { + final captchaTk = await Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute( + builder: (context) => TurnstileScreen(), + ), + ); + if (captchaTk == null) return; + setState(() => _isBusy = true); try { final sn = context.read(); final home = context.read(); - final resp = await sn.client.post('/cgi/id/check-in'); + final resp = await sn.client.post('/cgi/id/check-in', data: { + 'captcha_token': captchaTk, + }); _todayRecord = SnCheckInRecord.fromJson(resp.data); await home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson()); } catch (err) { diff --git a/lib/widgets/navigation/app_drawer_navigation.dart b/lib/widgets/navigation/app_drawer_navigation.dart index 6977b7e..e2b2606 100644 --- a/lib/widgets/navigation/app_drawer_navigation.dart +++ b/lib/widgets/navigation/app_drawer_navigation.dart @@ -185,6 +185,16 @@ class _DrawerContentList extends StatelessWidget { horizontal: 32, vertical: 12, ), + ListTile( + minTileHeight: 48, + contentPadding: EdgeInsets.only(left: 28, right: 16), + leading: const Icon(Symbols.home), + title: Text('screenHome').tr(), + onTap: () { + GoRouter.of(context).goNamed('home'); + Scaffold.of(context).closeDrawer(); + }, + ), ...rel.availableRealms.map((ele) { return ListTile( minTileHeight: 48,