Improve migration status detection for new databases

- Add intelligent migration requirement detection that checks for actual unencrypted data
- New databases without sensitive data no longer show false migration warnings
- Frontend now displays three states: completed, required, or not needed
- Fix TypeScript compilation errors in migration status checks
- Prevent unnecessary migration prompts for clean installations
This commit is contained in:
ZacharyZcR
2025-09-16 10:38:11 +08:00
parent a501df8dde
commit 0839cb4134
2 changed files with 77 additions and 6 deletions

View File

@@ -5,7 +5,7 @@ import { EncryptionKeyManager } from './encryption-key-manager.js';
import { databaseLogger } from './logger.js';
import { db } from '../database/db/index.js';
import { settings } from '../database/db/schema.js';
import { eq } from 'drizzle-orm';
import { eq, sql } from 'drizzle-orm';
interface MigrationConfig {
masterPassword?: string;
@@ -281,16 +281,24 @@ class EncryptionMigration {
static async checkMigrationStatus(): Promise<{
isEncryptionEnabled: boolean;
migrationCompleted: boolean;
migrationRequired: boolean;
migrationDate?: string;
}> {
try {
const encryptionEnabled = await db.select().from(settings).where(eq(settings.key, 'encryption_enabled'));
const migrationCompleted = await db.select().from(settings).where(eq(settings.key, 'encryption_migration_completed'));
const isEncryptionEnabled = encryptionEnabled.length > 0 && encryptionEnabled[0].value === 'true';
const isMigrationCompleted = migrationCompleted.length > 0;
// Check if migration is actually required by looking for unencrypted sensitive data
const migrationRequired = await this.checkIfMigrationRequired();
return {
isEncryptionEnabled: encryptionEnabled.length > 0 && encryptionEnabled[0].value === 'true',
migrationCompleted: migrationCompleted.length > 0,
migrationDate: migrationCompleted.length > 0 ? migrationCompleted[0].value : undefined
isEncryptionEnabled,
migrationCompleted: isMigrationCompleted,
migrationRequired,
migrationDate: isMigrationCompleted ? migrationCompleted[0].value : undefined
};
} catch (error) {
databaseLogger.error('Failed to check migration status', error, {
@@ -299,6 +307,67 @@ class EncryptionMigration {
throw error;
}
}
static async checkIfMigrationRequired(): Promise<boolean> {
try {
// Import table schemas
const { sshData, sshCredentials } = await import('../database/db/schema.js');
// Check if there's any unencrypted sensitive data in ssh_data
const sshDataCount = await db.select({ count: sql<number>`count(*)` }).from(sshData);
if (sshDataCount[0].count > 0) {
// Sample a few records to check if they contain unencrypted data
const sampleData = await db.select().from(sshData).limit(5);
for (const record of sampleData) {
if (record.password && !this.looksEncrypted(record.password)) {
return true; // Found unencrypted password
}
if (record.key && !this.looksEncrypted(record.key)) {
return true; // Found unencrypted key
}
}
}
// Check if there's any unencrypted sensitive data in ssh_credentials
const credentialsCount = await db.select({ count: sql<number>`count(*)` }).from(sshCredentials);
if (credentialsCount[0].count > 0) {
const sampleCredentials = await db.select().from(sshCredentials).limit(5);
for (const record of sampleCredentials) {
if (record.password && !this.looksEncrypted(record.password)) {
return true; // Found unencrypted password
}
if (record.privateKey && !this.looksEncrypted(record.privateKey)) {
return true; // Found unencrypted private key
}
if (record.keyPassword && !this.looksEncrypted(record.keyPassword)) {
return true; // Found unencrypted key password
}
}
}
return false; // No unencrypted sensitive data found
} catch (error) {
databaseLogger.warn('Failed to check if migration required, assuming required', {
operation: 'migration_check_failed',
error: error instanceof Error ? error.message : 'Unknown error'
});
return true; // If we can't check, assume migration is required for safety
}
}
private static looksEncrypted(data: string): boolean {
if (!data) return true; // Empty data doesn't need encryption
try {
// Check if it looks like our encrypted format: {"data":"...","iv":"...","tag":"..."}
const parsed = JSON.parse(data);
return !!(parsed.data && parsed.iv && parsed.tag);
} catch {
// If it's not JSON, check if it's a reasonable length for encrypted data
// Encrypted data is typically much longer than plaintext
return data.length > 100 && data.includes('='); // Base64-like characteristics
}
}
}
if (import.meta.url === `file://${process.argv[1]}`) {