From 3c2201d00b1d2437170fc6e36c8e6e78b6b45e28 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Mon, 28 Aug 2023 22:05:10 +0800 Subject: [PATCH] Update --- lib/api/client.dart | 70 +++++++- lib/api/client.g.dart | 168 ++++++++++++++++-- lib/api/status.dart | 37 ++++ lib/api/status.g.dart | 39 ++++ lib/api/token.dart | 27 +++ lib/api/token.g.dart | 25 +++ lib/api/user.dart | 5 +- lib/api/user.g.dart | 4 +- lib/globals.dart | 19 +- lib/home.dart | 1 + lib/set_server.dart | 105 ++++++++--- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 16 ++ pubspec.yaml | 2 + 14 files changed, 471 insertions(+), 49 deletions(-) create mode 100644 lib/api/status.dart create mode 100644 lib/api/status.g.dart create mode 100644 lib/api/token.dart create mode 100644 lib/api/token.g.dart diff --git a/lib/api/client.dart b/lib/api/client.dart index 3630f98..2306e6e 100644 --- a/lib/api/client.dart +++ b/lib/api/client.dart @@ -1,19 +1,75 @@ +import 'dart:convert'; +import 'package:cryptography/cryptography.dart'; import 'package:dio/dio.dart'; import 'package:retrofit/retrofit.dart'; import 'api_result.dart'; +import 'status.dart'; +import 'token.dart'; import 'user.dart'; part 'client.g.dart'; -@RestApi(parser: Parser.FlutterCompute) +final _pbkdf2a = Pbkdf2( + macAlgorithm: Hmac.sha512(), + iterations: 210000, + bits: 512, +); + +final _pbkdf2b = Pbkdf2( + macAlgorithm: Hmac.sha512(), + iterations: 1000, + bits: 512, +); + +const _utf8Encoder = Utf8Encoder(); +final _salt = _utf8Encoder.convert("eh-downloader-salt"); + +@RestApi() abstract class EHApi { factory EHApi(Dio dio, {String baseUrl}) = _EHApi; + @PUT('/user') + Future> createUser( + @Query("name") String name, @Query("password") String password, + {@Query("is_admin") bool? isAdmin, + @Query("permissions") int? permissions}); @GET('/user') - Future> getUser(); - @GET('/user') - Future> getUserById(@Query("id") int id); - @GET('/user') - Future> getUserByUsername( - @Query("username") String username); + Future> getUser( + {@Query("id") int? id, @Query("username") String? username}); + + @GET('/status') + Future> getStatus(); + + @PUT('/token') + Future> _createToken( + {@Query("username") String username, + @Query("password") String password, + @Query("t") int t, + @Query("set_cookie") bool? setCookie, + @Query("http_only") bool? httpOnly, + @Query("secure") bool? secure}); + Future> createToken( + {required String username, + required String password, + bool? setCookie, + bool? httpOnly, + bool? secure}) async { + int t = DateTime.now().millisecondsSinceEpoch; + final p = + await _pbkdf2a.deriveKeyFromPassword(password: password, nonce: _salt); + final p2 = await _pbkdf2b.deriveKey( + secretKey: p, nonce: _utf8Encoder.convert(t.toString())); + final p3 = base64Encode(await p2.extractBytes()); + return await _createToken( + username: username, + password: p3, + t: t, + setCookie: setCookie, + httpOnly: httpOnly, + secure: secure); + } + @DELETE('/token') + Future> deleteToken({@Query("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 dc636ae..b6983d2 100644 --- a/lib/api/client.g.dart +++ b/lib/api/client.g.dart @@ -19,20 +19,98 @@ class _EHApi implements EHApi { String? baseUrl; @override - Future> getUser() async { + Future> createUser( + String name, + String password, { + bool? isAdmin, + int? permissions, + }) async { + const _extra = {}; + final queryParameters = { + r'name': name, + r'password': password, + r'is_admin': isAdmin, + r'permissions': permissions, + }; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final Map? _data = null; + final _result = await _dio + .fetch>(_setStreamType>(Options( + method: 'PUT', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/user', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = ApiResult.fromJson( + _result.data!, + (json) => json as int, + ); + return value; + } + + @override + Future> getUser({ + int? id, + String? username, + }) async { + const _extra = {}; + final queryParameters = { + r'id': id, + r'username': username, + }; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final Map? _data = null; + final _result = await _dio + .fetch>(_setStreamType>(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/user', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = ApiResult.fromJson( + _result.data!, + (json) => BUser.fromJson(json as Map), + ); + return value; + } + + @override + Future> getStatus() async { const _extra = {}; final queryParameters = {}; final _headers = {}; final Map? _data = null; - final _result = await _dio - .fetch>(_setStreamType>(Options( + final _result = await _dio.fetch>( + _setStreamType>(Options( method: 'GET', headers: _headers, extra: _extra, ) .compose( _dio.options, - '/user', + '/status', queryParameters: queryParameters, data: _data, ) @@ -41,25 +119,43 @@ class _EHApi implements EHApi { _dio.options.baseUrl, baseUrl, )))); - final value = await compute(deserializeApiResult, _result.data!); + final value = ApiResult.fromJson( + _result.data!, + (json) => ServerStatus.fromJson(json as Map), + ); return value; } @override - Future> getUserById(int id) async { + Future> _createToken({ + required String username, + required String password, + required int t, + bool? setCookie, + bool? httpOnly, + bool? secure, + }) async { const _extra = {}; - final queryParameters = {r'id': id}; + final queryParameters = { + r'username': username, + r'password': password, + r't': t, + r'set_cookie': setCookie, + r'http_only': httpOnly, + r'secure': secure, + }; + queryParameters.removeWhere((k, v) => v == null); final _headers = {}; final Map? _data = null; final _result = await _dio - .fetch>(_setStreamType>(Options( - method: 'GET', + .fetch>(_setStreamType>(Options( + method: 'PUT', headers: _headers, extra: _extra, ) .compose( _dio.options, - '/user', + '/token', queryParameters: queryParameters, data: _data, ) @@ -68,25 +164,29 @@ class _EHApi implements EHApi { _dio.options.baseUrl, baseUrl, )))); - final value = await compute(deserializeApiResult, _result.data!); + final value = ApiResult.fromJson( + _result.data!, + (json) => Token.fromJson(json as Map), + ); return value; } @override - Future> getUserByUsername(String username) async { + Future> deleteToken({String? token}) async { const _extra = {}; - final queryParameters = {r'username': username}; + final queryParameters = {r'token': token}; + queryParameters.removeWhere((k, v) => v == null); final _headers = {}; final Map? _data = null; final _result = await _dio - .fetch>(_setStreamType>(Options( - method: 'GET', + .fetch>(_setStreamType>(Options( + method: 'DELETE', headers: _headers, extra: _extra, ) .compose( _dio.options, - '/user', + '/token', queryParameters: queryParameters, data: _data, ) @@ -95,7 +195,41 @@ class _EHApi implements EHApi { _dio.options.baseUrl, baseUrl, )))); - final value = await compute(deserializeApiResult, _result.data!); + final value = ApiResult.fromJson( + _result.data!, + (json) => json as bool, + ); + return value; + } + + @override + Future> getToken({String? token}) async { + const _extra = {}; + final queryParameters = {r'token': token}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final Map? _data = null; + final _result = await _dio + .fetch>(_setStreamType>(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/token', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = ApiResult.fromJson( + _result.data!, + (json) => Token.fromJson(json as Map), + ); return value; } diff --git a/lib/api/status.dart b/lib/api/status.dart new file mode 100644 index 0000000..9ad6bc1 --- /dev/null +++ b/lib/api/status.dart @@ -0,0 +1,37 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'status.g.dart'; + +@JsonSerializable() +class MeilisearchInfo { + const MeilisearchInfo({ + required this.host, + required this.key, + }); + final String host; + final String key; + factory MeilisearchInfo.fromJson(Map json) => _$MeilisearchInfoFromJson(json); + Map toJson() => _$MeilisearchInfoToJson(this); +} + +@JsonSerializable() +class ServerStatus { + const ServerStatus({ + required this.ffmpegApiEnabled, + required this.ffmpegBinaryEnabled, + required this.meilisearchEnabled, + this.meilisearch, + required this.noUser, + }); + @JsonKey(name: 'ffmpeg_api_enabled') + final bool ffmpegApiEnabled; + @JsonKey(name: 'ffmpeg_binary_enabled') + final bool ffmpegBinaryEnabled; + @JsonKey(name: 'meilisearch_enabled') + final bool meilisearchEnabled; + final MeilisearchInfo? meilisearch; + @JsonKey(name: 'no_user') + final bool noUser; + factory ServerStatus.fromJson(Map json) => _$ServerStatusFromJson(json); + Map toJson() => _$ServerStatusToJson(this); +} diff --git a/lib/api/status.g.dart b/lib/api/status.g.dart new file mode 100644 index 0000000..333730f --- /dev/null +++ b/lib/api/status.g.dart @@ -0,0 +1,39 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'status.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MeilisearchInfo _$MeilisearchInfoFromJson(Map json) => + MeilisearchInfo( + host: json['host'] as String, + key: json['key'] as String, + ); + +Map _$MeilisearchInfoToJson(MeilisearchInfo instance) => + { + 'host': instance.host, + 'key': instance.key, + }; + +ServerStatus _$ServerStatusFromJson(Map json) => ServerStatus( + ffmpegApiEnabled: json['ffmpeg_api_enabled'] as bool, + ffmpegBinaryEnabled: json['ffmpeg_binary_enabled'] as bool, + meilisearchEnabled: json['meilisearch_enabled'] as bool, + meilisearch: json['meilisearch'] == null + ? null + : MeilisearchInfo.fromJson( + json['meilisearch'] as Map), + noUser: json['no_user'] as bool, + ); + +Map _$ServerStatusToJson(ServerStatus instance) => + { + 'ffmpeg_api_enabled': instance.ffmpegApiEnabled, + 'ffmpeg_binary_enabled': instance.ffmpegBinaryEnabled, + 'meilisearch_enabled': instance.meilisearchEnabled, + 'meilisearch': instance.meilisearch, + 'no_user': instance.noUser, + }; diff --git a/lib/api/token.dart b/lib/api/token.dart new file mode 100644 index 0000000..8e8eef7 --- /dev/null +++ b/lib/api/token.dart @@ -0,0 +1,27 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'token.g.dart'; + +@JsonSerializable() +class Token { + const Token ({ + required this.id, + required this.uid, + required this.token, + required this.expired, + required this.httpOnly, + required this.secure, + }); + final int id; + final int uid; + final String token; + @JsonKey(fromJson: _fromJson, toJson: _toJson) + final DateTime expired; + @JsonKey(name: 'http_only') + final bool httpOnly; + final bool secure; + static DateTime _fromJson(String d) => DateTime.parse(d); + static String _toJson(DateTime d) => d.toIso8601String(); + factory Token.fromJson(Map json) => _$TokenFromJson(json); + Map toJson() => _$TokenToJson(this); +} diff --git a/lib/api/token.g.dart b/lib/api/token.g.dart new file mode 100644 index 0000000..3f9aec2 --- /dev/null +++ b/lib/api/token.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'token.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Token _$TokenFromJson(Map json) => Token( + id: json['id'] as int, + uid: json['uid'] as int, + token: json['token'] as String, + expired: Token._fromJson(json['expired'] as String), + httpOnly: json['http_only'] as bool, + secure: json['secure'] as bool, + ); + +Map _$TokenToJson(Token instance) => { + 'id': instance.id, + 'uid': instance.uid, + 'token': instance.token, + 'expired': Token._toJson(instance.expired), + 'http_only': instance.httpOnly, + 'secure': instance.secure, + }; diff --git a/lib/api/user.dart b/lib/api/user.dart index 3f049c5..67839fe 100644 --- a/lib/api/user.dart +++ b/lib/api/user.dart @@ -18,12 +18,13 @@ class BUser { const BUser({ required this.id, required this.username, - required this.is_admin, + required this.isAdmin, required this.permissions, }); final int id; final String username; - final bool is_admin; + @JsonKey(name: 'is_admin') + final bool isAdmin; final UserPermission permissions; factory BUser.fromJson(Map json) => _$BUserFromJson(json); Map toJson() => _$BUserToJson(this); diff --git a/lib/api/user.g.dart b/lib/api/user.g.dart index 600b60e..97957be 100644 --- a/lib/api/user.g.dart +++ b/lib/api/user.g.dart @@ -9,14 +9,14 @@ part of 'user.dart'; BUser _$BUserFromJson(Map json) => BUser( id: json['id'] as int, username: json['username'] as String, - is_admin: json['is_admin'] as bool, + isAdmin: json['is_admin'] as bool, permissions: $enumDecode(_$UserPermissionEnumMap, json['permissions']), ); Map _$BUserToJson(BUser instance) => { 'id': instance.id, 'username': instance.username, - 'is_admin': instance.is_admin, + 'is_admin': instance.isAdmin, 'permissions': _$UserPermissionEnumMap[instance.permissions]!, }; diff --git a/lib/globals.dart b/lib/globals.dart index 4876acf..e1b0183 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -4,9 +4,11 @@ import 'package:dio/dio.dart'; 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'; -final dio = Dio(); +final dio = Dio()..options.validateStatus = (int? _) {return true;}; SharedPreferences? _prefs; +EHApi? _api; Future prepareJar() async { final Directory appDocDir = await getApplicationDocumentsDirectory(); @@ -27,3 +29,18 @@ SharedPreferences get prefs { } return _prefs!; } + +void initApi(String baseUrl) { + _api = EHApi(dio, baseUrl: baseUrl); +} + +bool get apiInited { + return _api != null; +} + +EHApi get api { + if (_api == null) { + throw Exception('EHApi not initialized'); + } + return _api!; +} diff --git a/lib/home.dart b/lib/home.dart index a8ceba2..afe8484 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -19,6 +19,7 @@ class HomePage extends HookWidget { }); return; } + initApi(baseUrl); return; }, []); return const Scaffold( diff --git a/lib/set_server.dart b/lib/set_server.dart index 7284b0d..3e2ee3c 100644 --- a/lib/set_server.dart +++ b/lib/set_server.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'globals.dart'; class SetServerPage extends StatefulWidget { const SetServerPage({Key? key}) : super(key: key); @@ -10,29 +12,92 @@ class SetServerPage extends StatefulWidget { } class _SetServerPageState extends State { + String _serverUrl = ""; + String _apiPath = "/api/"; + bool _isValid = false; + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + String? baseUrl = prefs.getString('baseUrl'); + if (baseUrl != null) { + try { + Uri url = Uri.parse(baseUrl); + _serverUrl = url.origin; + _apiPath = url.path; + _isValid = true; + } catch (e) { + // Do nothing. + } + } + } + + static bool _checkIsValid(String serverUrl, String apiPath) { + try { + Uri url = Uri.parse(serverUrl + apiPath); + return url.isAbsolute; + } catch (e) { + return false; + } + } + + void _serverUrlChanged(String value) { + bool isValid = _checkIsValid(value, _apiPath); + setState(() { + _serverUrl = value; + _isValid = isValid; + }); + } + + void _apiPathChanged(String value) { + bool isValid = _checkIsValid(_serverUrl, value); + setState(() { + _apiPath = value; + _isValid = isValid; + }); + } + @override Widget build(BuildContext context) { return Scaffold( - body: Center( - child: Column( - children: [ - const Text('EH Downloader server url:'), - const TextField( - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Server URL', - ), - ), - const Text('API path'), - TextField( - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'API Path', - ), - controller: TextEditingController(text: "/api"), - ), - ], - )), + 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: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'EH Downloader Server URL', + ), + initialValue: _serverUrl, + onChanged: _serverUrlChanged, + )), + Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: TextFormField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'API Path', + ), + initialValue: _apiPath, + onChanged: _apiPathChanged, + )), + ElevatedButton( + onPressed: _isValid + ? () { + prefs.setString('baseUrl', _serverUrl + _apiPath); + context.go('/'); + } + : null, + child: const Text("Save")), + ], + ))), ); } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b8e2b22..2fdb1c1 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import cryptography_flutter import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + CryptographyFlutterPlugin.register(with: registry.registrar(forPlugin: "CryptographyFlutterPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index d82f460..83b58f2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -169,6 +169,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + cryptography: + dependency: "direct main" + description: + name: cryptography + sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35 + url: "https://pub.dev" + source: hosted + version: "2.5.0" + cryptography_flutter: + dependency: "direct main" + description: + name: cryptography_flutter + sha256: a66ce021e0b600688c2d51b0594cb156ee677ce9bfbc981b62219ad577dd302e + url: "https://pub.dev" + source: hosted + version: "2.3.0" dart_style: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4dbf8b9..cd54edc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,6 +7,8 @@ environment: sdk: '>=3.0.5 <4.0.0' dependencies: + cryptography: ^2.5.0 + cryptography_flutter: ^2.3.0 dio: ^5.3.2 dio_cookie_manager: ^3.1.0+1 flutter: