diff --git a/lib/api/api_result.dart b/lib/api/api_result.dart index e7a669f..4fc1fa8 100644 --- a/lib/api/api_result.dart +++ b/lib/api/api_result.dart @@ -19,4 +19,18 @@ class ApiResult { final String? error; Map toJson(Object? Function(T) toJsonT) => _$ApiResultToJson(this, toJsonT); + T unwrap() { + if (ok) { + return data!; + } else { + return throw error!; + } + } + (int, String) unwrapErr() { + if (ok) { + return throw 'unwrap_err called on ok ApiResult'; + } else { + return (status, error!); + } + } } diff --git a/lib/api/client.dart b/lib/api/client.dart index 5e3d9bd..b10f10a 100644 --- a/lib/api/client.dart +++ b/lib/api/client.dart @@ -30,10 +30,11 @@ abstract class _EHApi { factory _EHApi(Dio dio, {required String baseUrl}) = __EHApi; @PUT('/user') + @MultiPart() Future> createUser( - @Query("name") String name, @Query("password") String password, - {@Query("is_admin") bool? isAdmin, - @Query("permissions") int? permissions}); + @Part(name: "name") String name, @Part(name: "password") String password, + {@Part(name: "is_admin") bool? isAdmin, + @Part(name: "permissions") int? permissions}); @GET('/user') Future> getUser( {@Query("id") int? id, @Query("username") String? username}); @@ -42,19 +43,21 @@ abstract class _EHApi { Future> getStatus(); @PUT('/token') + @MultiPart() // ignore: unused_element Future> _createToken( - {@Query("username") required String username, - @Query("password") required String password, - @Query("t") required int t, + {@Part(name: "username") required String username, + @Part(name: "password") required String password, + @Part(name: "t") required int t, // ignore: unused_element - @Query("set_cookie") bool? setCookie, + @Part(name: "set_cookie") bool? setCookie, // ignore: unused_element - @Query("http_only") bool? httpOnly, + @Part(name: "http_only") bool? httpOnly, // ignore: unused_element - @Query("secure") bool? secure}); + @Part(name: "secure") bool? secure}); @DELETE('/token') - Future> deleteToken({@Query("token") String? token}); + @MultiPart() + Future> deleteToken({@Part(name: "token") String? token}); @GET('/token') Future> getToken( {@Query("token") String? token}); diff --git a/lib/api/client.g.dart b/lib/api/client.g.dart index 3d8db6c..d4e28b5 100644 --- a/lib/api/client.g.dart +++ b/lib/api/client.g.dart @@ -26,20 +26,36 @@ class __EHApi implements _EHApi { int? permissions, }) async { const _extra = {}; - final queryParameters = { - r'name': name, - r'password': password, - r'is_admin': isAdmin, - r'permissions': permissions, - }; + final queryParameters = {}; queryParameters.removeWhere((k, v) => v == null); final _headers = {}; - final Map? _data = null; + final _data = FormData(); + _data.fields.add(MapEntry( + 'name', + name, + )); + _data.fields.add(MapEntry( + 'password', + password, + )); + if (isAdmin != null) { + _data.fields.add(MapEntry( + 'is_admin', + isAdmin.toString(), + )); + } + if (permissions != null) { + _data.fields.add(MapEntry( + 'permissions', + permissions.toString(), + )); + } final _result = await _dio .fetch>(_setStreamType>(Options( method: 'PUT', headers: _headers, extra: _extra, + contentType: 'multipart/form-data', ) .compose( _dio.options, @@ -136,22 +152,46 @@ class __EHApi implements _EHApi { bool? secure, }) async { const _extra = {}; - final queryParameters = { - r'username': username, - r'password': password, - r't': t, - r'set_cookie': setCookie, - r'http_only': httpOnly, - r'secure': secure, - }; + final queryParameters = {}; queryParameters.removeWhere((k, v) => v == null); final _headers = {}; - final Map? _data = null; + final _data = FormData(); + _data.fields.add(MapEntry( + 'username', + username, + )); + _data.fields.add(MapEntry( + 'password', + password, + )); + _data.fields.add(MapEntry( + 't', + t.toString(), + )); + if (setCookie != null) { + _data.fields.add(MapEntry( + 'set_cookie', + setCookie.toString(), + )); + } + if (httpOnly != null) { + _data.fields.add(MapEntry( + 'http_only', + httpOnly.toString(), + )); + } + if (secure != null) { + _data.fields.add(MapEntry( + 'secure', + secure.toString(), + )); + } final _result = await _dio .fetch>(_setStreamType>(Options( method: 'PUT', headers: _headers, extra: _extra, + contentType: 'multipart/form-data', ) .compose( _dio.options, @@ -174,15 +214,22 @@ class __EHApi implements _EHApi { @override Future> deleteToken({String? token}) async { const _extra = {}; - final queryParameters = {r'token': token}; + final queryParameters = {}; queryParameters.removeWhere((k, v) => v == null); final _headers = {}; - final Map? _data = null; + final _data = FormData(); + if (token != null) { + _data.fields.add(MapEntry( + 'token', + token, + )); + } final _result = await _dio .fetch>(_setStreamType>(Options( method: 'DELETE', headers: _headers, extra: _extra, + contentType: 'multipart/form-data', ) .compose( _dio.options, diff --git a/lib/auth.dart b/lib/auth.dart new file mode 100644 index 0000000..26f6eb9 --- /dev/null +++ b/lib/auth.dart @@ -0,0 +1,27 @@ +import 'api/status.dart'; +import 'api/user.dart'; +import 'globals.dart'; + +class AuthInfo { + AuthInfo(); + BUser? _user; + BUser? get user => _user; + ServerStatus? _status; + ServerStatus? get status => _status; + bool get isAuthed => (_user != null); + + Future getServerStatus() async { + _status = (await api.getStatus()).unwrap(); + } + + Future checkAuth() async { + final re = await api.getUser(); + if (re.ok) { + _user = re.data!; + } else { + _user = null; + } + await getServerStatus(); + return re.ok; + } +} diff --git a/lib/globals.dart b/lib/globals.dart index 2037eb4..d7e8dc1 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -5,6 +5,7 @@ import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'api/client.dart'; +import 'auth.dart'; final dio = Dio() ..options.validateStatus = (int? _) { @@ -48,3 +49,5 @@ EHApi get api { } return _api!; } + +final AuthInfo auth = AuthInfo(); diff --git a/lib/home.dart b/lib/home.dart index afe8484..aa3e1d2 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -2,8 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; import 'globals.dart'; +final _log = Logger("HomePage"); + class HomePage extends HookWidget { const HomePage({Key? key}) : super(key: key); @@ -20,6 +23,17 @@ class HomePage extends HookWidget { return; } initApi(baseUrl); + if (!auth.isAuthed) { + auth.checkAuth().then((re) { + if (!re) { + SchedulerBinding.instance.addPostFrameCallback((_) { + context.go(auth.status!.noUser ? "/create_root_user" : "/login"); + }); + } + }).catchError((err) { + _log.log(Level.SEVERE, "Failed to check auth info:", err); + }); + } return; }, []); return const Scaffold( diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c298e75..a8f2381 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,5 +1,9 @@ { "title": "E-Hentai Downloader Dashboard", "serverHost": "EH Downloader Server Host", - "apiPath": "API Path" + "apiPath": "API Path", + "username": "Username", + "password": "Password", + "save": "Save", + "login": "Login" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f13e927..438c0fa 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,5 +1,9 @@ { "title": "E-Hentai 下载器面板", "serverHost": "EH Downloader 服务器主机", - "apiPath": "API 路径" + "apiPath": "API 路径", + "username": "用户名", + "password": "密码", + "save": "保存", + "login": "登录" } diff --git a/lib/login.dart b/lib/login.dart new file mode 100644 index 0000000..bc9742f --- /dev/null +++ b/lib/login.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:go_router/go_router.dart'; +import 'globals.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({Key? key}) : super(key: key); + + static const String routeName = '/login'; + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final _formKey = GlobalKey(); + String _username = ""; + String _password = ""; + bool _passwordVisible = false; + bool _isValid = false; + bool _isLogin = false; + + @override + void initState() { + super.initState(); + _username = ""; + _password = ""; + _passwordVisible = false; + _isValid = false; + _isLogin = false; + } + + static bool _checkIsValid(String username, String password) { + return (username.isNotEmpty && password.isNotEmpty); + } + + void _usernameChanged(String value) { + bool isValid = _checkIsValid(value, _password); + setState(() { + _username = value; + _isValid = isValid; + }); + } + + void _passwordChanged(String value) { + bool isValid = _checkIsValid(_username, value); + setState(() { + _password = value; + _isValid = isValid; + }); + } + + void _passwordVisibleChanged() { + setState(() { + _passwordVisible = !_passwordVisible; + }); + } + + static Future _login(String username, String password) async { + String baseUrl = api.baseUrl!; + final u = Uri.parse(baseUrl); + final re = await api.createToken( + username: username, + password: password, + setCookie: true, + httpOnly: true, + secure: u.scheme == 'https'); + return re.ok; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + padding: const EdgeInsets.symmetric(horizontal: 100), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: TextFormField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: AppLocalizations.of(context)!.username, + ), + initialValue: _username, + onChanged: _usernameChanged, + )), + Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: TextFormField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: AppLocalizations.of(context)!.password, + suffixIcon: IconButton( + icon: Icon( + _passwordVisible + ? Icons.visibility + : Icons.visibility_off, + color: Theme.of(context).primaryColorDark, + ), + onPressed: _passwordVisibleChanged, + ), + ), + initialValue: _password, + onChanged: _passwordChanged, + obscureText: !_passwordVisible, + )), + ElevatedButton( + onPressed: _isValid && !_isLogin + ? () { + setState(() { + _isLogin = true; + }); + _login(_username, _password).then((re) { + if (re) { + context.go("/"); + } else { + setState(() { + _isLogin = false; + }); + } + }).catchError((e) { + setState(() { + _isLogin = false; + }); + }); + } + : null, + child: Text(AppLocalizations.of(context)!.login)), + ]))), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index cf84440..0bc4e06 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,13 @@ +import 'package:chinese_font_library/chinese_font_library.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; import 'package:window_manager/window_manager.dart'; import 'globals.dart'; import 'home.dart'; +import 'login.dart'; import 'set_server.dart'; import 'utils.dart'; @@ -18,9 +21,25 @@ final _router = GoRouter( path: SetServerPage.routeName, builder: (context, state) => const SetServerPage(), ), + GoRoute( + path: LoginPage.routeName, + builder: (context, state) => const LoginPage(), + ), ], ); +Future initLogger() async { + var logLevel = prefs.getInt("logLevel"); + var logLevelName = prefs.getString("logLevelName"); + if (logLevel != null && logLevelName != null) { + Logger.root.level = Level(logLevelName, logLevel); + } + Logger.root.onRecord.listen((record) { + print('${record.level.name}: ${record.time}: ${record.message}'); + }); + return; +} + void main() async { if (!kIsWeb) await prepareJar(); await preparePrefs(); @@ -28,6 +47,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); await windowManager.ensureInitialized(); } + await initLogger(); runApp(const MainApp()); } @@ -36,6 +56,7 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { + var theme = ThemeData(); return MaterialApp.router( routerConfig: _router, onGenerateTitle: (context) { @@ -47,6 +68,7 @@ class MainApp extends StatelessWidget { }, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, + theme: (kIsWeb || isWindows) ? theme.useSystemChineseFont() : theme, ); } } diff --git a/lib/set_server.dart b/lib/set_server.dart index 74ee6ad..4197042 100644 --- a/lib/set_server.dart +++ b/lib/set_server.dart @@ -96,7 +96,7 @@ class _SetServerPageState extends State { context.go('/'); } : null, - child: const Text("Save")), + child: Text(AppLocalizations.of(context)!.save)), ], ))), ); diff --git a/lib/utils.dart b/lib/utils.dart index a4ad4ba..488bac1 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,4 +1,6 @@ import 'dart:io' show Platform; import 'package:flutter/foundation.dart'; -bool get isDesktop => !kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS); +bool get isDesktop => + !kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS); +bool get isWindows => Platform.isWindows; diff --git a/pubspec.lock b/pubspec.lock index 9cd3b79..85f94cf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -121,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + chinese_font_library: + dependency: "direct main" + description: + name: chinese_font_library + sha256: aec5b5ab290c873251a9f7772b2101a851f4f87bf3b544b515a864de569ebcc0 + url: "https://pub.dev" + source: hosted + version: "1.0.2" clock: dependency: transitive description: @@ -309,6 +317,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -374,7 +390,7 @@ packages: source: hosted version: "2.1.1" logging: - dependency: transitive + dependency: "direct main" description: name: logging sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" diff --git a/pubspec.yaml b/pubspec.yaml index 2fd807b..4f1a1ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,6 +7,7 @@ environment: sdk: '>=3.0.5 <4.0.0' dependencies: + chinese_font_library: ^1.0.2 cryptography: ^2.5.0 cryptography_flutter: ^2.3.0 dio: ^5.3.2 @@ -19,6 +20,7 @@ dependencies: go_router: ^10.1.0 intl: any json_annotation: ^4.8.1 + logging: ^1.2.0 path_provider: ^2.1.0 retrofit: ^4.0.1 shared_preferences: ^2.2.0