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
60 changes: 45 additions & 15 deletions lib/core/database/mongodb_connection.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:mongo_dart/mongo_dart.dart';
import 'package:querya_desktop/core/storage/connection_secrets_store.dart';
import 'package:querya_desktop/features/connections/ssl_certificate_support.dart';

/// MongoDB connection configuration and state.
Expand All @@ -10,42 +11,54 @@ class MongoConnection {
required this.host,
this.port = 27017,
this.username,
this.password,
String? password,
this.database,
this.authSource,
this.useSSL = false,
this.replicaSet,
this.connectionString,
});
String? connectionString,
}) : _password = password,
_connectionString = connectionString;

final int id;
final String name;
final String host;
final int port;
final String? username;
final String? password;
String? _password;
final String? database;
final String? authSource;
final bool useSSL;
final String? replicaSet;
final String? connectionString;
String? _connectionString;

String? get password => _password;
String? get connectionString => _connectionString;

Db? _db;
bool _isConnected = false;

/// Scrubs sensitive in-memory credentials once the network handshake completes.
void scrubCredentials() {
_password = null;
_connectionString = null;
}

/// Builds MongoDB connection URI from configuration.
String buildConnectionUri() {
if (connectionString != null && connectionString!.isNotEmpty) {
return connectionString!;
String buildConnectionUri({String? pass, String? connStr}) {
final effectiveConnStr = connStr ?? _connectionString;
if (effectiveConnStr != null && effectiveConnStr.isNotEmpty) {
return effectiveConnStr;
}

final buffer = StringBuffer('mongodb://');

// Add authentication if provided
final effectivePass = pass ?? _password;
if (username != null && username!.isNotEmpty) {
buffer.write(Uri.encodeComponent(username!));
if (password != null && password!.isNotEmpty) {
buffer.write(':${Uri.encodeComponent(password!)}');
if (effectivePass != null && effectivePass.isNotEmpty) {
buffer.write(':${Uri.encodeComponent(effectivePass)}');
}
buffer.write('@');
}
Expand Down Expand Up @@ -86,8 +99,8 @@ class MongoConnection {
/// exists, the method automatically adds `authSource=<original_db>` (defaults
/// to `admin`) so that authentication succeeds on databases other than the
/// one the user was created in.
String buildUriForDatabase(String databaseName) {
final baseUri = buildConnectionUri();
String buildUriForDatabase(String databaseName, {String? pass, String? connStr}) {
final baseUri = buildConnectionUri(pass: pass, connStr: connStr);
final uri = Uri.parse(baseUri);

// Determine the authSource that should be used.
Expand Down Expand Up @@ -120,20 +133,37 @@ class MongoConnection {
return;
}

var effectivePassword = _password;
var effectiveConnectionString = _connectionString;

if ((effectivePassword == null || effectivePassword.isEmpty) &&
(effectiveConnectionString == null || effectiveConnectionString.isEmpty) &&
id > 0) {
try {
final secrets = await ConnectionSecretsStore.readForConnection(id);
effectivePassword = secrets.password;
effectiveConnectionString = secrets.connectionString;
} catch (_) {}
}

try {
final uri = await _effectiveMongoUri();
final uri = await _effectiveMongoUri(
pass: effectivePassword,
connStr: effectiveConnectionString,
);
_db = await Db.create(uri);
await _db!.open();
_isConnected = true;
scrubCredentials();
} catch (e) {
_isConnected = false;
_db = null;
rethrow;
}
}

Future<String> _effectiveMongoUri() async {
final base = buildConnectionUri();
Future<String> _effectiveMongoUri({String? pass, String? connStr}) async {
final base = buildConnectionUri(pass: pass, connStr: connStr);
final parsed = Uri.parse(base);
final paths = extractSslCertificatePaths(parsed);
final params = Map<String, String>.from(parsed.queryParameters);
Expand Down
55 changes: 39 additions & 16 deletions lib/core/database/mysql_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:mysql_client/mysql_client.dart';
import 'package:querya_desktop/core/database/table_schema_meta.dart';
import 'package:querya_desktop/core/storage/connection_secrets_store.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/features/connections/ssl_certificate_support.dart';

Expand Down Expand Up @@ -38,11 +39,12 @@ class MysqlConnection {
required this.host,
this.port = 3306,
this.username,
this.password,
String? password,
this.database,
this.useSSL = true,
this.connectionString,
});
String? connectionString,
}) : _password = password,
_connectionString = connectionString;

factory MysqlConnection.fromConnectionRow(
ConnectionRow row, {
Expand All @@ -66,20 +68,27 @@ class MysqlConnection {
final String host;
final int port;
final String? username;
final String? password;
String? _password;
final String? database;
final bool useSSL;
final String? connectionString;
String? _connectionString;

String? get password => _password;
String? get connectionString => _connectionString;

MySQLConnection? _conn;
bool _isConnected = false;

bool get isConnected => _isConnected && _conn != null;

bool get _usesConnectionString =>
connectionString != null && connectionString!.trim().isNotEmpty;

/// Scrubs sensitive in-memory credentials once the network handshake completes.
void scrubCredentials() {
_password = null;
_connectionString = null;
}

bool _usesConnectionString(String? connStr) =>
connStr != null && connStr.trim().isNotEmpty;

/// MySQL identifier quoting (backticks).
static String quoteIdentifier(String id) {
Expand All @@ -88,17 +97,31 @@ class MysqlConnection {

Future<void> connect({int connectTimeoutMs = 10000}) async {
if (_isConnected && _conn != null) return;

var effectivePassword = _password;
var effectiveConnectionString = _connectionString;

if ((effectivePassword == null || effectivePassword.isEmpty) &&
(effectiveConnectionString == null || effectiveConnectionString.isEmpty) &&
id > 0) {
try {
final secrets = await ConnectionSecretsStore.readForConnection(id);
effectivePassword = secrets.password;
effectiveConnectionString = secrets.connectionString;
} catch (_) {}
}

try {
final user = username ?? '';
final pass = password ?? '';
if (_usesConnectionString) {
final pass = effectivePassword ?? '';
if (_usesConnectionString(effectiveConnectionString)) {
final dbName = database;
final uriStr = dbName != null && dbName.isNotEmpty
? replaceDatabaseInMysqlConnectionString(
connectionString!.trim(),
effectiveConnectionString!.trim(),
dbName,
)
: connectionString!.trim();
: effectiveConnectionString!.trim();
final parsed = _parseMysqlUri(uriStr, fallbackSsl: useSSL);
final sslPaths = extractSslCertificatePathsFromString(uriStr);
final securityContext = buildSecurityContext(sslPaths);
Expand All @@ -113,21 +136,21 @@ class MysqlConnection {
);
await _conn!.connect(timeoutMs: connectTimeoutMs);
} else {
final securityContext = buildSecurityContext(
extractSslCertificatePathsFromString(connectionString),
);
final sslPaths = extractSslCertificatePathsFromString(effectiveConnectionString);
final securityContext = buildSecurityContext(sslPaths);
_conn = await MySQLConnection.createConnection(
host: host,
port: port,
userName: user,
password: pass,
secure: useSSL,
secure: useSSL || sslPaths.hasAny,
databaseName: database,
securityContext: securityContext,
);
await _conn!.connect(timeoutMs: connectTimeoutMs);
}
_isConnected = true;
scrubCredentials();
} catch (e) {
_isConnected = false;
_conn = null;
Expand Down
50 changes: 38 additions & 12 deletions lib/core/database/postgres_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:io' show SecurityContext;

import 'package:flutter/foundation.dart';
import 'package:postgres/postgres.dart';
import 'package:querya_desktop/core/storage/connection_secrets_store.dart';
import 'package:querya_desktop/core/storage/local_db.dart';

// ignore: implementation_imports
Expand Down Expand Up @@ -44,14 +45,15 @@ class PostgresConnection {
required this.host,
this.port = 5432,
this.username,
this.password,
String? password,
this.database,
this.useSSL = false,
this.connectionString,
String? connectionString,
this.sslRootCert,
this.sslCert,
this.sslKey,
});
}) : _password = password,
_connectionString = connectionString;

/// Builds a connection from a saved [ConnectionRow] (host/port or URI).
factory PostgresConnection.fromConnectionRow(
Expand Down Expand Up @@ -91,29 +93,38 @@ class PostgresConnection {
final String host;
final int port;
final String? username;
final String? password;
String? _password;
final String? database;
final bool useSSL;
final String? connectionString;
String? _connectionString;
final String? sslRootCert;
final String? sslCert;
final String? sslKey;

String? get password => _password;
String? get connectionString => _connectionString;

Connection? _conn;
bool _isConnected = false;

bool get isConnected => _isConnected && _conn != null;

bool get _usesConnectionString =>
connectionString != null && connectionString!.trim().isNotEmpty;
/// Scrubs sensitive in-memory credentials once the network handshake completes.
void scrubCredentials() {
_password = null;
_connectionString = null;
}

Endpoint _buildEndpoint() {
bool _usesConnectionString(String? connStr) =>
connStr != null && connStr.trim().isNotEmpty;

Endpoint _buildEndpoint({String? pass}) {
return Endpoint(
host: host,
port: port,
database: database ?? 'postgres',
username: username,
password: password,
password: pass ?? _password,
);
}

Expand Down Expand Up @@ -148,14 +159,28 @@ class PostgresConnection {
/// form checkbox still applies; otherwise libpq-style URLs drive TLS mode.
Future<void> connect() async {
if (_isConnected && _conn != null) return;

var effectivePassword = _password;
var effectiveConnectionString = _connectionString;

if ((effectivePassword == null || effectivePassword.isEmpty) &&
(effectiveConnectionString == null || effectiveConnectionString.isEmpty) &&
id > 0) {
try {
final secrets = await ConnectionSecretsStore.readForConnection(id);
effectivePassword = secrets.password;
effectiveConnectionString = secrets.connectionString;
} catch (_) {}
}

try {
if (_usesConnectionString) {
if (_usesConnectionString(effectiveConnectionString)) {
// Pool passes target catalog via [database]; URI alone would always open
// the DB embedded in the string — every tree branch then queried the
// same database (duplicate tables under finance / logistics, etc.).
final dbName = database ?? 'postgres';
final uriForOpen = replaceDatabaseInConnectionString(
connectionString!.trim(),
effectiveConnectionString!.trim(),
dbName,
);
final parsed = parseConnectionString(uriForOpen);
Expand All @@ -176,11 +201,12 @@ class PostgresConnection {
);
} else {
_conn = await Connection.open(
_buildEndpoint(),
_buildEndpoint(pass: effectivePassword),
settings: _buildSettings(),
);
}
_isConnected = true;
scrubCredentials();
} catch (e, st) {
_isConnected = false;
_conn = null;
Expand Down
Loading
Loading