diff --git a/lib/data/model/app/error.dart b/lib/data/model/app/error.dart index ad6ecf75..01ec58c4 100644 --- a/lib/data/model/app/error.dart +++ b/lib/data/model/app/error.dart @@ -56,7 +56,7 @@ class WebdavErr extends Err { String? get solution => null; } -enum PveErrType { unknown, net, loginFailed } +enum PveErrType { unknown, net, loginFailed, needTfa } class PveErr extends Err { const PveErr({required super.type, super.message}); diff --git a/lib/data/model/server/custom.dart b/lib/data/model/server/custom.dart index 0edf4db7..10ec85ae 100644 --- a/lib/data/model/server/custom.dart +++ b/lib/data/model/server/custom.dart @@ -11,6 +11,8 @@ final class ServerCustom { final bool pveIgnoreCert; + final String? pvePwd; + /// {"title": "cmd"} final Map? cmds; @@ -28,6 +30,7 @@ final class ServerCustom { //this.temperature, this.pveAddr, this.pveIgnoreCert = false, + this.pvePwd, this.cmds, this.preferTempDev, this.logoUrl, @@ -45,6 +48,7 @@ final class ServerCustom { //other.temperature == temperature && other.pveAddr == pveAddr && other.pveIgnoreCert == pveIgnoreCert && + other.pvePwd == pvePwd && other.cmds == cmds && other.preferTempDev == preferTempDev && other.logoUrl == logoUrl && @@ -57,6 +61,7 @@ final class ServerCustom { //temperature.hashCode ^ pveAddr.hashCode ^ pveIgnoreCert.hashCode ^ + pvePwd.hashCode ^ cmds.hashCode ^ preferTempDev.hashCode ^ logoUrl.hashCode ^ diff --git a/lib/data/model/server/custom.g.dart b/lib/data/model/server/custom.g.dart index c67bbeb4..1619d62d 100644 --- a/lib/data/model/server/custom.g.dart +++ b/lib/data/model/server/custom.g.dart @@ -9,6 +9,7 @@ part of 'custom.dart'; ServerCustom _$ServerCustomFromJson(Map json) => ServerCustom( pveAddr: json['pveAddr'] as String?, pveIgnoreCert: json['pveIgnoreCert'] as bool? ?? false, + pvePwd: json['pvePwd'] as String?, cmds: (json['cmds'] as Map?)?.map( (k, e) => MapEntry(k, e as String), ), @@ -22,6 +23,7 @@ Map _$ServerCustomToJson(ServerCustom instance) => { 'pveAddr': ?instance.pveAddr, 'pveIgnoreCert': instance.pveIgnoreCert, + 'pvePwd': ?instance.pvePwd, 'cmds': ?instance.cmds, 'preferTempDev': ?instance.preferTempDev, 'logoUrl': ?instance.logoUrl, diff --git a/lib/data/model/server/pve.dart b/lib/data/model/server/pve.dart index 077a551d..0c81e88d 100644 --- a/lib/data/model/server/pve.dart +++ b/lib/data/model/server/pve.dart @@ -293,6 +293,10 @@ final class PveStorage extends PveResIface implements PveCtrlIface { }); static PveStorage fromJson(Map json) { + final rawContent = json['content'] as String?; + final contentParts = rawContent?.split(','); + contentParts?.sort(); + final content = contentParts?.join(',') ?? rawContent ?? ''; return PveStorage( id: json['id'], type: PveResType.storage, @@ -300,7 +304,7 @@ final class PveStorage extends PveResIface implements PveCtrlIface { node: json['node'], status: json['status'], plugintype: json['plugintype'], - content: json['content'], + content: content, shared: json['shared'], disk: json['disk'], maxdisk: json['maxdisk'], diff --git a/lib/data/provider/container.g.dart b/lib/data/provider/container.g.dart index 77647101..8254ac89 100644 --- a/lib/data/provider/container.g.dart +++ b/lib/data/provider/container.g.dart @@ -58,7 +58,7 @@ final class ContainerNotifierProvider } } -String _$containerNotifierHash() => r'85457ec75264199c284572ee45beeaccba2044a1'; +String _$containerNotifierHash() => r'2f8eb969f0e66e28f60b6fc11169e8f28315ed32'; final class ContainerNotifierFamily extends $Family with diff --git a/lib/data/provider/pve.dart b/lib/data/provider/pve.dart index 746fc998..c40d1ddc 100644 --- a/lib/data/provider/pve.dart +++ b/lib/data/provider/pve.dart @@ -8,7 +8,6 @@ import 'package:dio/io.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/server/pve.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; @@ -19,6 +18,13 @@ part 'pve.g.dart'; typedef PveCtrlFunc = Future Function(String node, String id); +enum PveLoadingStep { + none, + forwarding, + loggingIn, + fetchingData, +} + @freezed abstract class PveState with _$PveState { const factory PveState({ @@ -27,43 +33,51 @@ abstract class PveState with _$PveState { @Default(null) String? release, @Default(false) bool isBusy, @Default(false) bool isConnected, + @Default(PveLoadingStep.none) PveLoadingStep loadingStep, }) = _PveState; } @riverpod class PveNotifier extends _$PveNotifier { - late final Spi spi; - late String addr; - late final SSHClient _client; - late final ServerSocket _serverSocket; + String? addr; + ServerSocket? _serverSocket; final List _forwards = []; int _localPort = 0; - late final Dio session; - late final bool _ignoreCert; + Dio? _session; + bool _ignoreCert = false; + + Dio get session => _session!; + String get addrValue => addr!; + + SSHClient get _client { + final serverState = ref.read(serverProvider(spiParam.id)); + final client = serverState.client; + if (client == null) { + throw PveErr(type: PveErrType.net, message: 'Server client is null'); + } + return client; + } @override PveState build(Spi spiParam) { - spi = spiParam; - final serverState = ref.watch(serverProvider(spi.id)); - final client = serverState.client; - if (client == null) { - return const PveState(error: PveErr(type: PveErrType.net, message: 'Server client is null')); + ref.onDispose(() => dispose()); + final serverState = ref.watch(serverProvider(spiParam.id)); + if (serverState.client == null) { + return const PveState(loadingStep: PveLoadingStep.forwarding); } - _client = client; - final addr = spi.custom?.pveAddr; - if (addr == null) { + final pveAddr = spiParam.custom?.pveAddr; + if (pveAddr == null) { return PveState(error: PveErr(type: PveErrType.net, message: 'PVE address is null')); } - this.addr = addr; - _ignoreCert = spi.custom?.pveIgnoreCert ?? false; + addr = pveAddr; + _ignoreCert = spiParam.custom?.pveIgnoreCert ?? false; _initSession(); - // Async initialization Future.microtask(() => _init()); - return const PveState(); + return const PveState(loadingStep: PveLoadingStep.forwarding); } void _initSession() { - session = Dio() + _session = Dio() ..httpClientAdapter = IOHttpClientAdapter( createHttpClient: () { final client = HttpClient(); @@ -79,35 +93,53 @@ class PveNotifier extends _$PveNotifier { bool get onlyOneNode => state.data?.nodes.length == 1; + Future reconnect() async { + state = state.copyWith(error: null, isConnected: false, loadingStep: PveLoadingStep.forwarding); + await _init(); + } + Future _init() async { try { + if (!ref.mounted) return; + state = state.copyWith(loadingStep: PveLoadingStep.forwarding); await _forward(); + if (!ref.mounted) return; + state = state.copyWith(loadingStep: PveLoadingStep.loggingIn); await _login(); + if (!ref.mounted) return; + state = state.copyWith(loadingStep: PveLoadingStep.fetchingData); await _getRelease(); + if (!ref.mounted) return; state = state.copyWith(isConnected: true); - } on PveErr { - state = state.copyWith(error: PveErr(type: PveErrType.loginFailed, message: l10n.pveLoginFailed)); + await list(); + if (!ref.mounted) return; + state = state.copyWith(loadingStep: PveLoadingStep.none); + } on PveErr catch (e) { + if (!ref.mounted) return; + state = state.copyWith(error: e, loadingStep: PveLoadingStep.none); } catch (e, s) { + if (!ref.mounted) return; Loggers.app.warning('PVE init failed', e, s); - state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString())); + state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString()), loadingStep: PveLoadingStep.none); } } Future _forward() async { - final url = Uri.parse(addr); + final url = Uri.parse(addrValue); if (_localPort == 0) { _serverSocket = await ServerSocket.bind('localhost', 0); - _localPort = _serverSocket.port; - _serverSocket.listen((socket) async { - final forward = await _client.forwardLocal(url.host, url.port); - _forwards.add(forward); - forward.stream.cast>().pipe(socket); - socket.cast>().pipe(forward.sink); + _localPort = _serverSocket!.port; + _serverSocket!.listen((socket) async { + try { + final forward = await _client.forwardLocal(url.host, url.port); + _forwards.add(forward); + forward.stream.cast>().pipe(socket); + socket.cast>().pipe(forward.sink); + } catch (e, s) { + Loggers.app.warning('PVE forward failed', e, s); + socket.destroy(); + } }); - final newUrl = Uri.parse( - addr, - ).replace(host: 'localhost', port: _localPort).toString(); - dprint('Forwarding $newUrl to $addr'); } } @@ -116,15 +148,6 @@ class PveNotifier extends _$PveNotifier { String? proxyHost, int? proxyPort, ) async { - /* final serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, 0); - final _localPort = serverSocket.port; - serverSocket.listen((socket) async { - final forward = await _client.forwardLocal(url.host, url.port); - forwards.add(forward); - forward.stream.cast>().pipe(socket); - socket.cast>().pipe(forward.sink); - });*/ - if (url.isScheme('https')) { return SecureSocket.startConnect( 'localhost', @@ -137,11 +160,16 @@ class PveNotifier extends _$PveNotifier { } Future _login() async { + final useKeyAuth = spiParam.keyId != null; + final password = useKeyAuth ? spiParam.custom?.pvePwd : spiParam.pwd; + if (password == null) { + throw PveErr(type: PveErrType.loginFailed, message: 'PVE password is required. Please set it in server settings.'); + } final resp = await session.post( - '$addr/api2/extjs/access/ticket', + '$addrValue/api2/extjs/access/ticket', data: { - 'username': spi.user, - 'password': spi.pwd, + 'username': spiParam.user, + 'password': password, 'realm': 'pam', 'new-format': '1', }, @@ -149,21 +177,26 @@ class PveNotifier extends _$PveNotifier { headers: {HttpHeaders.contentTypeHeader: Headers.jsonContentType}, ), ); - try { - final ticket = resp.data['data']['ticket']; - session.options.headers['CSRFPreventionToken'] = - resp.data['data']['CSRFPreventionToken']; - session.options.headers['Cookie'] = 'PVEAuthCookie=$ticket'; - } catch (e) { - throw PveErr(type: PveErrType.loginFailed, message: e.toString()); + + final data = resp.data['data']; + if (data['NeedTFA'] == 1) { + throw PveErr(type: PveErrType.needTfa, message: 'Two-factor authentication is not supported yet. Please disable OTP on the PVE server and try again.'); } + + _setAuthHeaders(data); + } + + void _setAuthHeaders(Map data) { + final ticket = data['ticket']; + session.options.headers['CSRFPreventionToken'] = data['CSRFPreventionToken']; + session.options.headers['Cookie'] = 'PVEAuthCookie=$ticket'; } /// Returns true if the PVE version is 8.0 or later Future _getRelease() async { - final resp = await session.get('$addr/api2/extjs/version'); + final resp = await session.get('$addrValue/api2/extjs/version'); final version = resp.data['data']['release'] as String?; - if (version != null) { + if (version != null && ref.mounted) { state = state.copyWith(release: version); } } @@ -172,25 +205,29 @@ class PveNotifier extends _$PveNotifier { if (!state.isConnected || state.isBusy) return; state = state.copyWith(isBusy: true); try { - final resp = await session.get('$addr/api2/json/cluster/resources'); + final resp = await session.get('$addrValue/api2/json/cluster/resources'); final res = resp.data['data'] as List; final result = await Computer.shared.start(PveRes.parse, ( res, state.data, )); + if (!ref.mounted) return; state = state.copyWith(data: result, error: null); } catch (e) { + if (!ref.mounted) return; Loggers.app.warning('PVE list failed', e); state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString())); } finally { - state = state.copyWith(isBusy: false); + if (ref.mounted) { + state = state.copyWith(isBusy: false); + } } } Future reboot(String node, String id) async { if (!state.isConnected) return false; final resp = await session.post( - '$addr/api2/json/nodes/$node/$id/status/reboot', + '$addrValue/api2/json/nodes/$node/$id/status/reboot', ); final success = _isCtrlSuc(resp); if (success) await list(); // Refresh data @@ -200,7 +237,7 @@ class PveNotifier extends _$PveNotifier { Future start(String node, String id) async { if (!state.isConnected) return false; final resp = await session.post( - '$addr/api2/json/nodes/$node/$id/status/start', + '$addrValue/api2/json/nodes/$node/$id/status/start', ); final success = _isCtrlSuc(resp); if (success) await list(); // Refresh data @@ -210,7 +247,7 @@ class PveNotifier extends _$PveNotifier { Future stop(String node, String id) async { if (!state.isConnected) return false; final resp = await session.post( - '$addr/api2/json/nodes/$node/$id/status/stop', + '$addrValue/api2/json/nodes/$node/$id/status/stop', ); final success = _isCtrlSuc(resp); if (success) await list(); // Refresh data @@ -220,7 +257,7 @@ class PveNotifier extends _$PveNotifier { Future shutdown(String node, String id) async { if (!state.isConnected) return false; final resp = await session.post( - '$addr/api2/json/nodes/$node/$id/status/shutdown', + '$addrValue/api2/json/nodes/$node/$id/status/shutdown', ); final success = _isCtrlSuc(resp); if (success) await list(); // Refresh data @@ -233,7 +270,7 @@ class PveNotifier extends _$PveNotifier { Future dispose() async { try { - await _serverSocket.close(); + await _serverSocket?.close(); } catch (e, s) { Loggers.app.warning('Failed to close server socket', e, s); } diff --git a/lib/data/provider/pve.freezed.dart b/lib/data/provider/pve.freezed.dart index dc07269a..884962b0 100644 --- a/lib/data/provider/pve.freezed.dart +++ b/lib/data/provider/pve.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$PveState { - PveErr? get error; PveRes? get data; String? get release; bool get isBusy; bool get isConnected; + PveErr? get error; PveRes? get data; String? get release; bool get isBusy; bool get isConnected; PveLoadingStep get loadingStep; /// Create a copy of PveState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -25,16 +25,16 @@ $PveStateCopyWith get copyWith => _$PveStateCopyWithImpl(thi @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.loadingStep, loadingStep) || other.loadingStep == loadingStep)); } @override -int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected); +int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected,loadingStep); @override String toString() { - return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected)'; + return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected, loadingStep: $loadingStep)'; } @@ -45,7 +45,7 @@ abstract mixin class $PveStateCopyWith<$Res> { factory $PveStateCopyWith(PveState value, $Res Function(PveState) _then) = _$PveStateCopyWithImpl; @useResult $Res call({ - PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected + PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected, PveLoadingStep loadingStep }); @@ -62,14 +62,15 @@ class _$PveStateCopyWithImpl<$Res> /// Create a copy of PveState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,Object? loadingStep = null,}) { return _then(_self.copyWith( error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as PveErr?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable as PveRes?,release: freezed == release ? _self.release : release // ignore: cast_nullable_to_non_nullable as String?,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable -as bool, +as bool,loadingStep: null == loadingStep ? _self.loadingStep : loadingStep // ignore: cast_nullable_to_non_nullable +as PveLoadingStep, )); } @@ -154,10 +155,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected, PveLoadingStep loadingStep)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _PveState() when $default != null: -return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _: +return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected,_that.loadingStep);case _: return orElse(); } @@ -175,10 +176,10 @@ return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnec /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected, PveLoadingStep loadingStep) $default,) {final _that = this; switch (_that) { case _PveState(): -return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _: +return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected,_that.loadingStep);case _: throw StateError('Unexpected subclass'); } @@ -195,10 +196,10 @@ return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnec /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected, PveLoadingStep loadingStep)? $default,) {final _that = this; switch (_that) { case _PveState() when $default != null: -return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _: +return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected,_that.loadingStep);case _: return null; } @@ -210,7 +211,7 @@ return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnec class _PveState implements PveState { - const _PveState({this.error = null, this.data = null, this.release = null, this.isBusy = false, this.isConnected = false}); + const _PveState({this.error = null, this.data = null, this.release = null, this.isBusy = false, this.isConnected = false, this.loadingStep = PveLoadingStep.none}); @override@JsonKey() final PveErr? error; @@ -218,6 +219,7 @@ class _PveState implements PveState { @override@JsonKey() final String? release; @override@JsonKey() final bool isBusy; @override@JsonKey() final bool isConnected; +@override@JsonKey() final PveLoadingStep loadingStep; /// Create a copy of PveState /// with the given fields replaced by the non-null parameter values. @@ -229,16 +231,16 @@ _$PveStateCopyWith<_PveState> get copyWith => __$PveStateCopyWithImpl<_PveState> @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.loadingStep, loadingStep) || other.loadingStep == loadingStep)); } @override -int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected); +int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected,loadingStep); @override String toString() { - return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected)'; + return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected, loadingStep: $loadingStep)'; } @@ -249,7 +251,7 @@ abstract mixin class _$PveStateCopyWith<$Res> implements $PveStateCopyWith<$Res> factory _$PveStateCopyWith(_PveState value, $Res Function(_PveState) _then) = __$PveStateCopyWithImpl; @override @useResult $Res call({ - PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected + PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected, PveLoadingStep loadingStep }); @@ -266,14 +268,15 @@ class __$PveStateCopyWithImpl<$Res> /// Create a copy of PveState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,Object? loadingStep = null,}) { return _then(_PveState( error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as PveErr?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable as PveRes?,release: freezed == release ? _self.release : release // ignore: cast_nullable_to_non_nullable as String?,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable -as bool, +as bool,loadingStep: null == loadingStep ? _self.loadingStep : loadingStep // ignore: cast_nullable_to_non_nullable +as PveLoadingStep, )); } diff --git a/lib/data/provider/pve.g.dart b/lib/data/provider/pve.g.dart index cad05f89..476c92d1 100644 --- a/lib/data/provider/pve.g.dart +++ b/lib/data/provider/pve.g.dart @@ -58,7 +58,7 @@ final class PveNotifierProvider } } -String _$pveNotifierHash() => r'1e71faadee074b9c07bee731ef4ae6505e791967'; +String _$pveNotifierHash() => r'a66699f64eae680064a1904f475d0a241d6cb3f8'; final class PveNotifierFamily extends $Family with $ClassFamilyOverride { diff --git a/lib/data/provider/server/all.g.dart b/lib/data/provider/server/all.g.dart index 2fefa053..bf1fc413 100644 --- a/lib/data/provider/server/all.g.dart +++ b/lib/data/provider/server/all.g.dart @@ -41,7 +41,7 @@ final class ServersNotifierProvider } } -String _$serversNotifierHash() => r'dc5da44f9bd8d8dcfba3e6e932cca3e2f379e582'; +String _$serversNotifierHash() => r'277d1b219235f14bcc1b82a1e16260c2f28decdb'; abstract class _$ServersNotifier extends $Notifier { ServersState build(); diff --git a/lib/data/provider/server/single.g.dart b/lib/data/provider/server/single.g.dart index bfc9f2bb..3b0d4dc1 100644 --- a/lib/data/provider/server/single.g.dart +++ b/lib/data/provider/server/single.g.dart @@ -58,7 +58,7 @@ final class ServerNotifierProvider } } -String _$serverNotifierHash() => r'04b1beef4d96242fd10d5b523c6f5f17eb774bae'; +String _$serverNotifierHash() => r'1bda6d0a9688ab843cf30803dafe3400379dc5c3'; final class ServerNotifierFamily extends $Family with diff --git a/lib/data/provider/systemd.g.dart b/lib/data/provider/systemd.g.dart index 8ef2a6e0..1401c48f 100644 --- a/lib/data/provider/systemd.g.dart +++ b/lib/data/provider/systemd.g.dart @@ -58,7 +58,7 @@ final class SystemdNotifierProvider } } -String _$systemdNotifierHash() => r'030d556efc3d897419cd3462d37cb705813e24c7'; +String _$systemdNotifierHash() => r'd8b36c60dff036e98196ad4d084e4b2ae3a65e32'; final class SystemdNotifierFamily extends $Family with diff --git a/lib/data/res/github_id.dart b/lib/data/res/github_id.dart index 72269dea..5ab5ee4e 100644 --- a/lib/data/res/github_id.dart +++ b/lib/data/res/github_id.dart @@ -151,6 +151,7 @@ abstract final class GithubIds { 'Yinhono', 'kuvaldini', 'aliferne', + 'canronglan', }; } diff --git a/lib/generated/l10n/l10n.dart b/lib/generated/l10n/l10n.dart index 6143f63c..136d462f 100644 --- a/lib/generated/l10n/l10n.dart +++ b/lib/generated/l10n/l10n.dart @@ -1050,12 +1050,54 @@ abstract class AppLocalizations { /// **'Login failed. Unable to authenticate with username/password from server configuration for Linux PAM login.'** String get pveLoginFailed; + /// No description provided for @pveOtpRequired. + /// + /// In en, this message translates to: + /// **'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'** + String get pveOtpRequired; + /// No description provided for @pveVersionLow. /// /// In en, this message translates to: /// **'This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.'** String get pveVersionLow; + /// No description provided for @pveLoadingForwarding. + /// + /// In en, this message translates to: + /// **'Establishing SSH tunnel...'** + String get pveLoadingForwarding; + + /// No description provided for @pveLoadingLogin. + /// + /// In en, this message translates to: + /// **'Authenticating with PVE...'** + String get pveLoadingLogin; + + /// No description provided for @pveLoadingData. + /// + /// In en, this message translates to: + /// **'Fetching cluster data...'** + String get pveLoadingData; + + /// No description provided for @pveLoadingConnect. + /// + /// In en, this message translates to: + /// **'Connecting...'** + String get pveLoadingConnect; + + /// No description provided for @pvePassword. + /// + /// In en, this message translates to: + /// **'PVE Password'** + String get pvePassword; + + /// No description provided for @pvePasswordHint. + /// + /// In en, this message translates to: + /// **'Required when using key-based SSH authentication'** + String get pvePasswordHint; + /// No description provided for @read. /// /// In en, this message translates to: diff --git a/lib/generated/l10n/l10n_de.dart b/lib/generated/l10n/l10n_de.dart index 0add8880..42785d35 100644 --- a/lib/generated/l10n/l10n_de.dart +++ b/lib/generated/l10n/l10n_de.dart @@ -538,10 +538,33 @@ class AppLocalizationsDe extends AppLocalizations { String get pveLoginFailed => 'Anmeldung fehlgeschlagen. Kann nicht mit Benutzername/Passwort aus der Serverkonfiguration angemeldet werden, um sich über Linux PAM anzumelden.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Diese Funktion befindet sich derzeit in der Testphase und wurde nur auf PVE 8+ getestet. Bitte verwenden Sie sie mit Vorsicht.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Lesen'; diff --git a/lib/generated/l10n/l10n_en.dart b/lib/generated/l10n/l10n_en.dart index 43ee9a28..a2979309 100644 --- a/lib/generated/l10n/l10n_en.dart +++ b/lib/generated/l10n/l10n_en.dart @@ -535,10 +535,33 @@ class AppLocalizationsEn extends AppLocalizations { String get pveLoginFailed => 'Login failed. Unable to authenticate with username/password from server configuration for Linux PAM login.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Read'; diff --git a/lib/generated/l10n/l10n_es.dart b/lib/generated/l10n/l10n_es.dart index 308c6812..2bfd80a3 100644 --- a/lib/generated/l10n/l10n_es.dart +++ b/lib/generated/l10n/l10n_es.dart @@ -540,10 +540,33 @@ class AppLocalizationsEs extends AppLocalizations { String get pveLoginFailed => 'Fallo al iniciar sesión. No se puede autenticar con el nombre de usuario/contraseña de la configuración del servidor para el inicio de sesión de Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Esta función está actualmente en fase de prueba y solo se ha probado en PVE 8+. Úsela con precaución.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Leer'; diff --git a/lib/generated/l10n/l10n_fr.dart b/lib/generated/l10n/l10n_fr.dart index fbabaf75..2f39be8c 100644 --- a/lib/generated/l10n/l10n_fr.dart +++ b/lib/generated/l10n/l10n_fr.dart @@ -542,10 +542,33 @@ class AppLocalizationsFr extends AppLocalizations { String get pveLoginFailed => 'Échec de la connexion. Impossible d\'authentifier avec le nom d\'utilisateur / mot de passe de la configuration du serveur pour la connexion Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Cette fonctionnalité est actuellement en phase de test et n\'a été testée que sur PVE 8+. Veuillez l\'utiliser avec prudence.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Lire'; diff --git a/lib/generated/l10n/l10n_id.dart b/lib/generated/l10n/l10n_id.dart index 9718077f..22bc3978 100644 --- a/lib/generated/l10n/l10n_id.dart +++ b/lib/generated/l10n/l10n_id.dart @@ -535,10 +535,33 @@ class AppLocalizationsId extends AppLocalizations { String get pveLoginFailed => 'Login gagal. Tidak dapat mengautentikasi dengan nama pengguna/kata sandi dari konfigurasi server untuk login Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Fitur ini saat ini sedang dalam tahap pengujian dan hanya diuji pada PVE 8+. Gunakan dengan hati-hati.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Baca'; diff --git a/lib/generated/l10n/l10n_it.dart b/lib/generated/l10n/l10n_it.dart index 4e8bf6b4..e9c10586 100644 --- a/lib/generated/l10n/l10n_it.dart +++ b/lib/generated/l10n/l10n_it.dart @@ -536,10 +536,33 @@ class AppLocalizationsIt extends AppLocalizations { String get pveLoginFailed => 'Accesso fallito. Impossibile autenticarsi con nome utente/password dalla configurazione del server per l\'accesso Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Questa funzionalità è attualmente nella fase di test ed è stata testata solo su PVE 8+. Usala con cautela.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Leggi'; diff --git a/lib/generated/l10n/l10n_ja.dart b/lib/generated/l10n/l10n_ja.dart index ef2eb38e..47a7697d 100644 --- a/lib/generated/l10n/l10n_ja.dart +++ b/lib/generated/l10n/l10n_ja.dart @@ -517,9 +517,32 @@ class AppLocalizationsJa extends AppLocalizations { String get pveLoginFailed => 'ログインに失敗しました。Linux PAMログインのためにサーバー構成からのユーザー名/パスワードで認証できません。'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'この機能は現在テスト段階にあり、PVE 8+でのみテストされています。ご利用の際は慎重に。'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => '読み取り'; diff --git a/lib/generated/l10n/l10n_ko.dart b/lib/generated/l10n/l10n_ko.dart index b01e364a..25c5376c 100644 --- a/lib/generated/l10n/l10n_ko.dart +++ b/lib/generated/l10n/l10n_ko.dart @@ -514,10 +514,33 @@ class AppLocalizationsKo extends AppLocalizations { String get pveLoginFailed => '로그인에 실패했습니다. 서버 설정의 사용자 이름/비밀번호로 Linux PAM 인증을 할 수 없습니다.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => '이 기능은 현재 테스트 단계이며 PVE 8+에서만 테스트되었습니다. 주의하여 사용해 주세요.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => '읽기'; diff --git a/lib/generated/l10n/l10n_nl.dart b/lib/generated/l10n/l10n_nl.dart index 2ffbbc2a..86396c90 100644 --- a/lib/generated/l10n/l10n_nl.dart +++ b/lib/generated/l10n/l10n_nl.dart @@ -537,10 +537,33 @@ class AppLocalizationsNl extends AppLocalizations { String get pveLoginFailed => 'Aanmelden mislukt. Kan niet authenticeren met gebruikersnaam/wachtwoord van serverconfiguratie voor Linux PAM-login.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Deze functie bevindt zich momenteel in de testfase en is alleen getest op PVE 8+. Gebruik het met voorzichtigheid.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Lezen'; diff --git a/lib/generated/l10n/l10n_pt.dart b/lib/generated/l10n/l10n_pt.dart index b5fefa23..d2cbc7cc 100644 --- a/lib/generated/l10n/l10n_pt.dart +++ b/lib/generated/l10n/l10n_pt.dart @@ -535,10 +535,33 @@ class AppLocalizationsPt extends AppLocalizations { String get pveLoginFailed => 'Falha no login. Não é possível autenticar com o nome de usuário/senha da configuração do servidor para login no Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Esta funcionalidade está atualmente em fase de teste e foi testada apenas no PVE 8+. Por favor, use com cautela.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Leitura'; diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index cd3c553f..cda82788 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -538,10 +538,33 @@ class AppLocalizationsRu extends AppLocalizations { String get pveLoginFailed => 'Ошибка входа. Невозможно аутентифицироваться с помощью имени пользователя/пароля из конфигурации сервера для входа в Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Эта функция в настоящее время находится на стадии тестирования и была протестирована только на PVE 8+. Используйте ее с осторожностью.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Чтение'; diff --git a/lib/generated/l10n/l10n_tr.dart b/lib/generated/l10n/l10n_tr.dart index 145acfd2..c8f5f918 100644 --- a/lib/generated/l10n/l10n_tr.dart +++ b/lib/generated/l10n/l10n_tr.dart @@ -535,10 +535,33 @@ class AppLocalizationsTr extends AppLocalizations { String get pveLoginFailed => 'Giriş başarısız. Linux PAM girişi için sunucu yapılandırmasındaki kullanıcı adı/şifre ile kimlik doğrulama yapılamadı.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Bu özellik şu anda test aşamasında ve yalnızca PVE 8+ üzerinde test edildi. Lütfen dikkatli kullanın.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Oku'; diff --git a/lib/generated/l10n/l10n_uk.dart b/lib/generated/l10n/l10n_uk.dart index 3dabdb67..b53c172e 100644 --- a/lib/generated/l10n/l10n_uk.dart +++ b/lib/generated/l10n/l10n_uk.dart @@ -539,10 +539,33 @@ class AppLocalizationsUk extends AppLocalizations { String get pveLoginFailed => 'Не вдалося увійти. Неможливо пройти аутентифікацію за допомогою імені користувача/пароля з конфігурації сервера для входу Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Ця функція наразі перебуває на стадії тестування та випробувалася лише на PVE 8+. Будь ласка, використовуйте її з обережністю.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Читати'; diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart index 12e40a36..e2758aa4 100644 --- a/lib/generated/l10n/l10n_zh.dart +++ b/lib/generated/l10n/l10n_zh.dart @@ -506,9 +506,30 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pveLoginFailed => '登录失败。无法使用服务器配置中的用户名或密码通过 Linux PAM 方式认证。'; + @override + String get pveOtpRequired => '此 PVE 服务器已启用双因素认证,请输入 OTP 验证码。'; + @override String get pveVersionLow => '当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用'; + @override + String get pveLoadingForwarding => '正在建立 SSH 隧道...'; + + @override + String get pveLoadingLogin => '正在认证 PVE...'; + + @override + String get pveLoadingData => '正在获取集群数据...'; + + @override + String get pveLoadingConnect => '正在连接...'; + + @override + String get pvePassword => 'PVE 密码'; + + @override + String get pvePasswordHint => '使用密钥认证时需要填写'; + @override String get read => '读'; @@ -1315,9 +1336,30 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get pveLoginFailed => '登入失敗。無法使用伺服器設定中的使用者名稱或密碼透過 Linux PAM 方式認證。'; + @override + String get pveOtpRequired => '此 PVE 伺服器已啟用雙因素認證,請輸入 OTP 驗證碼。'; + @override String get pveVersionLow => '此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。'; + @override + String get pveLoadingForwarding => '正在建立 SSH 隧道...'; + + @override + String get pveLoadingLogin => '正在認證 PVE...'; + + @override + String get pveLoadingData => '正在獲取集群數據...'; + + @override + String get pveLoadingConnect => '正在連接...'; + + @override + String get pvePassword => 'PVE 密碼'; + + @override + String get pvePasswordHint => '使用密鑰認證時需要填寫'; + @override String get read => '讀取'; diff --git a/lib/hive/hive_adapters.g.dart b/lib/hive/hive_adapters.g.dart index c456396b..630a06f0 100644 --- a/lib/hive/hive_adapters.g.dart +++ b/lib/hive/hive_adapters.g.dart @@ -485,6 +485,7 @@ class ServerCustomAdapter extends TypeAdapter { return ServerCustom( pveAddr: fields[1] as String?, pveIgnoreCert: fields[2] == null ? false : fields[2] as bool, + pvePwd: fields[8] as String?, cmds: (fields[3] as Map?)?.cast(), preferTempDev: fields[4] as String?, logoUrl: fields[5] as String?, @@ -496,7 +497,7 @@ class ServerCustomAdapter extends TypeAdapter { @override void write(BinaryWriter writer, ServerCustom obj) { writer - ..writeByte(7) + ..writeByte(8) ..writeByte(1) ..write(obj.pveAddr) ..writeByte(2) @@ -510,7 +511,9 @@ class ServerCustomAdapter extends TypeAdapter { ..writeByte(6) ..write(obj.netDev) ..writeByte(7) - ..write(obj.scriptDir); + ..write(obj.scriptDir) + ..writeByte(8) + ..write(obj.pvePwd); } @override diff --git a/lib/hive/hive_adapters.g.yaml b/lib/hive/hive_adapters.g.yaml index 94d426fe..f5439221 100644 --- a/lib/hive/hive_adapters.g.yaml +++ b/lib/hive/hive_adapters.g.yaml @@ -185,7 +185,7 @@ types: index: 8 ServerCustom: typeId: 7 - nextIndex: 8 + nextIndex: 9 fields: pveAddr: index: 1 @@ -201,6 +201,8 @@ types: index: 6 scriptDir: index: 7 + pvePwd: + index: 8 WakeOnLanCfg: typeId: 8 nextIndex: 3 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 99a71f51..e58c8ba0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -154,7 +154,14 @@ "pushToken": "Push token", "pveIgnoreCertTip": "Not recommended to enable, beware of security risks! If you are using the default certificate from PVE, you need to enable this option.", "pveLoginFailed": "Login failed. Unable to authenticate with username/password from server configuration for Linux PAM login.", + "pveOtpRequired": "Two-factor authentication is enabled on this PVE server. Please enter the OTP code.", "pveVersionLow": "This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.", + "pveLoadingForwarding": "Establishing SSH tunnel...", + "pveLoadingLogin": "Authenticating with PVE...", + "pveLoadingData": "Fetching cluster data...", + "pveLoadingConnect": "Connecting...", + "pvePassword": "PVE Password", + "pvePasswordHint": "Required when using key-based SSH authentication", "read": "Read", "recentConnections": "Recent Connections", "rememberPwdInMem": "Remember password in memory", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1b96d475..608d9465 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -154,7 +154,14 @@ "pushToken": "消息推送 Token", "pveIgnoreCertTip": "不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项", "pveLoginFailed": "登录失败。无法使用服务器配置中的用户名或密码通过 Linux PAM 方式认证。", + "pveOtpRequired": "此 PVE 服务器已启用双因素认证,请输入 OTP 验证码。", "pveVersionLow": "当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用", + "pveLoadingForwarding": "正在建立 SSH 隧道...", + "pveLoadingLogin": "正在认证 PVE...", + "pveLoadingData": "正在获取集群数据...", + "pveLoadingConnect": "正在连接...", + "pvePassword": "PVE 密码", + "pvePasswordHint": "使用密钥认证时需要填写", "read": "读", "recentConnections": "最近连接记录", "rememberPwdInMem": "在内存中记住密码", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index 475f996d..aa9b3f52 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -154,7 +154,14 @@ "pushToken": "消息推送 Token", "pveIgnoreCertTip": "不建議啟用,請注意安全風險!如果您使用的是 PVE 的預設憑證,則需要啟用此選項。", "pveLoginFailed": "登入失敗。無法使用伺服器設定中的使用者名稱或密碼透過 Linux PAM 方式認證。", + "pveOtpRequired": "此 PVE 伺服器已啟用雙因素認證,請輸入 OTP 驗證碼。", "pveVersionLow": "此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。", + "pveLoadingForwarding": "正在建立 SSH 隧道...", + "pveLoadingLogin": "正在認證 PVE...", + "pveLoadingData": "正在獲取集群數據...", + "pveLoadingConnect": "正在連接...", + "pvePassword": "PVE 密碼", + "pvePasswordHint": "使用密鑰認證時需要填寫", "read": "讀取", "recentConnections": "最近連線記錄", "rememberPwdInMem": "在記憶體中記住密碼", diff --git a/lib/view/page/pve.dart b/lib/view/page/pve.dart index 1ecbe24a..6b655526 100644 --- a/lib/view/page/pve.dart +++ b/lib/view/page/pve.dart @@ -4,6 +4,7 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/extension/context/locale.dart'; +import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/server/pve.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/provider/pve.dart'; @@ -73,24 +74,28 @@ final class _PvePageState extends ConsumerState { : Btn.icon( icon: const Icon(Icons.refresh), onTap: () { - _notifier.list(); + _notifier.reconnect(); _initRefreshTimer(); }, ), ], ), body: pveState.error != null - ? Padding( - padding: const EdgeInsets.all(13), - child: Center(child: Text(pveState.error.toString())), - ) - : _buildBody(pveState.data), + ? _buildError(pveState.error!) + : _buildBody(pveState.data, pveState.loadingStep), ); } - Widget _buildBody(PveRes? data) { + Widget _buildError(PveErr error) { + return Padding( + padding: const EdgeInsets.all(13), + child: Center(child: Text(error.toString())), + ); + } + + Widget _buildBody(PveRes? data, PveLoadingStep loadingStep) { if (data == null) { - return UIs.centerLoading; + return _buildLoading(loadingStep); } PveResType? lastType; @@ -134,6 +139,25 @@ final class _PvePageState extends ConsumerState { ); } + Widget _buildLoading(PveLoadingStep step) { + final String message = switch (step) { + PveLoadingStep.forwarding => l10n.pveLoadingForwarding, + PveLoadingStep.loggingIn => l10n.pveLoadingLogin, + PveLoadingStep.fetchingData => l10n.pveLoadingData, + _ => l10n.pveLoadingConnect, + }; + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 17), + Text(message, style: UIs.text13Grey), + ], + ), + ); + } + Widget _buildNode(PveNode item) { final valueAnim = AlwaysStoppedAnimation(UIs.primaryColor); return Padding( diff --git a/lib/view/page/server/edit/actions.dart b/lib/view/page/server/edit/actions.dart index 7aabd7d5..2a838c87 100644 --- a/lib/view/page/server/edit/actions.dart +++ b/lib/view/page/server/edit/actions.dart @@ -71,6 +71,7 @@ extension _Actions on _ServerEditPageState { final custom = ServerCustom( pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull, pveIgnoreCert: _pveIgnoreCert.value, + pvePwd: _pvePwdCtrl.text.selfNotEmptyOrNull, cmds: customCmds.isEmpty ? null : customCmds, preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull, logoUrl: _logoUrlCtrl.text.selfNotEmptyOrNull, @@ -266,6 +267,7 @@ extension _Utils on _ServerEditPageState { if (custom != null) { _pveAddrCtrl.text = custom.pveAddr ?? ''; _pveIgnoreCert.value = custom.pveIgnoreCert; + _pvePwdCtrl.text = custom.pvePwd ?? ''; _customCmds.value = custom.cmds ?? {}; _preferTempDevCtrl.text = custom.preferTempDev ?? ''; _logoUrlCtrl.text = custom.logoUrl ?? ''; diff --git a/lib/view/page/server/edit/edit.dart b/lib/view/page/server/edit/edit.dart index 22ab507e..aab97500 100644 --- a/lib/view/page/server/edit/edit.dart +++ b/lib/view/page/server/edit/edit.dart @@ -48,6 +48,7 @@ class _ServerEditPageState extends ConsumerState final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); final _pveAddrCtrl = TextEditingController(); + final _pvePwdCtrl = TextEditingController(); final _preferTempDevCtrl = TextEditingController(); final _logoUrlCtrl = TextEditingController(); final _wolMacCtrl = TextEditingController(); @@ -98,6 +99,7 @@ class _ServerEditPageState extends ConsumerState _portFocus.dispose(); _usernameFocus.dispose(); _pveAddrCtrl.dispose(); + _pvePwdCtrl.dispose(); _keyIdx.dispose(); _autoConnect.dispose(); diff --git a/lib/view/page/server/edit/widget.dart b/lib/view/page/server/edit/widget.dart index 571ab67d..58769ff5 100644 --- a/lib/view/page/server/edit/widget.dart +++ b/lib/view/page/server/edit/widget.dart @@ -230,32 +230,45 @@ extension _Widgets on _ServerEditPageState { Widget _buildPVEs() { const addr = 'https://127.0.0.1:8006'; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CenterGreyTitle('PVE'), - Input( - controller: _pveAddrCtrl, - type: TextInputType.url, - icon: MingCute.web_line, - label: 'URL', - hint: addr, - suggestion: false, - ), - ListTile( - leading: const Icon(MingCute.certificate_line), - title: TipText('PVE ${l10n.ignoreCert}', l10n.pveIgnoreCertTip), - trailing: _pveIgnoreCert.listenVal( - (v) => Switch( - value: v, - onChanged: (val) { - _pveIgnoreCert.value = val; - }, - ), + return _keyIdx.listenVal((v) { + final useKeyAuth = v != null && v >= 0; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CenterGreyTitle('PVE'), + Input( + controller: _pveAddrCtrl, + type: TextInputType.url, + icon: MingCute.web_line, + label: 'URL', + hint: addr, + suggestion: false, ), - ).cardx, - ], - ); + if (useKeyAuth) + Input( + controller: _pvePwdCtrl, + type: TextInputType.visiblePassword, + icon: MingCute.lock_line, + label: l10n.pvePassword, + hint: l10n.pvePasswordHint, + obscureText: true, + suggestion: false, + ), + ListTile( + leading: const Icon(MingCute.certificate_line), + title: TipText('PVE ${l10n.ignoreCert}', l10n.pveIgnoreCertTip), + trailing: _pveIgnoreCert.listenVal( + (v) => Switch( + value: v, + onChanged: (val) { + _pveIgnoreCert.value = val; + }, + ), + ), + ).cardx, + ], + ); + }); } Widget _buildCustomCmds() { diff --git a/pubspec.lock b/pubspec.lock index f02dbcbd..94547b65 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: build - sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" + sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.0.5" build_config: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "3552e5c2874ed47cf9ed9d6813ac71b2276ee07622f48530468b8013f1767e3f" + sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e" url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.13.1" built_collection: dependency: transitive description: @@ -189,10 +189,10 @@ packages: dependency: transitive description: name: camera_avfoundation - sha256: "4e47c2796dab3f21fdfe1d15151bf628519093b171307cb64a71ba8e451697b5" + sha256: "11b4aee2f5e5e038982e152b4a342c749b414aa27857899d20f4323e94cb5f0b" url: "https://pub.dev" source: hosted - version: "0.9.23" + version: "0.9.23+2" camera_platform_interface: dependency: transitive description: @@ -460,10 +460,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: d974b6ba2606371ac71dd94254beefb6fa81185bde0b59bdc1df09885da85fde + sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" url: "https://pub.dev" source: hosted - version: "10.3.8" + version: "10.3.10" fixnum: dependency: transitive description: @@ -927,10 +927,10 @@ packages: dependency: transitive description: name: local_auth_android - sha256: dc9663a7bc8ac33d7d988e63901974f63d527ebef260eabd19c479447cc9c911 + sha256: b41970749c2d43791790724b76917eeee1e90de76e6b0eec3edca03a329bf44c url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" local_auth_darwin: dependency: transitive description: @@ -975,10 +975,10 @@ packages: dependency: transitive description: name: markdown - sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.1" matcher: dependency: transitive description: @@ -1491,10 +1491,10 @@ packages: dependency: transitive description: name: source_gen - sha256: adc962c96fffb2de1728ef396a995aaedcafbe635abdca13d2a987ce17e57751 + sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd" url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.2.2" source_helper: dependency: transitive description: