This commit is contained in:
2023-08-30 14:44:58 +08:00
parent 8c655b0fd5
commit 6884beaf3d
14 changed files with 327 additions and 33 deletions

View File

@@ -19,4 +19,18 @@ class ApiResult<T> {
final String? error;
Map<String, dynamic> 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!);
}
}
}

View File

@@ -30,10 +30,11 @@ abstract class _EHApi {
factory _EHApi(Dio dio, {required String baseUrl}) = __EHApi;
@PUT('/user')
@MultiPart()
Future<ApiResult<int>> 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<ApiResult<BUser>> getUser(
{@Query("id") int? id, @Query("username") String? username});
@@ -42,19 +43,21 @@ abstract class _EHApi {
Future<ApiResult<ServerStatus>> getStatus();
@PUT('/token')
@MultiPart()
// ignore: unused_element
Future<ApiResult<Token>> _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<ApiResult<bool>> deleteToken({@Query("token") String? token});
@MultiPart()
Future<ApiResult<bool>> deleteToken({@Part(name: "token") String? token});
@GET('/token')
Future<ApiResult<TokenWithUserInfo>> getToken(
{@Query("token") String? token});

View File

@@ -26,20 +26,36 @@ class __EHApi implements _EHApi {
int? permissions,
}) async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{
r'name': name,
r'password': password,
r'is_admin': isAdmin,
r'permissions': permissions,
};
final queryParameters = <String, dynamic>{};
queryParameters.removeWhere((k, v) => v == null);
final _headers = <String, dynamic>{};
final Map<String, dynamic>? _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<Map<String, dynamic>>(_setStreamType<ApiResult<int>>(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 = <String, dynamic>{};
final queryParameters = <String, dynamic>{
r'username': username,
r'password': password,
r't': t,
r'set_cookie': setCookie,
r'http_only': httpOnly,
r'secure': secure,
};
final queryParameters = <String, dynamic>{};
queryParameters.removeWhere((k, v) => v == null);
final _headers = <String, dynamic>{};
final Map<String, dynamic>? _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<Map<String, dynamic>>(_setStreamType<ApiResult<Token>>(Options(
method: 'PUT',
headers: _headers,
extra: _extra,
contentType: 'multipart/form-data',
)
.compose(
_dio.options,
@@ -174,15 +214,22 @@ class __EHApi implements _EHApi {
@override
Future<ApiResult<bool>> deleteToken({String? token}) async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{r'token': token};
final queryParameters = <String, dynamic>{};
queryParameters.removeWhere((k, v) => v == null);
final _headers = <String, dynamic>{};
final Map<String, dynamic>? _data = null;
final _data = FormData();
if (token != null) {
_data.fields.add(MapEntry(
'token',
token,
));
}
final _result = await _dio
.fetch<Map<String, dynamic>>(_setStreamType<ApiResult<bool>>(Options(
method: 'DELETE',
headers: _headers,
extra: _extra,
contentType: 'multipart/form-data',
)
.compose(
_dio.options,

27
lib/auth.dart Normal file
View File

@@ -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<void> getServerStatus() async {
_status = (await api.getStatus()).unwrap();
}
Future<bool> checkAuth() async {
final re = await api.getUser();
if (re.ok) {
_user = re.data!;
} else {
_user = null;
}
await getServerStatus();
return re.ok;
}
}

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,9 @@
{
"title": "E-Hentai 下载器面板",
"serverHost": "EH Downloader 服务器主机",
"apiPath": "API 路径"
"apiPath": "API 路径",
"username": "用户名",
"password": "密码",
"save": "保存",
"login": "登录"
}

136
lib/login.dart Normal file
View File

@@ -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<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
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<bool> _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)),
]))),
);
}
}

View File

@@ -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<void> 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,
);
}
}

View File

@@ -96,7 +96,7 @@ class _SetServerPageState extends State<SetServerPage> {
context.go('/');
}
: null,
child: const Text("Save")),
child: Text(AppLocalizations.of(context)!.save)),
],
))),
);

View File

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

View File

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

View File

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