import 'dart:io';
import 'dart:async';
+import 'package:biometric_storage/biometric_storage.dart';
import 'package:flutter/material.dart';
import 'package:collection/src/iterable_extensions.dart';
import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
import 'package:proxmox_login_manager/proxmox_login_model.dart';
import 'package:proxmox_login_manager/proxmox_tfa_form.dart';
import 'package:proxmox_login_manager/extension.dart';
+import 'package:proxmox_login_manager/proxmox_password_store.dart';
class ProxmoxProgressModel {
int inProgress = 0;
final ValueChanged<PveAccessDomainModel?> onDomainChanged;
final Function? onPasswordSubmitted;
final Function? onOriginSubmitted;
+ final Function? onSavePasswordChanged;
+ final bool? canSavePassword;
+ final bool? passwordSaved;
const ProxmoxLoginForm({
Key? key,
required this.onDomainChanged,
this.onPasswordSubmitted,
this.onOriginSubmitted,
+ this.onSavePasswordChanged,
+ this.canSavePassword,
+ this.passwordSaved,
}) : super(key: key);
@override
class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
bool _obscure = true;
+ bool? _savePwCheckbox;
FocusNode? passwordFocusNode;
@override
)
],
),
+ if (widget.canSavePassword ?? false)
+ CheckboxListTile(
+ title: const Text('Save password'),
+ value: _savePwCheckbox ?? widget.passwordSaved ?? false,
+ onChanged: (value) {
+ if (widget.onSavePasswordChanged != null) {
+ widget.onSavePasswordChanged!(value!);
+ }
+ setState(() {
+ _savePwCheckbox = value!;
+ });
+ },
+ )
],
),
);
final ProxmoxLoginModel? userModel;
final bool? isCreate;
final String? ticket;
+ final String? password;
const ProxmoxLoginPage({
Key? key,
this.userModel,
this.isCreate,
this.ticket = '',
+ this.password,
}) : super(key: key);
@override
_ProxmoxLoginPageState createState() => _ProxmoxLoginPageState();
final _formKey = GlobalKey<FormState>();
ProxmoxProgressModel _progressModel = ProxmoxProgressModel();
bool _submittButtonEnabled = true;
+ bool _canSavePassword = false;
+ bool _savePasswordCB = false;
@override
void initState() {
_progressModel = ProxmoxProgressModel();
if (!widget.isCreate! && userModel != null) {
_originController.text = userModel.origin?.toString() ?? '';
+ _passwordController.text = widget.password ?? '';
_accessDomains = _getAccessDomains();
_usernameController.text = userModel.username!;
- if (widget.ticket!.isNotEmpty && userModel.activeSession) {
+ _savePasswordCB = widget.password != null;
+ if ((widget.ticket!.isNotEmpty && userModel.activeSession) ||
+ widget.password != null) {
_onLoginButtonPressed(ticket: widget.ticket!, mRealm: userModel.realm);
}
}
disabledBackgroundColor: Colors.grey,
),
),
+ toggleableActiveColor: ProxmoxColors.orange,
colorScheme: ColorScheme.dark().copyWith(
primary: ProxmoxColors.orange,
secondary: ProxmoxColors.orange,
passwordController: _passwordController,
accessDomains: snapshot.data,
selectedDomain: _selectedDomain,
+ onSavePasswordChanged: (value) {
+ _savePasswordCB = value;
+ },
+ canSavePassword: _canSavePassword,
+ passwordSaved: widget.password != null,
onDomainChanged: (value) {
setState(() {
_selectedDomain = value;
try {
final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
-
//cleaned form fields
final origin = normalizeUrl(_originController.text.trim());
final username = _usernameController.text.trim();
- final password =
- ticket.isNotEmpty ? ticket : _passwordController.text.trim();
+ final String enteredPassword = _passwordController.text.trim();
+ final String? savedPassword = widget.password;
+
+ final password = ticket.isNotEmpty ? ticket : enteredPassword;
final realm = _selectedDomain?.realm ?? mRealm;
var client = await proxclient.authenticate(
status.singleWhereOrNull((element) => element.local ?? false)?.name;
var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
+ final savePW = enteredPassword != '' &&
+ _savePasswordCB &&
+ enteredPassword != savedPassword;
+ final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB;
+ String? id;
+
if (widget.isCreate!) {
final newLogin = ProxmoxLoginModel((b) => b
..origin = origin
..realm = realm
..productType = ProxmoxProductType.pve
..ticket = client.credentials.ticket
+ ..passwordSaved = savePW
..hostname = hostname);
loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin));
+ id = newLogin.identifier;
} else {
loginStorage = loginStorage!.rebuild((b) => b
..logins.rebuildWhere(
(m) => m == widget.userModel,
(b) => b
..ticket = client.credentials.ticket
+ ..passwordSaved =
+ savePW || (deletePW ? false : b.passwordSaved ?? false)
..hostname = hostname));
+ id = widget.userModel!.identifier;
+ }
+
+ if (id != null) {
+ if (savePW) {
+ await savePassword(id, enteredPassword);
+ } else if (deletePW) {
+ await deletePassword(id);
+ }
}
await loginStorage.saveToDisk();
..inProgress += 1
..message = 'Connection test...';
});
+
+ final canSavePW = await BiometricStorage().canAuthenticate();
+ setState(() {
+ _canSavePassword = canSavePW == CanAuthenticateResponse.success;
+ });
+
var host = _originController.text.trim();
var apiBaseUrl = normalizeUrl(host);
import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
as proxclient;
import 'package:proxmox_login_manager/extension.dart';
+import 'package:proxmox_login_manager/proxmox_password_store.dart';
typedef OnLoginCallback = Function(proxclient.ProxmoxApiClient client);
),
),
),
- ...logins.where((b) => !b.activeSession).map((l) => ListTile(
- title: Text(l.fullHostname),
- subtitle: Text(l.fullUsername),
+ ...logins.where((b) => !b.activeSession).map((login) =>
+ ListTile(
+ title: Text(login.fullHostname),
+ subtitle: Text(login.fullUsername),
trailing: Icon(Icons.navigate_next),
leading: PopupMenuButton(
itemBuilder: (context) => [
+ if (login.passwordSaved ?? false)
+ PopupMenuItem(
+ child: ListTile(
+ dense: true,
+ leading: Icon(Icons.key_off),
+ title: Text('Delete Password'),
+ onTap: () async {
+ await deletePassword(login.identifier!);
+
+ await snapshot.data!
+ .rebuild((b) => b
+ ..logins.rebuildWhere(
+ (m) => m == login,
+ (b) =>
+ b..passwordSaved = false))
+ .saveToDisk();
+ refreshFromStorage();
+ Navigator.of(context).pop();
+ },
+ ),
+ ),
PopupMenuItem(
child: ListTile(
dense: true,
leading: Icon(Icons.delete),
title: Text('Delete'),
onTap: () async {
+ await deletePassword(login.identifier!);
await snapshot.data!
- .rebuild((b) => b.logins.remove(l))
+ .rebuild(
+ (b) => b.logins.remove(login))
.saveToDisk();
refreshFromStorage();
Navigator.of(context).pop();
),
),
]),
- onTap: () => _login(user: l),
+ onTap: () => _login(user: login),
))
]);
return ListView(
}
Future<void> _login({ProxmoxLoginModel? user, bool isCreate = false}) async {
+ String? password;
+ String ticket = user?.ticket ?? '';
+ bool passwordSaved = user != null && (user.passwordSaved ?? false);
+
+ if (ticket == '' && passwordSaved) {
+ password = await getPassword(user.identifier!);
+ }
+
final client = await Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ProxmoxLoginPage(
userModel: user,
isCreate: isCreate,
ticket: user?.ticket,
+ password: password,
)));
refreshFromStorage();
if (client != null) {