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:
@@ -5,7 +5,7 @@ import { EncryptionKeyManager } from './encryption-key-manager.js';
|
|||||||
import { databaseLogger } from './logger.js';
|
import { databaseLogger } from './logger.js';
|
||||||
import { db } from '../database/db/index.js';
|
import { db } from '../database/db/index.js';
|
||||||
import { settings } from '../database/db/schema.js';
|
import { settings } from '../database/db/schema.js';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq, sql } from 'drizzle-orm';
|
||||||
|
|
||||||
interface MigrationConfig {
|
interface MigrationConfig {
|
||||||
masterPassword?: string;
|
masterPassword?: string;
|
||||||
@@ -281,16 +281,24 @@ class EncryptionMigration {
|
|||||||
static async checkMigrationStatus(): Promise<{
|
static async checkMigrationStatus(): Promise<{
|
||||||
isEncryptionEnabled: boolean;
|
isEncryptionEnabled: boolean;
|
||||||
migrationCompleted: boolean;
|
migrationCompleted: boolean;
|
||||||
|
migrationRequired: boolean;
|
||||||
migrationDate?: string;
|
migrationDate?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const encryptionEnabled = await db.select().from(settings).where(eq(settings.key, 'encryption_enabled'));
|
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 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 {
|
return {
|
||||||
isEncryptionEnabled: encryptionEnabled.length > 0 && encryptionEnabled[0].value === 'true',
|
isEncryptionEnabled,
|
||||||
migrationCompleted: migrationCompleted.length > 0,
|
migrationCompleted: isMigrationCompleted,
|
||||||
migrationDate: migrationCompleted.length > 0 ? migrationCompleted[0].value : undefined
|
migrationRequired,
|
||||||
|
migrationDate: isMigrationCompleted ? migrationCompleted[0].value : undefined
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
databaseLogger.error('Failed to check migration status', error, {
|
databaseLogger.error('Failed to check migration status', error, {
|
||||||
@@ -299,6 +307,67 @@ class EncryptionMigration {
|
|||||||
throw error;
|
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]}`) {
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
|||||||
@@ -876,8 +876,10 @@ export function AdminSettings({
|
|||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
{encryptionStatus.migration?.migrationCompleted ? (
|
{encryptionStatus.migration?.migrationCompleted ? (
|
||||||
<span className="text-green-600">✓ {t("admin.migrationCompleted")}</span>
|
<span className="text-green-600">✓ {t("admin.migrationCompleted")}</span>
|
||||||
) : (
|
) : encryptionStatus.migration?.migrationRequired ? (
|
||||||
<span className="text-yellow-600">⚠ {t("admin.migrationRequired")}</span>
|
<span className="text-yellow-600">⚠ {t("admin.migrationRequired")}</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-600">— No migration needed</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -913,7 +915,7 @@ export function AdminSettings({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{!encryptionStatus.migration?.migrationCompleted && (
|
{encryptionStatus.migration?.migrationRequired && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="font-medium">{t("admin.migrateExistingData")}</h4>
|
<h4 className="font-medium">{t("admin.migrateExistingData")}</h4>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|||||||
Reference in New Issue
Block a user