Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions lib/core/storage/local_db.dart
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,8 @@ class LocalDb {
final previousRow = ConnectionRow.fromMap(previousMaps.first);
final previousSecrets =
await ConnectionSecretsStore.readForConnection(row.id!);
final previousSsh =
await ConnectionSecretsStore.readSshSecretsForConnection(row.id!);

await db.transaction((txn) async {
final count = await txn.update(
Expand Down Expand Up @@ -830,6 +832,10 @@ class LocalDb {
passphrase: row.sshSecrets!.passphrase,
jumpPassword: row.sshSecrets!.jumpPassword,
);
} else if (row.sshTunnelConfig?.enabled != true) {
// The tunnel was turned off: its password and key do not stay in the
// keyring (#1311).
await ConnectionSecretsStore.writeSshSecretsForConnection(row.id!);
}
} catch (e) {
await db.update(
Expand All @@ -844,6 +850,14 @@ class LocalDb {
password: previousSecrets.password,
connectionString: previousSecrets.connectionString,
);
// The SSH secrets may be half written too (four keys, one by one).
await ConnectionSecretsStore.writeSshSecretsForConnection(
row.id!,
password: previousSsh.password,
privateKey: previousSsh.privateKey,
passphrase: previousSsh.passphrase,
jumpPassword: previousSsh.jumpPassword,
);
} catch (_) {
// Best-effort restore of previous secrets; surface the original error.
}
Expand Down Expand Up @@ -1040,8 +1054,13 @@ class ConnectionRow {
this.sortOrder = 0,
required this.createdAt,
this.sshSecrets,
this.removeSavedPassword = false,
});

/// Set by an edit form (never stored): the saved password is to be removed,
/// not kept because the password field was left blank (#1311).
final bool removeSavedPassword;

final int? id;
final String type;
final String name;
Expand Down Expand Up @@ -1194,6 +1213,7 @@ class ConnectionRow {
int? sortOrder,
String? createdAt,
SshTunnelSecrets? sshSecrets,
bool? removeSavedPassword,
bool clearPassword = false,
bool clearConnectionString = false,
bool clearSshSecrets = false,
Expand Down Expand Up @@ -1221,6 +1241,7 @@ class ConnectionRow {
sortOrder: sortOrder ?? this.sortOrder,
createdAt: createdAt ?? this.createdAt,
sshSecrets: clearSshSecrets ? null : (sshSecrets ?? this.sshSecrets),
removeSavedPassword: removeSavedPassword ?? this.removeSavedPassword,
);
}

Expand Down
13 changes: 10 additions & 3 deletions lib/features/connections/connection_edit_secrets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Future<
required String? password,
required String? connectionString,
required SshTunnelSecrets? sshSecrets,
bool useSavedPassword = true,
}) async {
final id = connectionId;
if (id == null || id <= 0) {
Expand All @@ -58,8 +59,10 @@ Future<
);
}
final prev = await ConnectionSecretsStore.readForConnection(id);
final effectivePassword =
(password == null || password.isEmpty) ? prev.password : password;
// "Remove the saved password" is ticked: test without it (#1311).
final effectivePassword = (password == null || password.isEmpty)
? (useSavedPassword ? prev.password : null)
: password;
var uri = connectionString;
if (uri != null && uri.trim().isNotEmpty) {
uri = injectUriPasswordIfMissing(uri, effectivePassword);
Expand Down Expand Up @@ -91,7 +94,11 @@ Future<ConnectionRow> mergeSecretsForConnectionUpdate(

final passwordEmpty =
edited.password == null || edited.password!.trim().isEmpty;
final password = passwordEmpty ? prev.password : edited.password;
// A blank field keeps the saved password, unless the user asked to remove
// it (#1311). A password typed together with the request wins: it replaces.
final String? password = edited.removeSavedPassword && passwordEmpty
? null
: (passwordEmpty ? prev.password : edited.password);

var connectionString = edited.connectionString;
if (connectionString == null || connectionString.trim().isEmpty) {
Expand Down
38 changes: 38 additions & 0 deletions lib/features/connections/remove_saved_password_option.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/shared/widgets/widgets.dart';

/// "Remove the saved password" in an edit form (#1311).
///
/// A blank password field means *keep the saved one*, and the form never shows
/// it, so without this a saved password could not be taken away (a database
/// that moved to trust authentication, a password that must not stay on disk).
class RemoveSavedPasswordOption extends material.StatelessWidget {
const RemoveSavedPasswordOption({
super.key,
required this.value,
required this.onChanged,
});

final bool value;
final material.ValueChanged<bool> onChanged;

@override
material.Widget build(material.BuildContext context) {
return material.Padding(
padding: const material.EdgeInsets.only(top: 6),
child: material.Row(
children: [
material.Checkbox(
key: const material.ValueKey('remove_saved_password'),
value: value,
onChanged: (v) => onChanged(v ?? false),
),
const Gap(8),
material.Flexible(
child: const Text('Remove the saved password').small(),
),
],
),
);
}
}
11 changes: 11 additions & 0 deletions lib/features/mongodb/mongodb_connection_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:querya_desktop/core/database/mongodb_connection.dart';
import 'package:querya_desktop/core/layout/window_layout.dart';
import 'package:querya_desktop/core/security/ssh_tunnel_config.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/features/connections/remove_saved_password_option.dart';
import 'package:querya_desktop/features/connections/connection_creation_flow.dart';
import 'package:querya_desktop/features/connections/ssh_tunnel_section.dart';
import 'package:querya_desktop/features/connections/ssl_certificate_support.dart';
Expand Down Expand Up @@ -98,6 +99,7 @@ class _MongoConnectionFormContentState

bool _useConnectionString = false;
bool _useSSL = false;
bool _removeSavedPassword = false;
bool _showPassword = false;
bool _isTesting = false;
String? _testResult;
Expand Down Expand Up @@ -293,6 +295,7 @@ class _MongoConnectionFormContentState
password: data.password,
connectionString: data.connectionString,
sshSecrets: _sshConfig.enabled ? _sshSecrets : null,
useSavedPassword: !_removeSavedPassword,
);
final connection = MongoConnection(
id: 0,
Expand Down Expand Up @@ -354,6 +357,7 @@ class _MongoConnectionFormContentState
);
row = row.withSshTunnelConfig(_sshConfig.enabled ? _sshConfig : null);
row = row.withEnvironment(_environment);
row = row.copyWith(removeSavedPassword: _removeSavedPassword);

material.Navigator.of(context).pop(row);
}
Expand Down Expand Up @@ -513,10 +517,17 @@ class _MongoConnectionFormContentState
placeholder: const Text('Username'),
),
const Gap(12),
if (_isEditing)
RemoveSavedPasswordOption(
value: _removeSavedPassword,
onChanged: (v) =>
setState(() => _removeSavedPassword = v),
),
material.Stack(
children: [
TextField(
controller: _passwordController,
enabled: !_removeSavedPassword,
placeholder: Text(
_isEditing
? 'Leave blank to keep existing'
Expand Down
11 changes: 11 additions & 0 deletions lib/features/mysql/mysql_connection_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:querya_desktop/core/database/mysql_connection.dart';
import 'package:querya_desktop/core/layout/window_layout.dart';
import 'package:querya_desktop/core/security/ssh_tunnel_config.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/features/connections/remove_saved_password_option.dart';
import 'package:querya_desktop/features/connections/connection_creation_flow.dart';
import 'package:querya_desktop/features/connections/ssh_tunnel_section.dart';
import 'package:querya_desktop/features/connections/ssl_certificate_support.dart';
Expand Down Expand Up @@ -63,6 +64,7 @@ class _MysqlConnectionFormContentState
ConnectionEnvironment? _environment;
final SshTunnelSecrets _sshSecrets = SshTunnelSecrets();

bool _removeSavedPassword = false;
bool _useSSL = true;
bool _showPassword = false;
bool _isTesting = false;
Expand Down Expand Up @@ -220,6 +222,7 @@ class _MysqlConnectionFormContentState
_passwordController.text.isEmpty ? null : _passwordController.text,
connectionString: uri.isEmpty ? null : uri,
sshSecrets: _sshConfig.enabled ? _sshSecrets : null,
useSavedPassword: !_removeSavedPassword,
);
final conn = MysqlConnection(
id: 0,
Expand Down Expand Up @@ -287,6 +290,7 @@ class _MysqlConnectionFormContentState
);
row = row.withSshTunnelConfig(_sshConfig.enabled ? _sshConfig : null);
row = row.withEnvironment(_environment);
row = row.copyWith(removeSavedPassword: _removeSavedPassword);
material.Navigator.of(context).pop(row);
}

Expand Down Expand Up @@ -463,11 +467,18 @@ class _MysqlConnectionFormContentState
),
const Gap(16),
const Text('Password').small().semiBold(),
if (_isEditing)
RemoveSavedPasswordOption(
value: _removeSavedPassword,
onChanged: (v) =>
setState(() => _removeSavedPassword = v),
),
const Gap(8),
material.Stack(
children: [
TextField(
controller: _passwordController,
enabled: !_removeSavedPassword,
placeholder: Text(
_isEditing
? 'Leave blank to keep existing'
Expand Down
11 changes: 11 additions & 0 deletions lib/features/postgresql/postgresql_connection_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:querya_desktop/core/database/postgres_connection.dart';
import 'package:querya_desktop/core/layout/window_layout.dart';
import 'package:querya_desktop/core/security/ssh_tunnel_config.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/features/connections/remove_saved_password_option.dart';
import 'package:querya_desktop/features/connections/connection_creation_flow.dart';
import 'package:querya_desktop/features/connections/ssh_tunnel_section.dart';
import 'package:querya_desktop/shared/widgets/form_validity_notifier.dart';
Expand Down Expand Up @@ -63,6 +64,7 @@ class _PostgresConnectionFormContentState
final SshTunnelSecrets _sshSecrets = SshTunnelSecrets();

bool _useSSL = false;
bool _removeSavedPassword = false;
bool _showPassword = false;
bool _isTesting = false;
String? _testResult;
Expand Down Expand Up @@ -281,6 +283,7 @@ class _PostgresConnectionFormContentState
_passwordController.text.isEmpty ? null : _passwordController.text,
connectionString: hasUri ? uri : null,
sshSecrets: _sshConfig.enabled ? _sshSecrets : null,
useSavedPassword: !_removeSavedPassword,
);
final conn = PostgresConnection(
id: 0,
Expand Down Expand Up @@ -376,6 +379,7 @@ class _PostgresConnectionFormContentState
);
row = row.withSshTunnelConfig(_sshConfig.enabled ? _sshConfig : null);
row = row.withEnvironment(_environment);
row = row.copyWith(removeSavedPassword: _removeSavedPassword);
material.Navigator.of(context).pop(row);
}

Expand Down Expand Up @@ -603,11 +607,18 @@ class _PostgresConnectionFormContentState
material.CrossAxisAlignment.stretch,
children: [
const Text('Password').small().semiBold(),
if (_isEditing)
RemoveSavedPasswordOption(
value: _removeSavedPassword,
onChanged: (v) => setState(
() => _removeSavedPassword = v),
),
const Gap(8),
material.Stack(
children: [
TextField(
controller: _passwordController,
enabled: !_removeSavedPassword,
placeholder: Text(
_isEditing
? 'Leave blank to keep existing'
Expand Down
15 changes: 14 additions & 1 deletion lib/features/redis/redis_connection_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:querya_desktop/core/database/redis_connection.dart';
import 'package:querya_desktop/core/layout/window_layout.dart';
import 'package:querya_desktop/core/security/ssh_tunnel_config.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/features/connections/remove_saved_password_option.dart';
import 'package:querya_desktop/features/connections/connection_creation_flow.dart';
import 'package:querya_desktop/features/connections/ssh_tunnel_section.dart';
import 'package:querya_desktop/features/connections/ssl_certificate_support.dart';
Expand Down Expand Up @@ -61,6 +62,7 @@ class _RedisConnectionFormContentState
ConnectionEnvironment? _environment;
final SshTunnelSecrets _sshSecrets = SshTunnelSecrets();

bool _removeSavedPassword = false;
bool _useSSL = false;
bool _showPassword = false;
bool _isTesting = false;
Expand Down Expand Up @@ -204,6 +206,7 @@ class _RedisConnectionFormContentState
password: draft.password,
connectionString: draft.connectionString,
sshSecrets: draft.sshSecrets,
useSavedPassword: !_removeSavedPassword,
);
final conn = RedisConnection.fromConnectionRow(draft.copyWith(
password: secrets.password,
Expand Down Expand Up @@ -259,7 +262,10 @@ class _RedisConnectionFormContentState

void _save() {
if (!_formValidNotifier.value) return;
material.Navigator.of(context).pop(_draftRow(id: widget.initial?.id));
material.Navigator.of(context).pop(
_draftRow(id: widget.initial?.id)
.copyWith(removeSavedPassword: _removeSavedPassword),
);
}

@override
Expand Down Expand Up @@ -405,11 +411,18 @@ class _RedisConnectionFormContentState
),
const Gap(16),
const Text('Password (optional)').small().semiBold(),
if (_isEditing)
RemoveSavedPasswordOption(
value: _removeSavedPassword,
onChanged: (v) =>
setState(() => _removeSavedPassword = v),
),
const Gap(8),
material.Stack(
children: [
TextField(
controller: _passwordController,
enabled: !_removeSavedPassword,
placeholder: Text(
_isEditing
? 'Leave blank to keep existing'
Expand Down
Loading
Loading