mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-22 17:51:29 +03:00
Merge PR #27.
This commit is contained in:
commit
7d8a09529e
@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'core/state.dart';
|
||||
import 'desktop/state.dart';
|
||||
|
||||
final log = Logger('about');
|
||||
|
||||
@ -13,7 +14,6 @@ class AboutPage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final rpcState = ref.watch(rpcStateProvider);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('About Yubico Authenticator'),
|
||||
@ -25,54 +25,42 @@ class AboutPage extends ConsumerWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('ykman version: ${rpcState.version}'),
|
||||
if (isDesktop)
|
||||
Text('ykman version: ${ref.watch(rpcStateProvider).version}'),
|
||||
Text('Dart version: ${Platform.version}'),
|
||||
const SizedBox(height: 8.0),
|
||||
Text('Log level: ${ref.watch(logLevelProvider)}'),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ref.read(logLevelProvider.notifier).setLevel(Level.INFO);
|
||||
log.info('Log level changed to INFO');
|
||||
},
|
||||
child: const Text('Info'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(logLevelProvider.notifier)
|
||||
.setLevel(Level.CONFIG);
|
||||
log.config('Log level changed to CONFIG');
|
||||
},
|
||||
child: const Text('Config'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ref.read(logLevelProvider.notifier).setLevel(Level.FINE);
|
||||
log.fine('Log level changed to FINE');
|
||||
},
|
||||
child: const Text('Fine'),
|
||||
),
|
||||
],
|
||||
children: [Level.INFO, Level.CONFIG, Level.FINE]
|
||||
.map((level) => TextButton(
|
||||
onPressed: () {
|
||||
ref.read(logLevelProvider.notifier).state = level;
|
||||
log.info(
|
||||
'Log level changed to ${level.name.toUpperCase()}');
|
||||
},
|
||||
child: Text(level.name.toUpperCase()),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
log.info('Running diagnostics...');
|
||||
final response =
|
||||
await ref.read(rpcProvider).command('diagnose', []);
|
||||
log.info('Response', response['diagnostics']);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Diagnostics done. See log for results...'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Run diagnostics...'),
|
||||
),
|
||||
if (isDesktop)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
log.info('Running diagnostics...');
|
||||
final response =
|
||||
await ref.read(rpcProvider).command('diagnose', []);
|
||||
log.info('Response', response['diagnostics']);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content:
|
||||
Text('Diagnostics done. See log for results...'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Run diagnostics...'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,93 +1,18 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:yubico_authenticator/management/models.dart';
|
||||
|
||||
import '../core/models.dart';
|
||||
import '../core/state.dart';
|
||||
import '../core/rpc.dart';
|
||||
import '../oath/menu_actions.dart';
|
||||
import 'models.dart';
|
||||
|
||||
const _usbPollDelay = Duration(milliseconds: 500);
|
||||
|
||||
const _nfcPollDelay = Duration(milliseconds: 2500);
|
||||
const _nfcAttachPollDelay = Duration(seconds: 1);
|
||||
const _nfcDetachPollDelay = Duration(seconds: 5);
|
||||
|
||||
final log = Logger('app.state');
|
||||
|
||||
final windowStateProvider =
|
||||
StateNotifierProvider<WindowStateNotifier, WindowState>(
|
||||
(ref) => WindowStateNotifier());
|
||||
|
||||
class WindowStateNotifier extends StateNotifier<WindowState>
|
||||
with WindowListener {
|
||||
Timer? _idleTimer;
|
||||
WindowStateNotifier()
|
||||
: super(WindowState(focused: true, visible: true, active: true)) {
|
||||
_init();
|
||||
}
|
||||
|
||||
void _init() async {
|
||||
windowManager.addListener(this);
|
||||
if (!await windowManager.isVisible() && mounted) {
|
||||
state = WindowState(focused: false, visible: false, active: true);
|
||||
_idleTimer = Timer(const Duration(seconds: 5), () {
|
||||
if (mounted) {
|
||||
state = state.copyWith(active: false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
windowManager.removeListener(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
set state(WindowState value) {
|
||||
log.config('Window state changed: $value');
|
||||
super.state = value;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowEvent(String eventName) {
|
||||
if (mounted) {
|
||||
switch (eventName) {
|
||||
case 'blur':
|
||||
state = state.copyWith(focused: false);
|
||||
_idleTimer?.cancel();
|
||||
_idleTimer = Timer(const Duration(seconds: 5), () {
|
||||
if (mounted) {
|
||||
state = state.copyWith(active: false);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'focus':
|
||||
state = state.copyWith(focused: true, active: true);
|
||||
_idleTimer?.cancel();
|
||||
break;
|
||||
case 'minimize':
|
||||
state = state.copyWith(visible: false, active: false);
|
||||
_idleTimer?.cancel();
|
||||
break;
|
||||
case 'restore':
|
||||
state = state.copyWith(visible: true, active: true);
|
||||
break;
|
||||
default:
|
||||
log.fine('Window event ignored: $eventName');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Default implementation is always focused, override with platform specific version.
|
||||
final windowStateProvider = Provider<WindowState>(
|
||||
(ref) => WindowState(focused: true, visible: true, active: true),
|
||||
);
|
||||
|
||||
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
|
||||
(ref) => ThemeModeNotifier(ref.watch(prefProvider)));
|
||||
@ -125,140 +50,15 @@ class SearchNotifier extends StateNotifier<String> {
|
||||
}
|
||||
}
|
||||
|
||||
final _usbDevicesProvider =
|
||||
StateNotifierProvider<UsbDeviceNotifier, List<UsbYubiKeyNode>>((ref) {
|
||||
final notifier = UsbDeviceNotifier(ref.watch(rpcProvider));
|
||||
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
|
||||
notifier._notifyWindowState(windowState);
|
||||
}, fireImmediately: true);
|
||||
return notifier;
|
||||
});
|
||||
// Override with platform implementation
|
||||
final attachedDevicesProvider = Provider<List<DeviceNode>>(
|
||||
(ref) => [],
|
||||
);
|
||||
|
||||
class UsbDeviceNotifier extends StateNotifier<List<UsbYubiKeyNode>> {
|
||||
final RpcSession _rpc;
|
||||
Timer? _pollTimer;
|
||||
int _usbState = -1;
|
||||
UsbDeviceNotifier(this._rpc) : super([]);
|
||||
|
||||
void _notifyWindowState(WindowState windowState) {
|
||||
if (windowState.active) {
|
||||
_pollDevices();
|
||||
} else {
|
||||
_pollTimer?.cancel();
|
||||
// Release any held device
|
||||
_rpc.command('get', ['usb']);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _pollDevices() async {
|
||||
_pollTimer?.cancel();
|
||||
|
||||
try {
|
||||
var scan = await _rpc.command('scan', ['usb']);
|
||||
|
||||
if (_usbState != scan['state'] || state.length != scan['pids'].length) {
|
||||
var usbResult = await _rpc.command('get', ['usb']);
|
||||
log.info('USB state change', jsonEncode(usbResult));
|
||||
_usbState = usbResult['data']['state'];
|
||||
List<UsbYubiKeyNode> usbDevices = [];
|
||||
|
||||
for (String id in (usbResult['children'] as Map).keys) {
|
||||
var path = ['usb', id];
|
||||
var deviceResult = await _rpc.command('get', path);
|
||||
var deviceData = deviceResult['data'];
|
||||
usbDevices.add(DeviceNode.usbYubiKey(
|
||||
path,
|
||||
deviceData['name'],
|
||||
deviceData['pid'],
|
||||
DeviceInfo.fromJson(deviceData['info']),
|
||||
) as UsbYubiKeyNode);
|
||||
}
|
||||
|
||||
log.info('USB state updated');
|
||||
if (mounted) {
|
||||
state = usbDevices;
|
||||
}
|
||||
}
|
||||
} on RpcError catch (e) {
|
||||
log.severe('Error polling USB', jsonEncode(e));
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
_pollTimer = Timer(_usbPollDelay, _pollDevices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final _nfcDevicesProvider =
|
||||
StateNotifierProvider<NfcDeviceNotifier, List<NfcReaderNode>>((ref) {
|
||||
final notifier = NfcDeviceNotifier(ref.watch(rpcProvider));
|
||||
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
|
||||
notifier._notifyWindowState(windowState);
|
||||
}, fireImmediately: true);
|
||||
return notifier;
|
||||
});
|
||||
|
||||
class NfcDeviceNotifier extends StateNotifier<List<NfcReaderNode>> {
|
||||
final RpcSession _rpc;
|
||||
Timer? _pollTimer;
|
||||
String _nfcState = '';
|
||||
NfcDeviceNotifier(this._rpc) : super([]);
|
||||
|
||||
void _notifyWindowState(WindowState windowState) {
|
||||
if (windowState.active) {
|
||||
_pollReaders();
|
||||
} else {
|
||||
_pollTimer?.cancel();
|
||||
// Release any held device
|
||||
_rpc.command('get', ['nfc']);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _pollReaders() async {
|
||||
_pollTimer?.cancel();
|
||||
|
||||
try {
|
||||
var children = await _rpc.command('scan', ['nfc']);
|
||||
var newState = children.keys.join(':');
|
||||
|
||||
if (mounted && newState != _nfcState) {
|
||||
log.info('NFC state change', jsonEncode(children));
|
||||
_nfcState = newState;
|
||||
state = children.entries
|
||||
.map((e) =>
|
||||
DeviceNode.nfcReader(['nfc', e.key], e.value['name'] as String)
|
||||
as NfcReaderNode)
|
||||
.toList();
|
||||
}
|
||||
} on RpcError catch (e) {
|
||||
log.severe('Error polling NFC', jsonEncode(e));
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
_pollTimer = Timer(_nfcPollDelay, _pollReaders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final attachedDevicesProvider = Provider<List<DeviceNode>>((ref) {
|
||||
final usbDevices = ref.watch(_usbDevicesProvider).toList();
|
||||
final nfcDevices = ref.watch(_nfcDevicesProvider).toList();
|
||||
usbDevices.sort((a, b) => a.name.compareTo(b.name));
|
||||
nfcDevices.sort((a, b) => a.name.compareTo(b.name));
|
||||
return [...usbDevices, ...nfcDevices];
|
||||
});
|
||||
// Override with platform implementation
|
||||
final currentDeviceDataProvider = Provider<YubiKeyData?>(
|
||||
(ref) => null,
|
||||
);
|
||||
|
||||
final currentDeviceProvider =
|
||||
StateNotifierProvider<CurrentDeviceNotifier, DeviceNode?>((ref) {
|
||||
@ -305,75 +105,6 @@ class CurrentDeviceNotifier extends StateNotifier<DeviceNode?> {
|
||||
}
|
||||
}
|
||||
|
||||
final currentDeviceDataProvider =
|
||||
StateNotifierProvider<CurrentDeviceDataNotifier, YubiKeyData?>((ref) {
|
||||
final notifier = CurrentDeviceDataNotifier(
|
||||
ref.watch(rpcProvider),
|
||||
ref.watch(currentDeviceProvider),
|
||||
);
|
||||
if (notifier._deviceNode is NfcReaderNode) {
|
||||
// If this is an NFC reader, listen on WindowState.
|
||||
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
|
||||
notifier._notifyWindowState(windowState);
|
||||
}, fireImmediately: true);
|
||||
}
|
||||
return notifier;
|
||||
});
|
||||
|
||||
class CurrentDeviceDataNotifier extends StateNotifier<YubiKeyData?> {
|
||||
final RpcSession _rpc;
|
||||
final DeviceNode? _deviceNode;
|
||||
Timer? _pollTimer;
|
||||
|
||||
CurrentDeviceDataNotifier(this._rpc, this._deviceNode) : super(null) {
|
||||
final dev = _deviceNode;
|
||||
if (dev is UsbYubiKeyNode) {
|
||||
state = YubiKeyData(dev, dev.name, dev.info);
|
||||
}
|
||||
}
|
||||
|
||||
void _notifyWindowState(WindowState windowState) {
|
||||
if (windowState.active) {
|
||||
_pollReader();
|
||||
} else {
|
||||
_pollTimer?.cancel();
|
||||
// TODO: Should we clear the key here?
|
||||
/*if (mounted) {
|
||||
state = null;
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _pollReader() async {
|
||||
_pollTimer?.cancel();
|
||||
final node = _deviceNode!;
|
||||
try {
|
||||
var result = await _rpc.command('get', node.path);
|
||||
if (mounted) {
|
||||
if (result['data']['present']) {
|
||||
state = YubiKeyData(node, result['data']['name'],
|
||||
DeviceInfo.fromJson(result['data']['info']));
|
||||
} else {
|
||||
state = null;
|
||||
}
|
||||
}
|
||||
} on RpcError catch (e) {
|
||||
log.severe('Error polling NFC', jsonEncode(e));
|
||||
}
|
||||
if (mounted) {
|
||||
_pollTimer = Timer(
|
||||
state == null ? _nfcAttachPollDelay : _nfcDetachPollDelay,
|
||||
_pollReader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final subPageProvider = StateNotifierProvider<SubPageNotifier, SubPage>(
|
||||
(ref) => SubPageNotifier(SubPage.authenticator));
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'models.freezed.dart';
|
||||
part 'models.g.dart';
|
||||
|
||||
@freezed
|
||||
class Version with _$Version {
|
||||
@ -19,19 +18,3 @@ class Version with _$Version {
|
||||
return '$major.$minor.$patch';
|
||||
}
|
||||
}
|
||||
|
||||
@Freezed(unionKey: 'kind')
|
||||
class RpcResponse with _$RpcResponse {
|
||||
factory RpcResponse.success(Map<String, dynamic> body) = Success;
|
||||
factory RpcResponse.signal(String status, Map<String, dynamic> body) = Signal;
|
||||
factory RpcResponse.error(
|
||||
String status, String message, Map<String, dynamic> body) = RpcError;
|
||||
|
||||
factory RpcResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$RpcResponseFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RpcState with _$RpcState {
|
||||
const factory RpcState(String version) = _RpcState;
|
||||
}
|
||||
|
@ -168,764 +168,3 @@ abstract class _Version extends Version {
|
||||
_$VersionCopyWith<_Version> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
RpcResponse _$RpcResponseFromJson(Map<String, dynamic> json) {
|
||||
switch (json['kind']) {
|
||||
case 'success':
|
||||
return Success.fromJson(json);
|
||||
case 'signal':
|
||||
return Signal.fromJson(json);
|
||||
case 'error':
|
||||
return RpcError.fromJson(json);
|
||||
|
||||
default:
|
||||
throw CheckedFromJsonException(
|
||||
json, 'kind', 'RpcResponse', 'Invalid union type "${json['kind']}"!');
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$RpcResponseTearOff {
|
||||
const _$RpcResponseTearOff();
|
||||
|
||||
Success success(Map<String, dynamic> body) {
|
||||
return Success(
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
Signal signal(String status, Map<String, dynamic> body) {
|
||||
return Signal(
|
||||
status,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
RpcError error(String status, String message, Map<String, dynamic> body) {
|
||||
return RpcError(
|
||||
status,
|
||||
message,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
RpcResponse fromJson(Map<String, Object?> json) {
|
||||
return RpcResponse.fromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
const $RpcResponse = _$RpcResponseTearOff();
|
||||
|
||||
/// @nodoc
|
||||
mixin _$RpcResponse {
|
||||
Map<String, dynamic> get body => throw _privateConstructorUsedError;
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(Map<String, dynamic> body) success,
|
||||
required TResult Function(String status, Map<String, dynamic> body) signal,
|
||||
required TResult Function(
|
||||
String status, String message, Map<String, dynamic> body)
|
||||
error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(Success value) success,
|
||||
required TResult Function(Signal value) signal,
|
||||
required TResult Function(RpcError value) error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$RpcResponseCopyWith<RpcResponse> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $RpcResponseCopyWith<$Res> {
|
||||
factory $RpcResponseCopyWith(
|
||||
RpcResponse value, $Res Function(RpcResponse) then) =
|
||||
_$RpcResponseCopyWithImpl<$Res>;
|
||||
$Res call({Map<String, dynamic> body});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$RpcResponseCopyWithImpl<$Res> implements $RpcResponseCopyWith<$Res> {
|
||||
_$RpcResponseCopyWithImpl(this._value, this._then);
|
||||
|
||||
final RpcResponse _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function(RpcResponse) _then;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? body = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
body: body == freezed
|
||||
? _value.body
|
||||
: body // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SuccessCopyWith<$Res> implements $RpcResponseCopyWith<$Res> {
|
||||
factory $SuccessCopyWith(Success value, $Res Function(Success) then) =
|
||||
_$SuccessCopyWithImpl<$Res>;
|
||||
@override
|
||||
$Res call({Map<String, dynamic> body});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SuccessCopyWithImpl<$Res> extends _$RpcResponseCopyWithImpl<$Res>
|
||||
implements $SuccessCopyWith<$Res> {
|
||||
_$SuccessCopyWithImpl(Success _value, $Res Function(Success) _then)
|
||||
: super(_value, (v) => _then(v as Success));
|
||||
|
||||
@override
|
||||
Success get _value => super._value as Success;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? body = freezed,
|
||||
}) {
|
||||
return _then(Success(
|
||||
body == freezed
|
||||
? _value.body
|
||||
: body // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$Success implements Success {
|
||||
_$Success(this.body, {String? $type}) : $type = $type ?? 'success';
|
||||
|
||||
factory _$Success.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SuccessFromJson(json);
|
||||
|
||||
@override
|
||||
final Map<String, dynamic> body;
|
||||
|
||||
@JsonKey(name: 'kind')
|
||||
final String $type;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RpcResponse.success(body: $body)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is Success &&
|
||||
const DeepCollectionEquality().equals(other.body, body));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, const DeepCollectionEquality().hash(body));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
$SuccessCopyWith<Success> get copyWith =>
|
||||
_$SuccessCopyWithImpl<Success>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(Map<String, dynamic> body) success,
|
||||
required TResult Function(String status, Map<String, dynamic> body) signal,
|
||||
required TResult Function(
|
||||
String status, String message, Map<String, dynamic> body)
|
||||
error,
|
||||
}) {
|
||||
return success(body);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
}) {
|
||||
return success?.call(body);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (success != null) {
|
||||
return success(body);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(Success value) success,
|
||||
required TResult Function(Signal value) signal,
|
||||
required TResult Function(RpcError value) error,
|
||||
}) {
|
||||
return success(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
}) {
|
||||
return success?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (success != null) {
|
||||
return success(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SuccessToJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Success implements RpcResponse {
|
||||
factory Success(Map<String, dynamic> body) = _$Success;
|
||||
|
||||
factory Success.fromJson(Map<String, dynamic> json) = _$Success.fromJson;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get body;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
$SuccessCopyWith<Success> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SignalCopyWith<$Res> implements $RpcResponseCopyWith<$Res> {
|
||||
factory $SignalCopyWith(Signal value, $Res Function(Signal) then) =
|
||||
_$SignalCopyWithImpl<$Res>;
|
||||
@override
|
||||
$Res call({String status, Map<String, dynamic> body});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SignalCopyWithImpl<$Res> extends _$RpcResponseCopyWithImpl<$Res>
|
||||
implements $SignalCopyWith<$Res> {
|
||||
_$SignalCopyWithImpl(Signal _value, $Res Function(Signal) _then)
|
||||
: super(_value, (v) => _then(v as Signal));
|
||||
|
||||
@override
|
||||
Signal get _value => super._value as Signal;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? status = freezed,
|
||||
Object? body = freezed,
|
||||
}) {
|
||||
return _then(Signal(
|
||||
status == freezed
|
||||
? _value.status
|
||||
: status // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
body == freezed
|
||||
? _value.body
|
||||
: body // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$Signal implements Signal {
|
||||
_$Signal(this.status, this.body, {String? $type}) : $type = $type ?? 'signal';
|
||||
|
||||
factory _$Signal.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SignalFromJson(json);
|
||||
|
||||
@override
|
||||
final String status;
|
||||
@override
|
||||
final Map<String, dynamic> body;
|
||||
|
||||
@JsonKey(name: 'kind')
|
||||
final String $type;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RpcResponse.signal(status: $status, body: $body)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is Signal &&
|
||||
const DeepCollectionEquality().equals(other.status, status) &&
|
||||
const DeepCollectionEquality().equals(other.body, body));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(status),
|
||||
const DeepCollectionEquality().hash(body));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
$SignalCopyWith<Signal> get copyWith =>
|
||||
_$SignalCopyWithImpl<Signal>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(Map<String, dynamic> body) success,
|
||||
required TResult Function(String status, Map<String, dynamic> body) signal,
|
||||
required TResult Function(
|
||||
String status, String message, Map<String, dynamic> body)
|
||||
error,
|
||||
}) {
|
||||
return signal(status, body);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
}) {
|
||||
return signal?.call(status, body);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (signal != null) {
|
||||
return signal(status, body);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(Success value) success,
|
||||
required TResult Function(Signal value) signal,
|
||||
required TResult Function(RpcError value) error,
|
||||
}) {
|
||||
return signal(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
}) {
|
||||
return signal?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (signal != null) {
|
||||
return signal(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SignalToJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Signal implements RpcResponse {
|
||||
factory Signal(String status, Map<String, dynamic> body) = _$Signal;
|
||||
|
||||
factory Signal.fromJson(Map<String, dynamic> json) = _$Signal.fromJson;
|
||||
|
||||
String get status;
|
||||
@override
|
||||
Map<String, dynamic> get body;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
$SignalCopyWith<Signal> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $RpcErrorCopyWith<$Res> implements $RpcResponseCopyWith<$Res> {
|
||||
factory $RpcErrorCopyWith(RpcError value, $Res Function(RpcError) then) =
|
||||
_$RpcErrorCopyWithImpl<$Res>;
|
||||
@override
|
||||
$Res call({String status, String message, Map<String, dynamic> body});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$RpcErrorCopyWithImpl<$Res> extends _$RpcResponseCopyWithImpl<$Res>
|
||||
implements $RpcErrorCopyWith<$Res> {
|
||||
_$RpcErrorCopyWithImpl(RpcError _value, $Res Function(RpcError) _then)
|
||||
: super(_value, (v) => _then(v as RpcError));
|
||||
|
||||
@override
|
||||
RpcError get _value => super._value as RpcError;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? status = freezed,
|
||||
Object? message = freezed,
|
||||
Object? body = freezed,
|
||||
}) {
|
||||
return _then(RpcError(
|
||||
status == freezed
|
||||
? _value.status
|
||||
: status // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
message == freezed
|
||||
? _value.message
|
||||
: message // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
body == freezed
|
||||
? _value.body
|
||||
: body // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$RpcError implements RpcError {
|
||||
_$RpcError(this.status, this.message, this.body, {String? $type})
|
||||
: $type = $type ?? 'error';
|
||||
|
||||
factory _$RpcError.fromJson(Map<String, dynamic> json) =>
|
||||
_$$RpcErrorFromJson(json);
|
||||
|
||||
@override
|
||||
final String status;
|
||||
@override
|
||||
final String message;
|
||||
@override
|
||||
final Map<String, dynamic> body;
|
||||
|
||||
@JsonKey(name: 'kind')
|
||||
final String $type;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RpcResponse.error(status: $status, message: $message, body: $body)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is RpcError &&
|
||||
const DeepCollectionEquality().equals(other.status, status) &&
|
||||
const DeepCollectionEquality().equals(other.message, message) &&
|
||||
const DeepCollectionEquality().equals(other.body, body));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(status),
|
||||
const DeepCollectionEquality().hash(message),
|
||||
const DeepCollectionEquality().hash(body));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
$RpcErrorCopyWith<RpcError> get copyWith =>
|
||||
_$RpcErrorCopyWithImpl<RpcError>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(Map<String, dynamic> body) success,
|
||||
required TResult Function(String status, Map<String, dynamic> body) signal,
|
||||
required TResult Function(
|
||||
String status, String message, Map<String, dynamic> body)
|
||||
error,
|
||||
}) {
|
||||
return error(status, message, body);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
}) {
|
||||
return error?.call(status, message, body);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (error != null) {
|
||||
return error(status, message, body);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(Success value) success,
|
||||
required TResult Function(Signal value) signal,
|
||||
required TResult Function(RpcError value) error,
|
||||
}) {
|
||||
return error(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
}) {
|
||||
return error?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (error != null) {
|
||||
return error(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$RpcErrorToJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class RpcError implements RpcResponse {
|
||||
factory RpcError(String status, String message, Map<String, dynamic> body) =
|
||||
_$RpcError;
|
||||
|
||||
factory RpcError.fromJson(Map<String, dynamic> json) = _$RpcError.fromJson;
|
||||
|
||||
String get status;
|
||||
String get message;
|
||||
@override
|
||||
Map<String, dynamic> get body;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
$RpcErrorCopyWith<RpcError> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$RpcStateTearOff {
|
||||
const _$RpcStateTearOff();
|
||||
|
||||
_RpcState call(String version) {
|
||||
return _RpcState(
|
||||
version,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
const $RpcState = _$RpcStateTearOff();
|
||||
|
||||
/// @nodoc
|
||||
mixin _$RpcState {
|
||||
String get version => throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$RpcStateCopyWith<RpcState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $RpcStateCopyWith<$Res> {
|
||||
factory $RpcStateCopyWith(RpcState value, $Res Function(RpcState) then) =
|
||||
_$RpcStateCopyWithImpl<$Res>;
|
||||
$Res call({String version});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$RpcStateCopyWithImpl<$Res> implements $RpcStateCopyWith<$Res> {
|
||||
_$RpcStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
final RpcState _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function(RpcState) _then;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? version = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
version: version == freezed
|
||||
? _value.version
|
||||
: version // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$RpcStateCopyWith<$Res> implements $RpcStateCopyWith<$Res> {
|
||||
factory _$RpcStateCopyWith(_RpcState value, $Res Function(_RpcState) then) =
|
||||
__$RpcStateCopyWithImpl<$Res>;
|
||||
@override
|
||||
$Res call({String version});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$RpcStateCopyWithImpl<$Res> extends _$RpcStateCopyWithImpl<$Res>
|
||||
implements _$RpcStateCopyWith<$Res> {
|
||||
__$RpcStateCopyWithImpl(_RpcState _value, $Res Function(_RpcState) _then)
|
||||
: super(_value, (v) => _then(v as _RpcState));
|
||||
|
||||
@override
|
||||
_RpcState get _value => super._value as _RpcState;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? version = freezed,
|
||||
}) {
|
||||
return _then(_RpcState(
|
||||
version == freezed
|
||||
? _value.version
|
||||
: version // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$_RpcState implements _RpcState {
|
||||
const _$_RpcState(this.version);
|
||||
|
||||
@override
|
||||
final String version;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RpcState(version: $version)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _RpcState &&
|
||||
const DeepCollectionEquality().equals(other.version, version));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, const DeepCollectionEquality().hash(version));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
_$RpcStateCopyWith<_RpcState> get copyWith =>
|
||||
__$RpcStateCopyWithImpl<_RpcState>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _RpcState implements RpcState {
|
||||
const factory _RpcState(String version) = _$_RpcState;
|
||||
|
||||
@override
|
||||
String get version;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$RpcStateCopyWith<_RpcState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
@ -1,90 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'models.dart';
|
||||
import 'rpc.dart';
|
||||
|
||||
// This must be initialized before use, in main.dart.
|
||||
final prefProvider = Provider<SharedPreferences>((ref) {
|
||||
throw UnimplementedError();
|
||||
});
|
||||
|
||||
// This must be initialized before use, in main.dart.
|
||||
final rpcProvider = Provider<RpcSession>((ref) {
|
||||
throw UnimplementedError();
|
||||
});
|
||||
final logLevelProvider = StateProvider<Level>((ref) => Logger.root.level);
|
||||
|
||||
final rpcStateProvider = StateNotifierProvider<RpcStateNotifier, RpcState>(
|
||||
(ref) => RpcStateNotifier(ref.watch(rpcProvider)));
|
||||
|
||||
class RpcStateNotifier extends StateNotifier<RpcState> {
|
||||
final RpcSession rpc;
|
||||
RpcStateNotifier(this.rpc) : super(const RpcState('unknown')) {
|
||||
_init();
|
||||
}
|
||||
|
||||
_init() async {
|
||||
final response = await rpc.command('get', []);
|
||||
if (mounted) {
|
||||
state = state.copyWith(version: response['data']['version']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final logLevelProvider = StateNotifierProvider<LogLevelNotifier, Level>(
|
||||
(ref) => LogLevelNotifier(ref.watch(rpcProvider), Logger.root.level));
|
||||
|
||||
class LogLevelNotifier extends StateNotifier<Level> {
|
||||
final RpcSession rpc;
|
||||
LogLevelNotifier(this.rpc, Level state) : super(state);
|
||||
|
||||
setLevel(Level level) {
|
||||
Logger.root.level = level;
|
||||
rpc.setLogLevel(level);
|
||||
state = level;
|
||||
}
|
||||
}
|
||||
|
||||
typedef ErrorHandler = Future<void> Function(RpcError e);
|
||||
|
||||
class RpcNodeSession {
|
||||
final RpcSession _rpc;
|
||||
final List<String> devicePath;
|
||||
final List<String> subPath;
|
||||
final Map<String, ErrorHandler> _errorHandlers = {};
|
||||
|
||||
RpcNodeSession(this._rpc, this.devicePath, this.subPath);
|
||||
|
||||
void setErrorHandler(String status, ErrorHandler handler) {
|
||||
_errorHandlers[status] = handler;
|
||||
}
|
||||
|
||||
void unserErrorHandler(String status) {
|
||||
_errorHandlers.remove(status);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> command(
|
||||
String action, {
|
||||
List<String> target = const [],
|
||||
Map<dynamic, dynamic>? params,
|
||||
Signaler? signal,
|
||||
}) async {
|
||||
try {
|
||||
return await _rpc.command(
|
||||
action,
|
||||
devicePath + subPath + target,
|
||||
params: params,
|
||||
signal: signal,
|
||||
);
|
||||
} on RpcError catch (e) {
|
||||
final handler = _errorHandlers[e.status];
|
||||
if (handler != null) {
|
||||
log.info('Attempting recovery on "${e.status}"');
|
||||
await handler(e);
|
||||
return command(action, target: target, params: params, signal: signal);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||
|
228
lib/desktop/devices.dart
Executable file
228
lib/desktop/devices.dart
Executable file
@ -0,0 +1,228 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../app/models.dart';
|
||||
import '../app/state.dart';
|
||||
import '../management/models.dart';
|
||||
import 'models.dart';
|
||||
import 'rpc.dart';
|
||||
import 'state.dart';
|
||||
|
||||
const _usbPollDelay = Duration(milliseconds: 500);
|
||||
|
||||
const _nfcPollDelay = Duration(milliseconds: 2500);
|
||||
const _nfcAttachPollDelay = Duration(seconds: 1);
|
||||
const _nfcDetachPollDelay = Duration(seconds: 5);
|
||||
|
||||
final log = Logger('desktop.devices');
|
||||
|
||||
final _usbDevicesProvider =
|
||||
StateNotifierProvider<UsbDeviceNotifier, List<UsbYubiKeyNode>>((ref) {
|
||||
final notifier = UsbDeviceNotifier(ref.watch(rpcProvider));
|
||||
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
|
||||
notifier._notifyWindowState(windowState);
|
||||
}, fireImmediately: true);
|
||||
return notifier;
|
||||
});
|
||||
|
||||
class UsbDeviceNotifier extends StateNotifier<List<UsbYubiKeyNode>> {
|
||||
final RpcSession _rpc;
|
||||
Timer? _pollTimer;
|
||||
int _usbState = -1;
|
||||
UsbDeviceNotifier(this._rpc) : super([]);
|
||||
|
||||
void _notifyWindowState(WindowState windowState) {
|
||||
if (windowState.active) {
|
||||
_pollDevices();
|
||||
} else {
|
||||
_pollTimer?.cancel();
|
||||
// Release any held device
|
||||
_rpc.command('get', ['usb']);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _pollDevices() async {
|
||||
_pollTimer?.cancel();
|
||||
|
||||
try {
|
||||
var scan = await _rpc.command('scan', ['usb']);
|
||||
|
||||
if (_usbState != scan['state'] || state.length != scan['pids'].length) {
|
||||
var usbResult = await _rpc.command('get', ['usb']);
|
||||
log.info('USB state change', jsonEncode(usbResult));
|
||||
_usbState = usbResult['data']['state'];
|
||||
List<UsbYubiKeyNode> usbDevices = [];
|
||||
|
||||
for (String id in (usbResult['children'] as Map).keys) {
|
||||
var path = ['usb', id];
|
||||
var deviceResult = await _rpc.command('get', path);
|
||||
var deviceData = deviceResult['data'];
|
||||
usbDevices.add(DeviceNode.usbYubiKey(
|
||||
path,
|
||||
deviceData['name'],
|
||||
deviceData['pid'],
|
||||
DeviceInfo.fromJson(deviceData['info']),
|
||||
) as UsbYubiKeyNode);
|
||||
}
|
||||
|
||||
log.info('USB state updated');
|
||||
if (mounted) {
|
||||
state = usbDevices;
|
||||
}
|
||||
}
|
||||
} on RpcError catch (e) {
|
||||
log.severe('Error polling USB', jsonEncode(e));
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
_pollTimer = Timer(_usbPollDelay, _pollDevices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final _nfcDevicesProvider =
|
||||
StateNotifierProvider<NfcDeviceNotifier, List<NfcReaderNode>>((ref) {
|
||||
final notifier = NfcDeviceNotifier(ref.watch(rpcProvider));
|
||||
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
|
||||
notifier._notifyWindowState(windowState);
|
||||
}, fireImmediately: true);
|
||||
return notifier;
|
||||
});
|
||||
|
||||
class NfcDeviceNotifier extends StateNotifier<List<NfcReaderNode>> {
|
||||
final RpcSession _rpc;
|
||||
Timer? _pollTimer;
|
||||
String _nfcState = '';
|
||||
NfcDeviceNotifier(this._rpc) : super([]);
|
||||
|
||||
void _notifyWindowState(WindowState windowState) {
|
||||
if (windowState.active) {
|
||||
_pollReaders();
|
||||
} else {
|
||||
_pollTimer?.cancel();
|
||||
// Release any held device
|
||||
_rpc.command('get', ['nfc']);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _pollReaders() async {
|
||||
_pollTimer?.cancel();
|
||||
|
||||
try {
|
||||
var children = await _rpc.command('scan', ['nfc']);
|
||||
var newState = children.keys.join(':');
|
||||
|
||||
if (mounted && newState != _nfcState) {
|
||||
log.info('NFC state change', jsonEncode(children));
|
||||
_nfcState = newState;
|
||||
state = children.entries
|
||||
.map((e) =>
|
||||
DeviceNode.nfcReader(['nfc', e.key], e.value['name'] as String)
|
||||
as NfcReaderNode)
|
||||
.toList();
|
||||
}
|
||||
} on RpcError catch (e) {
|
||||
log.severe('Error polling NFC', jsonEncode(e));
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
_pollTimer = Timer(_nfcPollDelay, _pollReaders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final desktopDevicesProvider = Provider<List<DeviceNode>>((ref) {
|
||||
final usbDevices = ref.watch(_usbDevicesProvider).toList();
|
||||
final nfcDevices = ref.watch(_nfcDevicesProvider).toList();
|
||||
usbDevices.sort((a, b) => a.name.compareTo(b.name));
|
||||
nfcDevices.sort((a, b) => a.name.compareTo(b.name));
|
||||
return [...usbDevices, ...nfcDevices];
|
||||
});
|
||||
|
||||
final _desktopDeviceDataProvider =
|
||||
StateNotifierProvider<CurrentDeviceDataNotifier, YubiKeyData?>((ref) {
|
||||
final notifier = CurrentDeviceDataNotifier(
|
||||
ref.watch(rpcProvider),
|
||||
ref.watch(currentDeviceProvider),
|
||||
);
|
||||
if (notifier._deviceNode is NfcReaderNode) {
|
||||
// If this is an NFC reader, listen on WindowState.
|
||||
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
|
||||
notifier._notifyWindowState(windowState);
|
||||
}, fireImmediately: true);
|
||||
}
|
||||
return notifier;
|
||||
});
|
||||
|
||||
final desktopDeviceDataProvider = Provider<YubiKeyData?>(
|
||||
(ref) => ref.watch(_desktopDeviceDataProvider),
|
||||
);
|
||||
|
||||
class CurrentDeviceDataNotifier extends StateNotifier<YubiKeyData?> {
|
||||
final RpcSession _rpc;
|
||||
final DeviceNode? _deviceNode;
|
||||
Timer? _pollTimer;
|
||||
|
||||
CurrentDeviceDataNotifier(this._rpc, this._deviceNode) : super(null) {
|
||||
final dev = _deviceNode;
|
||||
if (dev is UsbYubiKeyNode) {
|
||||
state = YubiKeyData(dev, dev.name, dev.info);
|
||||
}
|
||||
}
|
||||
|
||||
void _notifyWindowState(WindowState windowState) {
|
||||
if (windowState.active) {
|
||||
_pollReader();
|
||||
} else {
|
||||
_pollTimer?.cancel();
|
||||
// TODO: Should we clear the key here?
|
||||
/*if (mounted) {
|
||||
state = null;
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _pollReader() async {
|
||||
_pollTimer?.cancel();
|
||||
final node = _deviceNode!;
|
||||
try {
|
||||
var result = await _rpc.command('get', node.path);
|
||||
if (mounted) {
|
||||
if (result['data']['present']) {
|
||||
state = YubiKeyData(node, result['data']['name'],
|
||||
DeviceInfo.fromJson(result['data']['info']));
|
||||
} else {
|
||||
state = null;
|
||||
}
|
||||
}
|
||||
} on RpcError catch (e) {
|
||||
log.severe('Error polling NFC', jsonEncode(e));
|
||||
}
|
||||
if (mounted) {
|
||||
_pollTimer = Timer(
|
||||
state == null ? _nfcAttachPollDelay : _nfcDetachPollDelay,
|
||||
_pollReader);
|
||||
}
|
||||
}
|
||||
}
|
56
lib/desktop/init.dart
Executable file
56
lib/desktop/init.dart
Executable file
@ -0,0 +1,56 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:yubico_authenticator/desktop/devices.dart';
|
||||
import 'package:yubico_authenticator/desktop/oath/state.dart';
|
||||
import 'package:yubico_authenticator/desktop/state.dart';
|
||||
import 'package:yubico_authenticator/oath/state.dart';
|
||||
|
||||
import '../app/state.dart';
|
||||
import 'rpc.dart';
|
||||
|
||||
final log = Logger('desktop.init');
|
||||
|
||||
Future<List<Override>> initializeAndGetOverrides() async {
|
||||
await windowManager.ensureInitialized();
|
||||
|
||||
// Linux doesn't currently support hiding the window at start currently.
|
||||
// For now, this size should match linux/flutter/my_application.cc to avoid window flicker at startup.
|
||||
unawaited(windowManager.waitUntilReadyToShow().then((_) async {
|
||||
await windowManager.setSize(const Size(400, 720));
|
||||
await windowManager.show();
|
||||
}));
|
||||
|
||||
// Either use the _YKMAN_EXE environment variable, or look relative to executable.
|
||||
var exe = Platform.environment['_YKMAN_PATH'];
|
||||
if (exe?.isEmpty ?? true) {
|
||||
var relativePath = 'ykman/ykman';
|
||||
if (Platform.isMacOS) {
|
||||
relativePath = '../Resources/' + relativePath;
|
||||
} else if (Platform.isWindows) {
|
||||
relativePath += '.exe';
|
||||
}
|
||||
exe = Uri.file(Platform.resolvedExecutable)
|
||||
.resolve(relativePath)
|
||||
.toFilePath();
|
||||
}
|
||||
|
||||
log.info('Starting subprocess: $exe');
|
||||
var rpc = await RpcSession.launch(exe!);
|
||||
log.info('ykman process started', exe);
|
||||
rpc.setLogLevel(Logger.root.level);
|
||||
|
||||
return [
|
||||
rpcProvider.overrideWithValue(rpc),
|
||||
windowStateProvider.overrideWithProvider(desktopWindowStateProvider),
|
||||
attachedDevicesProvider.overrideWithProvider(desktopDevicesProvider),
|
||||
currentDeviceDataProvider.overrideWithProvider(desktopDeviceDataProvider),
|
||||
oathStateProvider.overrideWithProvider(desktopOathState),
|
||||
credentialListProvider
|
||||
.overrideWithProvider(desktopOathCredentialListProvider),
|
||||
];
|
||||
}
|
20
lib/desktop/models.dart
Executable file
20
lib/desktop/models.dart
Executable file
@ -0,0 +1,20 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'models.freezed.dart';
|
||||
part 'models.g.dart';
|
||||
|
||||
@Freezed(unionKey: 'kind')
|
||||
class RpcResponse with _$RpcResponse {
|
||||
factory RpcResponse.success(Map<String, dynamic> body) = Success;
|
||||
factory RpcResponse.signal(String status, Map<String, dynamic> body) = Signal;
|
||||
factory RpcResponse.error(
|
||||
String status, String message, Map<String, dynamic> body) = RpcError;
|
||||
|
||||
factory RpcResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$RpcResponseFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RpcState with _$RpcState {
|
||||
const factory RpcState(String version) = _RpcState;
|
||||
}
|
776
lib/desktop/models.freezed.dart
Executable file
776
lib/desktop/models.freezed.dart
Executable file
@ -0,0 +1,776 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
|
||||
|
||||
part of 'models.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
RpcResponse _$RpcResponseFromJson(Map<String, dynamic> json) {
|
||||
switch (json['kind']) {
|
||||
case 'success':
|
||||
return Success.fromJson(json);
|
||||
case 'signal':
|
||||
return Signal.fromJson(json);
|
||||
case 'error':
|
||||
return RpcError.fromJson(json);
|
||||
|
||||
default:
|
||||
throw CheckedFromJsonException(
|
||||
json, 'kind', 'RpcResponse', 'Invalid union type "${json['kind']}"!');
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$RpcResponseTearOff {
|
||||
const _$RpcResponseTearOff();
|
||||
|
||||
Success success(Map<String, dynamic> body) {
|
||||
return Success(
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
Signal signal(String status, Map<String, dynamic> body) {
|
||||
return Signal(
|
||||
status,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
RpcError error(String status, String message, Map<String, dynamic> body) {
|
||||
return RpcError(
|
||||
status,
|
||||
message,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
RpcResponse fromJson(Map<String, Object?> json) {
|
||||
return RpcResponse.fromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
const $RpcResponse = _$RpcResponseTearOff();
|
||||
|
||||
/// @nodoc
|
||||
mixin _$RpcResponse {
|
||||
Map<String, dynamic> get body => throw _privateConstructorUsedError;
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(Map<String, dynamic> body) success,
|
||||
required TResult Function(String status, Map<String, dynamic> body) signal,
|
||||
required TResult Function(
|
||||
String status, String message, Map<String, dynamic> body)
|
||||
error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(Success value) success,
|
||||
required TResult Function(Signal value) signal,
|
||||
required TResult Function(RpcError value) error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$RpcResponseCopyWith<RpcResponse> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $RpcResponseCopyWith<$Res> {
|
||||
factory $RpcResponseCopyWith(
|
||||
RpcResponse value, $Res Function(RpcResponse) then) =
|
||||
_$RpcResponseCopyWithImpl<$Res>;
|
||||
$Res call({Map<String, dynamic> body});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$RpcResponseCopyWithImpl<$Res> implements $RpcResponseCopyWith<$Res> {
|
||||
_$RpcResponseCopyWithImpl(this._value, this._then);
|
||||
|
||||
final RpcResponse _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function(RpcResponse) _then;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? body = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
body: body == freezed
|
||||
? _value.body
|
||||
: body // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SuccessCopyWith<$Res> implements $RpcResponseCopyWith<$Res> {
|
||||
factory $SuccessCopyWith(Success value, $Res Function(Success) then) =
|
||||
_$SuccessCopyWithImpl<$Res>;
|
||||
@override
|
||||
$Res call({Map<String, dynamic> body});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SuccessCopyWithImpl<$Res> extends _$RpcResponseCopyWithImpl<$Res>
|
||||
implements $SuccessCopyWith<$Res> {
|
||||
_$SuccessCopyWithImpl(Success _value, $Res Function(Success) _then)
|
||||
: super(_value, (v) => _then(v as Success));
|
||||
|
||||
@override
|
||||
Success get _value => super._value as Success;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? body = freezed,
|
||||
}) {
|
||||
return _then(Success(
|
||||
body == freezed
|
||||
? _value.body
|
||||
: body // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$Success implements Success {
|
||||
_$Success(this.body, {String? $type}) : $type = $type ?? 'success';
|
||||
|
||||
factory _$Success.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SuccessFromJson(json);
|
||||
|
||||
@override
|
||||
final Map<String, dynamic> body;
|
||||
|
||||
@JsonKey(name: 'kind')
|
||||
final String $type;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RpcResponse.success(body: $body)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is Success &&
|
||||
const DeepCollectionEquality().equals(other.body, body));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, const DeepCollectionEquality().hash(body));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
$SuccessCopyWith<Success> get copyWith =>
|
||||
_$SuccessCopyWithImpl<Success>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(Map<String, dynamic> body) success,
|
||||
required TResult Function(String status, Map<String, dynamic> body) signal,
|
||||
required TResult Function(
|
||||
String status, String message, Map<String, dynamic> body)
|
||||
error,
|
||||
}) {
|
||||
return success(body);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
}) {
|
||||
return success?.call(body);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (success != null) {
|
||||
return success(body);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(Success value) success,
|
||||
required TResult Function(Signal value) signal,
|
||||
required TResult Function(RpcError value) error,
|
||||
}) {
|
||||
return success(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
}) {
|
||||
return success?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (success != null) {
|
||||
return success(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SuccessToJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Success implements RpcResponse {
|
||||
factory Success(Map<String, dynamic> body) = _$Success;
|
||||
|
||||
factory Success.fromJson(Map<String, dynamic> json) = _$Success.fromJson;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get body;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
$SuccessCopyWith<Success> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SignalCopyWith<$Res> implements $RpcResponseCopyWith<$Res> {
|
||||
factory $SignalCopyWith(Signal value, $Res Function(Signal) then) =
|
||||
_$SignalCopyWithImpl<$Res>;
|
||||
@override
|
||||
$Res call({String status, Map<String, dynamic> body});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SignalCopyWithImpl<$Res> extends _$RpcResponseCopyWithImpl<$Res>
|
||||
implements $SignalCopyWith<$Res> {
|
||||
_$SignalCopyWithImpl(Signal _value, $Res Function(Signal) _then)
|
||||
: super(_value, (v) => _then(v as Signal));
|
||||
|
||||
@override
|
||||
Signal get _value => super._value as Signal;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? status = freezed,
|
||||
Object? body = freezed,
|
||||
}) {
|
||||
return _then(Signal(
|
||||
status == freezed
|
||||
? _value.status
|
||||
: status // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
body == freezed
|
||||
? _value.body
|
||||
: body // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$Signal implements Signal {
|
||||
_$Signal(this.status, this.body, {String? $type}) : $type = $type ?? 'signal';
|
||||
|
||||
factory _$Signal.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SignalFromJson(json);
|
||||
|
||||
@override
|
||||
final String status;
|
||||
@override
|
||||
final Map<String, dynamic> body;
|
||||
|
||||
@JsonKey(name: 'kind')
|
||||
final String $type;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RpcResponse.signal(status: $status, body: $body)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is Signal &&
|
||||
const DeepCollectionEquality().equals(other.status, status) &&
|
||||
const DeepCollectionEquality().equals(other.body, body));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(status),
|
||||
const DeepCollectionEquality().hash(body));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
$SignalCopyWith<Signal> get copyWith =>
|
||||
_$SignalCopyWithImpl<Signal>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(Map<String, dynamic> body) success,
|
||||
required TResult Function(String status, Map<String, dynamic> body) signal,
|
||||
required TResult Function(
|
||||
String status, String message, Map<String, dynamic> body)
|
||||
error,
|
||||
}) {
|
||||
return signal(status, body);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
}) {
|
||||
return signal?.call(status, body);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (signal != null) {
|
||||
return signal(status, body);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(Success value) success,
|
||||
required TResult Function(Signal value) signal,
|
||||
required TResult Function(RpcError value) error,
|
||||
}) {
|
||||
return signal(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
}) {
|
||||
return signal?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (signal != null) {
|
||||
return signal(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SignalToJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Signal implements RpcResponse {
|
||||
factory Signal(String status, Map<String, dynamic> body) = _$Signal;
|
||||
|
||||
factory Signal.fromJson(Map<String, dynamic> json) = _$Signal.fromJson;
|
||||
|
||||
String get status;
|
||||
@override
|
||||
Map<String, dynamic> get body;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
$SignalCopyWith<Signal> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $RpcErrorCopyWith<$Res> implements $RpcResponseCopyWith<$Res> {
|
||||
factory $RpcErrorCopyWith(RpcError value, $Res Function(RpcError) then) =
|
||||
_$RpcErrorCopyWithImpl<$Res>;
|
||||
@override
|
||||
$Res call({String status, String message, Map<String, dynamic> body});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$RpcErrorCopyWithImpl<$Res> extends _$RpcResponseCopyWithImpl<$Res>
|
||||
implements $RpcErrorCopyWith<$Res> {
|
||||
_$RpcErrorCopyWithImpl(RpcError _value, $Res Function(RpcError) _then)
|
||||
: super(_value, (v) => _then(v as RpcError));
|
||||
|
||||
@override
|
||||
RpcError get _value => super._value as RpcError;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? status = freezed,
|
||||
Object? message = freezed,
|
||||
Object? body = freezed,
|
||||
}) {
|
||||
return _then(RpcError(
|
||||
status == freezed
|
||||
? _value.status
|
||||
: status // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
message == freezed
|
||||
? _value.message
|
||||
: message // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
body == freezed
|
||||
? _value.body
|
||||
: body // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$RpcError implements RpcError {
|
||||
_$RpcError(this.status, this.message, this.body, {String? $type})
|
||||
: $type = $type ?? 'error';
|
||||
|
||||
factory _$RpcError.fromJson(Map<String, dynamic> json) =>
|
||||
_$$RpcErrorFromJson(json);
|
||||
|
||||
@override
|
||||
final String status;
|
||||
@override
|
||||
final String message;
|
||||
@override
|
||||
final Map<String, dynamic> body;
|
||||
|
||||
@JsonKey(name: 'kind')
|
||||
final String $type;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RpcResponse.error(status: $status, message: $message, body: $body)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is RpcError &&
|
||||
const DeepCollectionEquality().equals(other.status, status) &&
|
||||
const DeepCollectionEquality().equals(other.message, message) &&
|
||||
const DeepCollectionEquality().equals(other.body, body));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(status),
|
||||
const DeepCollectionEquality().hash(message),
|
||||
const DeepCollectionEquality().hash(body));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
$RpcErrorCopyWith<RpcError> get copyWith =>
|
||||
_$RpcErrorCopyWithImpl<RpcError>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(Map<String, dynamic> body) success,
|
||||
required TResult Function(String status, Map<String, dynamic> body) signal,
|
||||
required TResult Function(
|
||||
String status, String message, Map<String, dynamic> body)
|
||||
error,
|
||||
}) {
|
||||
return error(status, message, body);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
}) {
|
||||
return error?.call(status, message, body);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (error != null) {
|
||||
return error(status, message, body);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(Success value) success,
|
||||
required TResult Function(Signal value) signal,
|
||||
required TResult Function(RpcError value) error,
|
||||
}) {
|
||||
return error(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
}) {
|
||||
return error?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(Success value)? success,
|
||||
TResult Function(Signal value)? signal,
|
||||
TResult Function(RpcError value)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (error != null) {
|
||||
return error(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$RpcErrorToJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class RpcError implements RpcResponse {
|
||||
factory RpcError(String status, String message, Map<String, dynamic> body) =
|
||||
_$RpcError;
|
||||
|
||||
factory RpcError.fromJson(Map<String, dynamic> json) = _$RpcError.fromJson;
|
||||
|
||||
String get status;
|
||||
String get message;
|
||||
@override
|
||||
Map<String, dynamic> get body;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
$RpcErrorCopyWith<RpcError> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$RpcStateTearOff {
|
||||
const _$RpcStateTearOff();
|
||||
|
||||
_RpcState call(String version) {
|
||||
return _RpcState(
|
||||
version,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
const $RpcState = _$RpcStateTearOff();
|
||||
|
||||
/// @nodoc
|
||||
mixin _$RpcState {
|
||||
String get version => throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$RpcStateCopyWith<RpcState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $RpcStateCopyWith<$Res> {
|
||||
factory $RpcStateCopyWith(RpcState value, $Res Function(RpcState) then) =
|
||||
_$RpcStateCopyWithImpl<$Res>;
|
||||
$Res call({String version});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$RpcStateCopyWithImpl<$Res> implements $RpcStateCopyWith<$Res> {
|
||||
_$RpcStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
final RpcState _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function(RpcState) _then;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? version = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
version: version == freezed
|
||||
? _value.version
|
||||
: version // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$RpcStateCopyWith<$Res> implements $RpcStateCopyWith<$Res> {
|
||||
factory _$RpcStateCopyWith(_RpcState value, $Res Function(_RpcState) then) =
|
||||
__$RpcStateCopyWithImpl<$Res>;
|
||||
@override
|
||||
$Res call({String version});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$RpcStateCopyWithImpl<$Res> extends _$RpcStateCopyWithImpl<$Res>
|
||||
implements _$RpcStateCopyWith<$Res> {
|
||||
__$RpcStateCopyWithImpl(_RpcState _value, $Res Function(_RpcState) _then)
|
||||
: super(_value, (v) => _then(v as _RpcState));
|
||||
|
||||
@override
|
||||
_RpcState get _value => super._value as _RpcState;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? version = freezed,
|
||||
}) {
|
||||
return _then(_RpcState(
|
||||
version == freezed
|
||||
? _value.version
|
||||
: version // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$_RpcState implements _RpcState {
|
||||
const _$_RpcState(this.version);
|
||||
|
||||
@override
|
||||
final String version;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RpcState(version: $version)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _RpcState &&
|
||||
const DeepCollectionEquality().equals(other.version, version));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, const DeepCollectionEquality().hash(version));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
_$RpcStateCopyWith<_RpcState> get copyWith =>
|
||||
__$RpcStateCopyWithImpl<_RpcState>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _RpcState implements RpcState {
|
||||
const factory _RpcState(String version) = _$_RpcState;
|
||||
|
||||
@override
|
||||
String get version;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$RpcStateCopyWith<_RpcState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
330
lib/desktop/oath/state.dart
Executable file
330
lib/desktop/oath/state.dart
Executable file
@ -0,0 +1,330 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../oath/models.dart';
|
||||
import '../../oath/state.dart';
|
||||
import '../rpc.dart';
|
||||
import '../state.dart';
|
||||
|
||||
final log = Logger('desktop.oath.state');
|
||||
|
||||
final _sessionProvider =
|
||||
Provider.autoDispose.family<RpcNodeSession, List<String>>(
|
||||
(ref, devicePath) =>
|
||||
RpcNodeSession(ref.watch(rpcProvider), devicePath, ['ccid', 'oath']),
|
||||
);
|
||||
|
||||
final desktopOathState = StateNotifierProvider.autoDispose
|
||||
.family<OathStateNotifier, OathState?, List<String>>(
|
||||
(ref, devicePath) {
|
||||
final session = ref.watch(_sessionProvider(devicePath));
|
||||
final notifier = _DesktopOathStateNotifier(session, ref);
|
||||
session
|
||||
..setErrorHandler('state-reset', (_) async {
|
||||
ref.refresh(_sessionProvider(devicePath));
|
||||
})
|
||||
..setErrorHandler('auth-required', (_) async {
|
||||
await notifier.refresh();
|
||||
});
|
||||
ref.onDispose(() {
|
||||
session
|
||||
..unserErrorHandler('state-reset')
|
||||
..unserErrorHandler('auth-required');
|
||||
});
|
||||
return notifier..refresh();
|
||||
},
|
||||
);
|
||||
|
||||
class _DesktopOathStateNotifier extends OathStateNotifier {
|
||||
final RpcNodeSession _session;
|
||||
final Ref _ref;
|
||||
_DesktopOathStateNotifier(this._session, this._ref) : super();
|
||||
|
||||
refresh() async {
|
||||
var result = await _session.command('get');
|
||||
log.config('application status', jsonEncode(result));
|
||||
var oathState = OathState.fromJson(result['data']);
|
||||
final key = _ref.read(oathLockKeyProvider(_session.devicePath));
|
||||
if (oathState.locked && key != null) {
|
||||
final result = await _session.command('validate', params: {'key': key});
|
||||
if (result['unlocked']) {
|
||||
oathState = oathState.copyWith(locked: false);
|
||||
} else {
|
||||
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
state = oathState;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reset() async {
|
||||
await _session.command('reset');
|
||||
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
_ref.refresh(_sessionProvider(_session.devicePath));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> unlock(String password) async {
|
||||
var result =
|
||||
await _session.command('derive', params: {'password': password});
|
||||
var key = result['key'];
|
||||
final status = await _session.command('validate', params: {'key': key});
|
||||
if (mounted && status['unlocked']) {
|
||||
log.config('applet unlocked');
|
||||
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
|
||||
state = state?.copyWith(locked: false);
|
||||
}
|
||||
return status['unlocked'];
|
||||
}
|
||||
|
||||
Future<bool> _checkPassword(String password) async {
|
||||
var result =
|
||||
await _session.command('derive', params: {'password': password});
|
||||
return _ref.read(oathLockKeyProvider(_session.devicePath)) == result['key'];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setPassword(String? current, String password) async {
|
||||
if (state?.hasKey ?? false) {
|
||||
if (current != null) {
|
||||
if (!await _checkPassword(current)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var result =
|
||||
await _session.command('derive', params: {'password': password});
|
||||
var key = result['key'];
|
||||
await _session.command('set_key', params: {'key': key});
|
||||
log.config('OATH key set');
|
||||
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
|
||||
if (mounted) {
|
||||
state = state?.copyWith(hasKey: true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> unsetPassword(String current) async {
|
||||
if (state?.hasKey ?? false) {
|
||||
if (!await _checkPassword(current)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
await _session.command('unset_key');
|
||||
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
if (mounted) {
|
||||
state = state?.copyWith(hasKey: false, locked: false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final desktopOathCredentialListProvider = StateNotifierProvider.autoDispose
|
||||
.family<OathCredentialListNotifier, List<OathPair>?, List<String>>(
|
||||
(ref, devicePath) {
|
||||
var notifier = _DesktopCredentialListNotifier(
|
||||
ref.watch(_sessionProvider(devicePath)),
|
||||
ref.watch(oathStateProvider(devicePath).select((s) => s?.locked ?? true)),
|
||||
);
|
||||
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
|
||||
notifier._notifyWindowState(windowState);
|
||||
}, fireImmediately: true);
|
||||
return notifier;
|
||||
},
|
||||
);
|
||||
|
||||
extension on OathCredential {
|
||||
bool get isSteam => issuer == 'Steam' && oathType == OathType.totp;
|
||||
}
|
||||
|
||||
const String _steamCharTable = '23456789BCDFGHJKMNPQRTVWXY';
|
||||
String _formatSteam(String response) {
|
||||
final offset = int.parse(response.substring(response.length - 1), radix: 16);
|
||||
var number =
|
||||
int.parse(response.substring(offset * 2, offset * 2 + 8), radix: 16) &
|
||||
0x7fffffff;
|
||||
var value = '';
|
||||
for (var i = 0; i < 5; i++) {
|
||||
value += _steamCharTable[number % _steamCharTable.length];
|
||||
number ~/= _steamCharTable.length;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
class _DesktopCredentialListNotifier extends OathCredentialListNotifier {
|
||||
final RpcNodeSession _session;
|
||||
final bool _locked;
|
||||
Timer? _timer;
|
||||
_DesktopCredentialListNotifier(this._session, this._locked) : super();
|
||||
|
||||
void _notifyWindowState(WindowState windowState) {
|
||||
if (_locked) return;
|
||||
if (windowState.active) {
|
||||
_scheduleRefresh();
|
||||
} else {
|
||||
_timer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<OathCode> calculate(OathCredential credential,
|
||||
{bool update = true}) async {
|
||||
final OathCode code;
|
||||
if (credential.isSteam) {
|
||||
final timeStep = DateTime.now().millisecondsSinceEpoch ~/ 30000;
|
||||
var result = await _session.command('calculate', target: [
|
||||
'accounts',
|
||||
credential.id
|
||||
], params: {
|
||||
'challenge': timeStep.toRadixString(16).padLeft(16, '0'),
|
||||
});
|
||||
code = OathCode(
|
||||
_formatSteam(result['response']), timeStep * 30, (timeStep + 1) * 30);
|
||||
} else {
|
||||
var result =
|
||||
await _session.command('code', target: ['accounts', credential.id]);
|
||||
code = OathCode.fromJson(result);
|
||||
}
|
||||
log.config('Calculate', jsonEncode(code));
|
||||
if (update && mounted) {
|
||||
final creds = state!.toList();
|
||||
final i = creds.indexWhere((e) => e.credential.id == credential.id);
|
||||
state = creds..[i] = creds[i].copyWith(code: code);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<OathCredential> addAccount(Uri otpauth,
|
||||
{bool requireTouch = false, bool update = true}) async {
|
||||
var result = await _session.command('put', target: [
|
||||
'accounts'
|
||||
], params: {
|
||||
'uri': otpauth.toString(),
|
||||
'require_touch': requireTouch,
|
||||
});
|
||||
final credential = OathCredential.fromJson(result);
|
||||
if (update && mounted) {
|
||||
state = state!.toList()..add(OathPair(credential, null));
|
||||
if (!requireTouch && credential.oathType == OathType.totp) {
|
||||
await calculate(credential);
|
||||
}
|
||||
}
|
||||
return credential;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<OathCredential> renameAccount(
|
||||
OathCredential credential,
|
||||
String? issuer,
|
||||
String name,
|
||||
) async {
|
||||
final result = await _session.command('rename', target: [
|
||||
'accounts',
|
||||
credential.id,
|
||||
], params: {
|
||||
'issuer': issuer,
|
||||
'name': name,
|
||||
});
|
||||
String credentialId = result['credential_id'];
|
||||
final renamedCredential =
|
||||
credential.copyWith(id: credentialId, issuer: issuer, name: name);
|
||||
if (mounted) {
|
||||
final newState = state!.toList();
|
||||
final index = newState.indexWhere((e) => e.credential == credential);
|
||||
final oldPair = newState.removeAt(index);
|
||||
newState.add(OathPair(
|
||||
renamedCredential,
|
||||
oldPair.code,
|
||||
));
|
||||
state = newState;
|
||||
}
|
||||
return renamedCredential;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAccount(OathCredential credential) async {
|
||||
await _session.command('delete', target: ['accounts', credential.id]);
|
||||
if (mounted) {
|
||||
state = state!.toList()..removeWhere((e) => e.credential == credential);
|
||||
}
|
||||
}
|
||||
|
||||
refresh() async {
|
||||
if (_locked) return;
|
||||
log.config('refreshing credentials...');
|
||||
var result = await _session.command('calculate_all', target: ['accounts']);
|
||||
log.config('Entries', jsonEncode(result));
|
||||
|
||||
final pairs = [];
|
||||
for (var e in result['entries']) {
|
||||
final credential = OathCredential.fromJson(e['credential']);
|
||||
final code = e['code'] == null
|
||||
? null
|
||||
: credential.isSteam // Steam codes require a re-calculate
|
||||
? await calculate(credential, update: false)
|
||||
: OathCode.fromJson(e['code']);
|
||||
pairs.add(OathPair(credential, code));
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
final current = state?.toList() ?? [];
|
||||
for (var pair in pairs) {
|
||||
final i =
|
||||
current.indexWhere((e) => e.credential.id == pair.credential.id);
|
||||
if (i < 0) {
|
||||
current.add(pair);
|
||||
} else if (pair.code != null) {
|
||||
current[i] = current[i].copyWith(code: pair.code);
|
||||
}
|
||||
}
|
||||
state = current;
|
||||
_scheduleRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
_scheduleRefresh() {
|
||||
_timer?.cancel();
|
||||
if (_locked) return;
|
||||
if (state == null) {
|
||||
refresh();
|
||||
} else if (mounted) {
|
||||
final expirations = (state ?? [])
|
||||
.where((pair) =>
|
||||
pair.credential.oathType == OathType.totp &&
|
||||
!pair.credential.touchRequired)
|
||||
.map((e) => e.code)
|
||||
.whereType<OathCode>()
|
||||
.map((e) => e.validTo);
|
||||
if (expirations.isEmpty) {
|
||||
_timer = null;
|
||||
} else {
|
||||
final earliest = expirations.reduce(min) * 1000;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if (earliest < now) {
|
||||
refresh();
|
||||
} else {
|
||||
_timer = Timer(Duration(milliseconds: earliest - now), refresh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -142,3 +142,46 @@ class RpcSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typedef ErrorHandler = Future<void> Function(RpcError e);
|
||||
|
||||
class RpcNodeSession {
|
||||
final RpcSession _rpc;
|
||||
final List<String> devicePath;
|
||||
final List<String> subPath;
|
||||
final Map<String, ErrorHandler> _errorHandlers = {};
|
||||
|
||||
RpcNodeSession(this._rpc, this.devicePath, this.subPath);
|
||||
|
||||
void setErrorHandler(String status, ErrorHandler handler) {
|
||||
_errorHandlers[status] = handler;
|
||||
}
|
||||
|
||||
void unserErrorHandler(String status) {
|
||||
_errorHandlers.remove(status);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> command(
|
||||
String action, {
|
||||
List<String> target = const [],
|
||||
Map<dynamic, dynamic>? params,
|
||||
Signaler? signal,
|
||||
}) async {
|
||||
try {
|
||||
return await _rpc.command(
|
||||
action,
|
||||
devicePath + subPath + target,
|
||||
params: params,
|
||||
signal: signal,
|
||||
);
|
||||
} on RpcError catch (e) {
|
||||
final handler = _errorHandlers[e.status];
|
||||
if (handler != null) {
|
||||
log.info('Attempting recovery on "${e.status}"');
|
||||
await handler(e);
|
||||
return command(action, target: target, params: params, signal: signal);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
110
lib/desktop/state.dart
Executable file
110
lib/desktop/state.dart
Executable file
@ -0,0 +1,110 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../core/state.dart';
|
||||
import '../app/models.dart';
|
||||
import 'models.dart';
|
||||
import 'rpc.dart';
|
||||
|
||||
// This must be initialized before use in initialize.dart.
|
||||
final rpcProvider = Provider<RpcSession>((ref) {
|
||||
throw UnimplementedError();
|
||||
});
|
||||
|
||||
final rpcStateProvider = StateNotifierProvider<_RpcStateNotifier, RpcState>(
|
||||
(ref) {
|
||||
final rpc = ref.watch(rpcProvider);
|
||||
ref.listen<Level>(logLevelProvider, (_, level) {
|
||||
rpc.setLogLevel(level);
|
||||
}, fireImmediately: true);
|
||||
return _RpcStateNotifier(rpc);
|
||||
},
|
||||
);
|
||||
|
||||
class _RpcStateNotifier extends StateNotifier<RpcState> {
|
||||
final RpcSession rpc;
|
||||
_RpcStateNotifier(this.rpc) : super(const RpcState('unknown')) {
|
||||
_init();
|
||||
}
|
||||
|
||||
_init() async {
|
||||
final response = await rpc.command('get', []);
|
||||
if (mounted) {
|
||||
state = state.copyWith(version: response['data']['version']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final _windowStateProvider =
|
||||
StateNotifierProvider<_WindowStateNotifier, WindowState>(
|
||||
(ref) => _WindowStateNotifier());
|
||||
|
||||
final desktopWindowStateProvider = Provider<WindowState>(
|
||||
(ref) => ref.watch(_windowStateProvider),
|
||||
);
|
||||
|
||||
class _WindowStateNotifier extends StateNotifier<WindowState>
|
||||
with WindowListener {
|
||||
Timer? _idleTimer;
|
||||
_WindowStateNotifier()
|
||||
: super(WindowState(focused: true, visible: true, active: true)) {
|
||||
_init();
|
||||
}
|
||||
|
||||
void _init() async {
|
||||
windowManager.addListener(this);
|
||||
if (!await windowManager.isVisible() && mounted) {
|
||||
state = WindowState(focused: false, visible: false, active: true);
|
||||
_idleTimer = Timer(const Duration(seconds: 5), () {
|
||||
if (mounted) {
|
||||
state = state.copyWith(active: false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
windowManager.removeListener(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
set state(WindowState value) {
|
||||
log.config('Window state changed: $value');
|
||||
super.state = value;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowEvent(String eventName) {
|
||||
if (mounted) {
|
||||
switch (eventName) {
|
||||
case 'blur':
|
||||
state = state.copyWith(focused: false);
|
||||
_idleTimer?.cancel();
|
||||
_idleTimer = Timer(const Duration(seconds: 5), () {
|
||||
if (mounted) {
|
||||
state = state.copyWith(active: false);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'focus':
|
||||
state = state.copyWith(focused: true, active: true);
|
||||
_idleTimer?.cancel();
|
||||
break;
|
||||
case 'minimize':
|
||||
state = state.copyWith(visible: false, active: false);
|
||||
_idleTimer?.cancel();
|
||||
break;
|
||||
case 'restore':
|
||||
state = state.copyWith(visible: true, active: true);
|
||||
break;
|
||||
default:
|
||||
log.fine('Window event ignored: $eventName');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'app/app.dart';
|
||||
import 'app/views/main_page.dart';
|
||||
import 'core/rpc.dart';
|
||||
import 'core/state.dart';
|
||||
import 'desktop/init.dart' as desktop;
|
||||
|
||||
import 'error_page.dart';
|
||||
|
||||
@ -20,47 +17,23 @@ final log = Logger('main');
|
||||
void main() async {
|
||||
_initLogging(Level.INFO);
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await windowManager.ensureInitialized();
|
||||
|
||||
// Either use the _YKMAN_EXE environment variable, or look relative to executable.
|
||||
var exe = Platform.environment['_YKMAN_PATH'];
|
||||
if (exe?.isEmpty ?? true) {
|
||||
var relativePath = 'ykman/ykman';
|
||||
if (Platform.isMacOS) {
|
||||
relativePath = '../Resources/' + relativePath;
|
||||
} else if (Platform.isWindows) {
|
||||
relativePath += '.exe';
|
||||
}
|
||||
exe = Uri.file(Platform.resolvedExecutable)
|
||||
.resolve(relativePath)
|
||||
.toFilePath();
|
||||
}
|
||||
|
||||
Widget page;
|
||||
List<Override> overrides = [
|
||||
prefProvider.overrideWithValue(await SharedPreferences.getInstance())
|
||||
prefProvider.overrideWithValue(await SharedPreferences.getInstance()),
|
||||
];
|
||||
|
||||
log.info('Starting subprocess: $exe');
|
||||
Widget page;
|
||||
try {
|
||||
var rpc = await RpcSession.launch(exe!);
|
||||
// Enable logging TODO: Make this configurable
|
||||
log.info('ykman process started', exe);
|
||||
rpc.setLogLevel(Logger.root.level);
|
||||
overrides.add(rpcProvider.overrideWithValue(rpc));
|
||||
// Platform specific initialization
|
||||
if (isDesktop) {
|
||||
log.config('Initializing desktop platform.');
|
||||
overrides.addAll(await desktop.initializeAndGetOverrides());
|
||||
}
|
||||
page = const MainPage();
|
||||
} catch (e) {
|
||||
log.warning('ykman process failed: $e');
|
||||
log.warning('Platform initialization failed: $e');
|
||||
page = ErrorPage(error: e.toString());
|
||||
}
|
||||
|
||||
// Linux doesn't currently support hiding the window at start currently.
|
||||
// For now, this size should match linux/flutter/my_application.cc to avoid window flicker at startup.
|
||||
unawaited(windowManager.waitUntilReadyToShow().then((_) async {
|
||||
await windowManager.setSize(const Size(400, 720));
|
||||
await windowManager.show();
|
||||
}));
|
||||
|
||||
runApp(ProviderScope(
|
||||
overrides: overrides,
|
||||
child: YubicoAuthenticatorApp(page: page),
|
||||
|
@ -1,12 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:yubico_authenticator/app/models.dart';
|
||||
|
||||
import '../app/state.dart';
|
||||
import '../core/state.dart';
|
||||
@ -14,12 +11,8 @@ import 'models.dart';
|
||||
|
||||
final log = Logger('oath.state');
|
||||
|
||||
final _sessionProvider = Provider.autoDispose
|
||||
.family<RpcNodeSession, List<String>>((ref, devicePath) =>
|
||||
RpcNodeSession(ref.watch(rpcProvider), devicePath, ['ccid', 'oath']));
|
||||
|
||||
// This remembers the key for all devices for the duration of the process.
|
||||
final _lockKeyProvider =
|
||||
final oathLockKeyProvider =
|
||||
StateNotifierProvider.family<_LockKeyNotifier, String?, List<String>>(
|
||||
(ref, devicePath) => _LockKeyNotifier(null));
|
||||
|
||||
@ -37,163 +30,26 @@ class _LockKeyNotifier extends StateNotifier<String?> {
|
||||
|
||||
final oathStateProvider = StateNotifierProvider.autoDispose
|
||||
.family<OathStateNotifier, OathState?, List<String>>(
|
||||
(ref, devicePath) {
|
||||
final session = ref.watch(_sessionProvider(devicePath));
|
||||
final notifier = OathStateNotifier(session, ref);
|
||||
session
|
||||
..setErrorHandler('state-reset', (_) async {
|
||||
ref.refresh(_sessionProvider(devicePath));
|
||||
})
|
||||
..setErrorHandler('auth-required', (_) async {
|
||||
await notifier.refresh();
|
||||
});
|
||||
ref.onDispose(() {
|
||||
session
|
||||
..unserErrorHandler('state-reset')
|
||||
..unserErrorHandler('auth-required');
|
||||
});
|
||||
return notifier..refresh();
|
||||
},
|
||||
(ref, devicePath) => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
class OathStateNotifier extends StateNotifier<OathState?> {
|
||||
final RpcNodeSession _session;
|
||||
final Ref _ref;
|
||||
OathStateNotifier(this._session, this._ref) : super(null);
|
||||
abstract class OathStateNotifier extends StateNotifier<OathState?> {
|
||||
OathStateNotifier() : super(null);
|
||||
|
||||
refresh() async {
|
||||
var result = await _session.command('get');
|
||||
log.config('application status', jsonEncode(result));
|
||||
var oathState = OathState.fromJson(result['data']);
|
||||
final key = _ref.read(_lockKeyProvider(_session.devicePath));
|
||||
if (oathState.locked && key != null) {
|
||||
final result = await _session.command('validate', params: {'key': key});
|
||||
if (result['unlocked']) {
|
||||
oathState = oathState.copyWith(locked: false);
|
||||
} else {
|
||||
_ref.read(_lockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
state = oathState;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reset() async {
|
||||
await _session.command('reset');
|
||||
_ref.read(_lockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
_ref.refresh(_sessionProvider(_session.devicePath));
|
||||
}
|
||||
|
||||
Future<bool> unlock(String password) async {
|
||||
var result =
|
||||
await _session.command('derive', params: {'password': password});
|
||||
var key = result['key'];
|
||||
final status = await _session.command('validate', params: {'key': key});
|
||||
if (mounted && status['unlocked']) {
|
||||
log.config('applet unlocked');
|
||||
_ref.read(_lockKeyProvider(_session.devicePath).notifier).setKey(key);
|
||||
state = state?.copyWith(locked: false);
|
||||
}
|
||||
return status['unlocked'];
|
||||
}
|
||||
|
||||
Future<bool> _checkPassword(String password) async {
|
||||
var result =
|
||||
await _session.command('derive', params: {'password': password});
|
||||
return _ref.read(_lockKeyProvider(_session.devicePath)) == result['key'];
|
||||
}
|
||||
|
||||
Future<bool> setPassword(String? current, String password) async {
|
||||
if (state?.hasKey ?? false) {
|
||||
if (current != null) {
|
||||
if (!await _checkPassword(current)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var result =
|
||||
await _session.command('derive', params: {'password': password});
|
||||
var key = result['key'];
|
||||
await _session.command('set_key', params: {'key': key});
|
||||
log.config('OATH key set');
|
||||
_ref.read(_lockKeyProvider(_session.devicePath).notifier).setKey(key);
|
||||
if (mounted) {
|
||||
state = state?.copyWith(hasKey: true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> unsetPassword(String current) async {
|
||||
if (state?.hasKey ?? false) {
|
||||
if (!await _checkPassword(current)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
await _session.command('unset_key');
|
||||
_ref.read(_lockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
if (mounted) {
|
||||
state = state?.copyWith(hasKey: false, locked: false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Future<void> reset();
|
||||
Future<bool> unlock(String password);
|
||||
Future<bool> setPassword(String? current, String password);
|
||||
Future<bool> unsetPassword(String current);
|
||||
}
|
||||
|
||||
final credentialListProvider = StateNotifierProvider.autoDispose
|
||||
.family<CredentialListNotifier, List<OathPair>?, List<String>>(
|
||||
(ref, devicePath) {
|
||||
var notifier = CredentialListNotifier(
|
||||
ref.watch(_sessionProvider(devicePath)),
|
||||
ref.watch(oathStateProvider(devicePath).select((s) => s?.locked ?? true)),
|
||||
);
|
||||
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
|
||||
notifier._notifyWindowState(windowState);
|
||||
}, fireImmediately: true);
|
||||
return notifier;
|
||||
},
|
||||
.family<OathCredentialListNotifier, List<OathPair>?, List<String>>(
|
||||
(ref, arg) => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
extension on OathCredential {
|
||||
bool get isSteam => issuer == 'Steam' && oathType == OathType.totp;
|
||||
}
|
||||
|
||||
const String _steamCharTable = '23456789BCDFGHJKMNPQRTVWXY';
|
||||
String _formatSteam(String response) {
|
||||
final offset = int.parse(response.substring(response.length - 1), radix: 16);
|
||||
var number =
|
||||
int.parse(response.substring(offset * 2, offset * 2 + 8), radix: 16) &
|
||||
0x7fffffff;
|
||||
var value = '';
|
||||
for (var i = 0; i < 5; i++) {
|
||||
value += _steamCharTable[number % _steamCharTable.length];
|
||||
number ~/= _steamCharTable.length;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
class CredentialListNotifier extends StateNotifier<List<OathPair>?> {
|
||||
final RpcNodeSession _session;
|
||||
final bool _locked;
|
||||
Timer? _timer;
|
||||
CredentialListNotifier(this._session, this._locked) : super(null);
|
||||
|
||||
void _notifyWindowState(WindowState windowState) {
|
||||
if (_locked) return;
|
||||
if (windowState.active) {
|
||||
_scheduleRefresh();
|
||||
} else {
|
||||
_timer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
abstract class OathCredentialListNotifier
|
||||
extends StateNotifier<List<OathPair>?> {
|
||||
OathCredentialListNotifier() : super(null);
|
||||
|
||||
@override
|
||||
@protected
|
||||
@ -201,142 +57,11 @@ class CredentialListNotifier extends StateNotifier<List<OathPair>?> {
|
||||
super.state = value != null ? List.unmodifiable(value) : null;
|
||||
}
|
||||
|
||||
Future<OathCode> calculate(OathCredential credential,
|
||||
{bool update = true}) async {
|
||||
final OathCode code;
|
||||
if (credential.isSteam) {
|
||||
final timeStep = DateTime.now().millisecondsSinceEpoch ~/ 30000;
|
||||
var result = await _session.command('calculate', target: [
|
||||
'accounts',
|
||||
credential.id
|
||||
], params: {
|
||||
'challenge': timeStep.toRadixString(16).padLeft(16, '0'),
|
||||
});
|
||||
code = OathCode(
|
||||
_formatSteam(result['response']), timeStep * 30, (timeStep + 1) * 30);
|
||||
} else {
|
||||
var result =
|
||||
await _session.command('code', target: ['accounts', credential.id]);
|
||||
code = OathCode.fromJson(result);
|
||||
}
|
||||
log.config('Calculate', jsonEncode(code));
|
||||
if (update && mounted) {
|
||||
final creds = state!.toList();
|
||||
final i = creds.indexWhere((e) => e.credential.id == credential.id);
|
||||
state = creds..[i] = creds[i].copyWith(code: code);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
Future<OathCredential> addAccount(Uri otpauth,
|
||||
{bool requireTouch = false, bool update = true}) async {
|
||||
var result = await _session.command('put', target: [
|
||||
'accounts'
|
||||
], params: {
|
||||
'uri': otpauth.toString(),
|
||||
'require_touch': requireTouch,
|
||||
});
|
||||
final credential = OathCredential.fromJson(result);
|
||||
if (update && mounted) {
|
||||
state = state!.toList()..add(OathPair(credential, null));
|
||||
if (!requireTouch && credential.oathType == OathType.totp) {
|
||||
await calculate(credential);
|
||||
}
|
||||
}
|
||||
return credential;
|
||||
}
|
||||
|
||||
Future<void> renameAccount(
|
||||
OathCredential credential,
|
||||
String? issuer,
|
||||
String name,
|
||||
) async {
|
||||
final result = await _session.command('rename', target: [
|
||||
'accounts',
|
||||
credential.id,
|
||||
], params: {
|
||||
'issuer': issuer,
|
||||
'name': name,
|
||||
});
|
||||
String credentialId = result['credential_id'];
|
||||
if (mounted) {
|
||||
final newState = state!.toList();
|
||||
final index = newState.indexWhere((e) => e.credential == credential);
|
||||
final oldPair = newState.removeAt(index);
|
||||
newState.add(OathPair(
|
||||
credential.copyWith(id: credentialId, issuer: issuer, name: name),
|
||||
oldPair.code,
|
||||
));
|
||||
state = newState;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteAccount(OathCredential credential) async {
|
||||
await _session.command('delete', target: ['accounts', credential.id]);
|
||||
if (mounted) {
|
||||
state = state!.toList()..removeWhere((e) => e.credential == credential);
|
||||
}
|
||||
}
|
||||
|
||||
refresh() async {
|
||||
if (_locked) return;
|
||||
log.config('refreshing credentials...');
|
||||
var result = await _session.command('calculate_all', target: ['accounts']);
|
||||
log.config('Entries', jsonEncode(result));
|
||||
|
||||
final pairs = [];
|
||||
for (var e in result['entries']) {
|
||||
final credential = OathCredential.fromJson(e['credential']);
|
||||
final code = e['code'] == null
|
||||
? null
|
||||
: credential.isSteam // Steam codes require a re-calculate
|
||||
? await calculate(credential, update: false)
|
||||
: OathCode.fromJson(e['code']);
|
||||
pairs.add(OathPair(credential, code));
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
final current = state?.toList() ?? [];
|
||||
for (var pair in pairs) {
|
||||
final i =
|
||||
current.indexWhere((e) => e.credential.id == pair.credential.id);
|
||||
if (i < 0) {
|
||||
current.add(pair);
|
||||
} else if (pair.code != null) {
|
||||
current[i] = current[i].copyWith(code: pair.code);
|
||||
}
|
||||
}
|
||||
state = current;
|
||||
_scheduleRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
_scheduleRefresh() {
|
||||
_timer?.cancel();
|
||||
if (_locked) return;
|
||||
if (state == null) {
|
||||
refresh();
|
||||
} else if (mounted) {
|
||||
final expirations = (state ?? [])
|
||||
.where((pair) =>
|
||||
pair.credential.oathType == OathType.totp &&
|
||||
!pair.credential.touchRequired)
|
||||
.map((e) => e.code)
|
||||
.whereType<OathCode>()
|
||||
.map((e) => e.validTo);
|
||||
if (expirations.isEmpty) {
|
||||
_timer = null;
|
||||
} else {
|
||||
final earliest = expirations.reduce(min) * 1000;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if (earliest < now) {
|
||||
refresh();
|
||||
} else {
|
||||
_timer = Timer(Duration(milliseconds: earliest - now), refresh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Future<OathCode> calculate(OathCredential credential);
|
||||
Future<OathCredential> addAccount(Uri otpauth, {bool requireTouch = false});
|
||||
Future<OathCredential> renameAccount(
|
||||
OathCredential credential, String? issuer, String name);
|
||||
Future<void> deleteAccount(OathCredential credential);
|
||||
}
|
||||
|
||||
final favoritesProvider =
|
||||
|
Loading…
Reference in New Issue
Block a user