filepilot-enterprise-vault 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -0
- package/admin/enterprise.png +0 -0
- package/admin/index.html +5297 -0
- package/admin/install.html +511 -0
- package/bin/cli.js +247 -0
- package/compliance-checks.cjs +221 -0
- package/db.cjs +999 -0
- package/kms-providers.cjs +329 -0
- package/package.json +59 -0
- package/server.cjs +4214 -0
package/db.cjs
ADDED
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
const { Sequelize, DataTypes } = require('sequelize');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const DATA_DIR = process.env.VAULT_DATA_DIR || process.cwd();
|
|
6
|
+
|
|
7
|
+
// Load environment variables from .env if present in DATA_DIR
|
|
8
|
+
const envPath = path.join(DATA_DIR, '.env');
|
|
9
|
+
console.log(`[DB] DATA_DIR resolved to: ${DATA_DIR}`);
|
|
10
|
+
console.log(`[DB] Checking for .env file at: ${envPath} (Exists: ${fs.existsSync(envPath)})`);
|
|
11
|
+
if (fs.existsSync(envPath)) {
|
|
12
|
+
try {
|
|
13
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
14
|
+
content.split(/\r?\n/).forEach(line => {
|
|
15
|
+
const parts = line.split('=');
|
|
16
|
+
if (parts.length >= 2) {
|
|
17
|
+
const key = parts[0].trim();
|
|
18
|
+
const val = parts.slice(1).join('=').trim().replace(/^['"]|['"]$/g, '');
|
|
19
|
+
if (key && !key.startsWith('#')) {
|
|
20
|
+
console.log(`[DB] Setting process.env.${key} from .env file`);
|
|
21
|
+
if (!process.env[key]) {
|
|
22
|
+
process.env[key] = val;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error("Failed to load .env file:", err);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const configPath = process.env.VAULT_CONFIG_PATH || path.join(DATA_DIR, 'config.json');
|
|
33
|
+
|
|
34
|
+
// Default database configuration (embedded SQLite fallback on first boot)
|
|
35
|
+
let dbConfig = {
|
|
36
|
+
dialect: 'sqlite',
|
|
37
|
+
storage: path.join(DATA_DIR, 'vault.db'),
|
|
38
|
+
logging: false
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (process.env.DB_DIALECT) {
|
|
42
|
+
const dialect = process.env.DB_DIALECT;
|
|
43
|
+
if (dialect === 'postgres') {
|
|
44
|
+
dbConfig = {
|
|
45
|
+
dialect: 'postgres',
|
|
46
|
+
host: process.env.DB_HOST || 'localhost',
|
|
47
|
+
port: parseInt(process.env.DB_PORT) || 5432,
|
|
48
|
+
username: process.env.DB_USERNAME,
|
|
49
|
+
password: process.env.DB_PASSWORD,
|
|
50
|
+
database: process.env.DB_NAME,
|
|
51
|
+
logging: false,
|
|
52
|
+
dialectOptions: (process.env.DB_SSL === 'true') ? {
|
|
53
|
+
ssl: {
|
|
54
|
+
require: true,
|
|
55
|
+
rejectUnauthorized: false
|
|
56
|
+
}
|
|
57
|
+
} : {}
|
|
58
|
+
};
|
|
59
|
+
} else if (dialect === 'mysql') {
|
|
60
|
+
dbConfig = {
|
|
61
|
+
dialect: 'mysql',
|
|
62
|
+
host: process.env.DB_HOST || 'localhost',
|
|
63
|
+
port: parseInt(process.env.DB_PORT) || 3306,
|
|
64
|
+
username: process.env.DB_USERNAME,
|
|
65
|
+
password: process.env.DB_PASSWORD,
|
|
66
|
+
database: process.env.DB_NAME,
|
|
67
|
+
logging: false
|
|
68
|
+
};
|
|
69
|
+
} else {
|
|
70
|
+
dbConfig = {
|
|
71
|
+
dialect: 'sqlite',
|
|
72
|
+
storage: process.env.DB_STORAGE || path.join(DATA_DIR, 'vault.db'),
|
|
73
|
+
logging: false
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
} else if (fs.existsSync(configPath)) {
|
|
77
|
+
try {
|
|
78
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
79
|
+
const parsed = JSON.parse(raw);
|
|
80
|
+
if (parsed.database) {
|
|
81
|
+
if (parsed.database.dialect === 'postgres') {
|
|
82
|
+
dbConfig = {
|
|
83
|
+
dialect: 'postgres',
|
|
84
|
+
host: parsed.database.host || 'localhost',
|
|
85
|
+
port: parseInt(parsed.database.port) || 5432,
|
|
86
|
+
username: parsed.database.username,
|
|
87
|
+
password: parsed.database.password,
|
|
88
|
+
database: parsed.database.database,
|
|
89
|
+
logging: false,
|
|
90
|
+
dialectOptions: parsed.database.ssl ? {
|
|
91
|
+
ssl: {
|
|
92
|
+
require: true,
|
|
93
|
+
rejectUnauthorized: false
|
|
94
|
+
}
|
|
95
|
+
} : {}
|
|
96
|
+
};
|
|
97
|
+
} else if (parsed.database.dialect === 'mysql') {
|
|
98
|
+
dbConfig = {
|
|
99
|
+
dialect: 'mysql',
|
|
100
|
+
host: parsed.database.host || 'localhost',
|
|
101
|
+
port: parseInt(parsed.database.port) || 3306,
|
|
102
|
+
username: parsed.database.username,
|
|
103
|
+
password: parsed.database.password,
|
|
104
|
+
database: parsed.database.database,
|
|
105
|
+
logging: false
|
|
106
|
+
};
|
|
107
|
+
} else {
|
|
108
|
+
dbConfig = {
|
|
109
|
+
dialect: 'sqlite',
|
|
110
|
+
storage: parsed.database.storage || path.join(DATA_DIR, 'vault.db'),
|
|
111
|
+
logging: false
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error('Error loading database configuration:', err);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const sequelize = new Sequelize(dbConfig);
|
|
121
|
+
|
|
122
|
+
// Define Models:
|
|
123
|
+
// 1. User
|
|
124
|
+
const User = sequelize.define('User', {
|
|
125
|
+
id: {
|
|
126
|
+
type: DataTypes.UUID,
|
|
127
|
+
defaultValue: DataTypes.UUIDV4,
|
|
128
|
+
primaryKey: true
|
|
129
|
+
},
|
|
130
|
+
username: {
|
|
131
|
+
type: DataTypes.STRING,
|
|
132
|
+
allowNull: false,
|
|
133
|
+
unique: true
|
|
134
|
+
},
|
|
135
|
+
passwordHash: {
|
|
136
|
+
type: DataTypes.STRING,
|
|
137
|
+
allowNull: false
|
|
138
|
+
},
|
|
139
|
+
role: {
|
|
140
|
+
type: DataTypes.STRING, // 'admin' | 'manager' | 'operator' | 'auditor'
|
|
141
|
+
allowNull: false,
|
|
142
|
+
defaultValue: 'operator'
|
|
143
|
+
},
|
|
144
|
+
mfa_enabled: {
|
|
145
|
+
type: DataTypes.BOOLEAN,
|
|
146
|
+
allowNull: false,
|
|
147
|
+
defaultValue: false
|
|
148
|
+
},
|
|
149
|
+
mfa_secret_encrypted: {
|
|
150
|
+
type: DataTypes.TEXT,
|
|
151
|
+
allowNull: true
|
|
152
|
+
},
|
|
153
|
+
mfa_backup_codes_hash: {
|
|
154
|
+
type: DataTypes.TEXT,
|
|
155
|
+
allowNull: true
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// 2. VaultGroup
|
|
160
|
+
const VaultGroup = sequelize.define('VaultGroup', {
|
|
161
|
+
id: {
|
|
162
|
+
type: DataTypes.STRING,
|
|
163
|
+
primaryKey: true
|
|
164
|
+
},
|
|
165
|
+
name: {
|
|
166
|
+
type: DataTypes.STRING,
|
|
167
|
+
allowNull: false
|
|
168
|
+
},
|
|
169
|
+
ipAllowlist: {
|
|
170
|
+
type: DataTypes.TEXT,
|
|
171
|
+
allowNull: true,
|
|
172
|
+
defaultValue: ''
|
|
173
|
+
},
|
|
174
|
+
wrapped_dek: {
|
|
175
|
+
type: DataTypes.TEXT,
|
|
176
|
+
allowNull: true
|
|
177
|
+
},
|
|
178
|
+
dek_version: {
|
|
179
|
+
type: DataTypes.INTEGER,
|
|
180
|
+
defaultValue: 1
|
|
181
|
+
},
|
|
182
|
+
migrated: {
|
|
183
|
+
type: DataTypes.BOOLEAN,
|
|
184
|
+
defaultValue: false
|
|
185
|
+
},
|
|
186
|
+
kms_provider: {
|
|
187
|
+
type: DataTypes.STRING,
|
|
188
|
+
allowNull: false,
|
|
189
|
+
defaultValue: 'local'
|
|
190
|
+
},
|
|
191
|
+
kms_config: {
|
|
192
|
+
type: DataTypes.TEXT,
|
|
193
|
+
allowNull: true
|
|
194
|
+
},
|
|
195
|
+
kms_credentials_encrypted: {
|
|
196
|
+
type: DataTypes.TEXT,
|
|
197
|
+
allowNull: true
|
|
198
|
+
},
|
|
199
|
+
legal_hold_active: {
|
|
200
|
+
type: DataTypes.BOOLEAN,
|
|
201
|
+
defaultValue: false
|
|
202
|
+
},
|
|
203
|
+
legal_hold_reason: {
|
|
204
|
+
type: DataTypes.TEXT,
|
|
205
|
+
allowNull: true
|
|
206
|
+
},
|
|
207
|
+
legal_hold_placed_by: {
|
|
208
|
+
type: DataTypes.STRING,
|
|
209
|
+
allowNull: true
|
|
210
|
+
},
|
|
211
|
+
legal_hold_placed_at: {
|
|
212
|
+
type: DataTypes.DATE,
|
|
213
|
+
allowNull: true
|
|
214
|
+
},
|
|
215
|
+
legal_hold_freeze_writes: {
|
|
216
|
+
type: DataTypes.BOOLEAN,
|
|
217
|
+
defaultValue: false
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// 3. ConnectionProfile
|
|
222
|
+
const ConnectionProfile = sequelize.define('ConnectionProfile', {
|
|
223
|
+
id: {
|
|
224
|
+
type: DataTypes.STRING,
|
|
225
|
+
primaryKey: true
|
|
226
|
+
},
|
|
227
|
+
groupId: {
|
|
228
|
+
type: DataTypes.STRING,
|
|
229
|
+
allowNull: false
|
|
230
|
+
},
|
|
231
|
+
name: {
|
|
232
|
+
type: DataTypes.STRING,
|
|
233
|
+
allowNull: false
|
|
234
|
+
},
|
|
235
|
+
protocol: {
|
|
236
|
+
type: DataTypes.STRING,
|
|
237
|
+
allowNull: false
|
|
238
|
+
},
|
|
239
|
+
host: {
|
|
240
|
+
type: DataTypes.STRING,
|
|
241
|
+
allowNull: false
|
|
242
|
+
},
|
|
243
|
+
port: {
|
|
244
|
+
type: DataTypes.INTEGER,
|
|
245
|
+
allowNull: false
|
|
246
|
+
},
|
|
247
|
+
username: {
|
|
248
|
+
type: DataTypes.STRING,
|
|
249
|
+
allowNull: false
|
|
250
|
+
},
|
|
251
|
+
passwordEncrypted: {
|
|
252
|
+
type: DataTypes.TEXT,
|
|
253
|
+
allowNull: true
|
|
254
|
+
},
|
|
255
|
+
authMode: {
|
|
256
|
+
type: DataTypes.STRING,
|
|
257
|
+
allowNull: false,
|
|
258
|
+
defaultValue: 'password'
|
|
259
|
+
},
|
|
260
|
+
clientProfileId: {
|
|
261
|
+
type: DataTypes.STRING,
|
|
262
|
+
allowNull: true
|
|
263
|
+
},
|
|
264
|
+
jumpHost: {
|
|
265
|
+
type: DataTypes.STRING,
|
|
266
|
+
allowNull: true
|
|
267
|
+
},
|
|
268
|
+
jumpPort: {
|
|
269
|
+
type: DataTypes.INTEGER,
|
|
270
|
+
allowNull: true
|
|
271
|
+
},
|
|
272
|
+
jumpUsername: {
|
|
273
|
+
type: DataTypes.STRING,
|
|
274
|
+
allowNull: true
|
|
275
|
+
},
|
|
276
|
+
jumpPasswordEncrypted: {
|
|
277
|
+
type: DataTypes.TEXT,
|
|
278
|
+
allowNull: true
|
|
279
|
+
},
|
|
280
|
+
jumpAuthMode: {
|
|
281
|
+
type: DataTypes.STRING,
|
|
282
|
+
allowNull: true,
|
|
283
|
+
defaultValue: 'password'
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// 4. AccessKey
|
|
288
|
+
const AccessKey = sequelize.define('AccessKey', {
|
|
289
|
+
token_hash: {
|
|
290
|
+
type: DataTypes.STRING,
|
|
291
|
+
primaryKey: true
|
|
292
|
+
},
|
|
293
|
+
userId: {
|
|
294
|
+
type: DataTypes.UUID,
|
|
295
|
+
allowNull: true
|
|
296
|
+
},
|
|
297
|
+
groupId: {
|
|
298
|
+
type: DataTypes.STRING,
|
|
299
|
+
allowNull: false
|
|
300
|
+
},
|
|
301
|
+
status: {
|
|
302
|
+
type: DataTypes.STRING,
|
|
303
|
+
allowNull: false,
|
|
304
|
+
defaultValue: 'active'
|
|
305
|
+
},
|
|
306
|
+
expiresAt: {
|
|
307
|
+
type: DataTypes.STRING,
|
|
308
|
+
allowNull: true
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Associations
|
|
313
|
+
User.hasMany(AccessKey, { foreignKey: 'userId', onDelete: 'SET NULL' });
|
|
314
|
+
AccessKey.belongsTo(User, { foreignKey: 'userId' });
|
|
315
|
+
|
|
316
|
+
VaultGroup.hasMany(ConnectionProfile, { foreignKey: 'groupId', onDelete: 'CASCADE' });
|
|
317
|
+
ConnectionProfile.belongsTo(VaultGroup, { foreignKey: 'groupId' });
|
|
318
|
+
|
|
319
|
+
VaultGroup.hasMany(AccessKey, { foreignKey: 'groupId', onDelete: 'CASCADE' });
|
|
320
|
+
AccessKey.belongsTo(VaultGroup, { foreignKey: 'groupId' });
|
|
321
|
+
|
|
322
|
+
// 5. TransferLog
|
|
323
|
+
const TransferLog = sequelize.define('TransferLog', {
|
|
324
|
+
id: {
|
|
325
|
+
type: DataTypes.INTEGER,
|
|
326
|
+
primaryKey: true,
|
|
327
|
+
autoIncrement: true
|
|
328
|
+
},
|
|
329
|
+
token: {
|
|
330
|
+
type: DataTypes.STRING,
|
|
331
|
+
allowNull: true
|
|
332
|
+
},
|
|
333
|
+
username: {
|
|
334
|
+
type: DataTypes.STRING,
|
|
335
|
+
allowNull: true
|
|
336
|
+
},
|
|
337
|
+
connectionId: {
|
|
338
|
+
type: DataTypes.STRING,
|
|
339
|
+
allowNull: false
|
|
340
|
+
},
|
|
341
|
+
filePath: {
|
|
342
|
+
type: DataTypes.TEXT,
|
|
343
|
+
allowNull: false
|
|
344
|
+
},
|
|
345
|
+
fileSize: {
|
|
346
|
+
type: DataTypes.BIGINT,
|
|
347
|
+
allowNull: true,
|
|
348
|
+
defaultValue: 0
|
|
349
|
+
},
|
|
350
|
+
action: {
|
|
351
|
+
type: DataTypes.STRING, // 'upload' | 'download' | 'delete'
|
|
352
|
+
allowNull: false
|
|
353
|
+
},
|
|
354
|
+
status: {
|
|
355
|
+
type: DataTypes.STRING, // 'success' | 'failed'
|
|
356
|
+
allowNull: false,
|
|
357
|
+
defaultValue: 'success'
|
|
358
|
+
},
|
|
359
|
+
errorMessage: {
|
|
360
|
+
type: DataTypes.TEXT,
|
|
361
|
+
allowNull: true
|
|
362
|
+
},
|
|
363
|
+
archived: {
|
|
364
|
+
type: DataTypes.BOOLEAN,
|
|
365
|
+
allowNull: false,
|
|
366
|
+
defaultValue: false
|
|
367
|
+
},
|
|
368
|
+
entry_hash: {
|
|
369
|
+
type: DataTypes.STRING(64),
|
|
370
|
+
allowNull: true
|
|
371
|
+
},
|
|
372
|
+
prev_hash: {
|
|
373
|
+
type: DataTypes.STRING(64),
|
|
374
|
+
allowNull: true
|
|
375
|
+
},
|
|
376
|
+
metadata: {
|
|
377
|
+
type: DataTypes.TEXT,
|
|
378
|
+
allowNull: true
|
|
379
|
+
},
|
|
380
|
+
hash_version: {
|
|
381
|
+
type: DataTypes.INTEGER,
|
|
382
|
+
allowNull: true
|
|
383
|
+
}
|
|
384
|
+
}, {
|
|
385
|
+
hooks: {
|
|
386
|
+
beforeCreate: async (log, options) => {
|
|
387
|
+
const crypto = require('crypto');
|
|
388
|
+
const prevLog = await TransferLog.findOne({
|
|
389
|
+
order: [['id', 'DESC']]
|
|
390
|
+
});
|
|
391
|
+
const prevHash = prevLog ? (prevLog.entry_hash || '0') : '0';
|
|
392
|
+
log.prev_hash = prevHash;
|
|
393
|
+
log.hash_version = 2; // New entries are version 2
|
|
394
|
+
|
|
395
|
+
const logFields = {
|
|
396
|
+
token: log.token || null,
|
|
397
|
+
username: log.username || null,
|
|
398
|
+
connectionId: log.connectionId,
|
|
399
|
+
filePath: log.filePath,
|
|
400
|
+
fileSize: log.fileSize ? parseInt(log.fileSize) : 0,
|
|
401
|
+
action: log.action,
|
|
402
|
+
status: log.status || 'success',
|
|
403
|
+
errorMessage: log.errorMessage || null,
|
|
404
|
+
metadata: log.metadata || null,
|
|
405
|
+
prev_hash: prevHash
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const hashInput = JSON.stringify(logFields);
|
|
409
|
+
log.entry_hash = crypto.createHash('sha256').update(hashInput).digest('hex');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// 6. FileVersion
|
|
415
|
+
const FileVersion = sequelize.define('FileVersion', {
|
|
416
|
+
id: {
|
|
417
|
+
type: DataTypes.INTEGER,
|
|
418
|
+
primaryKey: true,
|
|
419
|
+
autoIncrement: true
|
|
420
|
+
},
|
|
421
|
+
connectionId: {
|
|
422
|
+
type: DataTypes.STRING,
|
|
423
|
+
allowNull: false
|
|
424
|
+
},
|
|
425
|
+
filePath: {
|
|
426
|
+
type: DataTypes.TEXT,
|
|
427
|
+
allowNull: false
|
|
428
|
+
},
|
|
429
|
+
version: {
|
|
430
|
+
type: DataTypes.INTEGER,
|
|
431
|
+
allowNull: false
|
|
432
|
+
},
|
|
433
|
+
size: {
|
|
434
|
+
type: DataTypes.BIGINT,
|
|
435
|
+
allowNull: false
|
|
436
|
+
},
|
|
437
|
+
hash: {
|
|
438
|
+
type: DataTypes.STRING,
|
|
439
|
+
allowNull: true
|
|
440
|
+
},
|
|
441
|
+
backupPath: {
|
|
442
|
+
type: DataTypes.TEXT,
|
|
443
|
+
allowNull: true
|
|
444
|
+
},
|
|
445
|
+
modifiedBy: {
|
|
446
|
+
type: DataTypes.STRING, // username or user_id
|
|
447
|
+
allowNull: true
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// 7. SystemConfig
|
|
452
|
+
const SystemConfig = sequelize.define('SystemConfig', {
|
|
453
|
+
key: {
|
|
454
|
+
type: DataTypes.STRING,
|
|
455
|
+
primaryKey: true
|
|
456
|
+
},
|
|
457
|
+
value: {
|
|
458
|
+
type: DataTypes.TEXT,
|
|
459
|
+
allowNull: true
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// 8. PendingReversion
|
|
464
|
+
const PendingReversion = sequelize.define('PendingReversion', {
|
|
465
|
+
id: {
|
|
466
|
+
type: DataTypes.INTEGER,
|
|
467
|
+
autoIncrement: true,
|
|
468
|
+
primaryKey: true
|
|
469
|
+
},
|
|
470
|
+
groupId: {
|
|
471
|
+
type: DataTypes.STRING,
|
|
472
|
+
allowNull: false
|
|
473
|
+
},
|
|
474
|
+
connectionId: {
|
|
475
|
+
type: DataTypes.STRING,
|
|
476
|
+
allowNull: false
|
|
477
|
+
},
|
|
478
|
+
filePath: {
|
|
479
|
+
type: DataTypes.STRING,
|
|
480
|
+
allowNull: false
|
|
481
|
+
},
|
|
482
|
+
content: {
|
|
483
|
+
type: DataTypes.TEXT,
|
|
484
|
+
allowNull: false
|
|
485
|
+
},
|
|
486
|
+
status: {
|
|
487
|
+
type: DataTypes.STRING,
|
|
488
|
+
defaultValue: 'pending'
|
|
489
|
+
},
|
|
490
|
+
tokenHash: {
|
|
491
|
+
type: DataTypes.STRING,
|
|
492
|
+
allowNull: true
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// 9. AdminSession
|
|
497
|
+
const AdminSession = sequelize.define('AdminSession', {
|
|
498
|
+
id: {
|
|
499
|
+
type: DataTypes.UUID,
|
|
500
|
+
defaultValue: DataTypes.UUIDV4,
|
|
501
|
+
primaryKey: true
|
|
502
|
+
},
|
|
503
|
+
session_hash: {
|
|
504
|
+
type: DataTypes.STRING(64),
|
|
505
|
+
unique: true,
|
|
506
|
+
allowNull: false
|
|
507
|
+
},
|
|
508
|
+
userId: {
|
|
509
|
+
type: DataTypes.UUID,
|
|
510
|
+
allowNull: false,
|
|
511
|
+
references: {
|
|
512
|
+
model: 'Users',
|
|
513
|
+
key: 'id'
|
|
514
|
+
},
|
|
515
|
+
onDelete: 'CASCADE'
|
|
516
|
+
},
|
|
517
|
+
ipAddress: {
|
|
518
|
+
type: DataTypes.STRING(45),
|
|
519
|
+
allowNull: false
|
|
520
|
+
},
|
|
521
|
+
userAgent: {
|
|
522
|
+
type: DataTypes.TEXT,
|
|
523
|
+
allowNull: true
|
|
524
|
+
},
|
|
525
|
+
csrfToken: {
|
|
526
|
+
type: DataTypes.STRING(32),
|
|
527
|
+
allowNull: false
|
|
528
|
+
},
|
|
529
|
+
expiresAt: {
|
|
530
|
+
type: DataTypes.DATE,
|
|
531
|
+
allowNull: false
|
|
532
|
+
},
|
|
533
|
+
lastActivityAt: {
|
|
534
|
+
type: DataTypes.DATE,
|
|
535
|
+
allowNull: false,
|
|
536
|
+
defaultValue: DataTypes.NOW
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// 9.5. MfaChallenge
|
|
541
|
+
const MfaChallenge = sequelize.define('MfaChallenge', {
|
|
542
|
+
id: {
|
|
543
|
+
type: DataTypes.STRING,
|
|
544
|
+
primaryKey: true
|
|
545
|
+
},
|
|
546
|
+
userId: {
|
|
547
|
+
type: DataTypes.UUID,
|
|
548
|
+
allowNull: false,
|
|
549
|
+
references: {
|
|
550
|
+
model: 'Users',
|
|
551
|
+
key: 'id'
|
|
552
|
+
},
|
|
553
|
+
onDelete: 'CASCADE'
|
|
554
|
+
},
|
|
555
|
+
createdAt: {
|
|
556
|
+
type: DataTypes.DATE,
|
|
557
|
+
allowNull: false,
|
|
558
|
+
defaultValue: DataTypes.NOW
|
|
559
|
+
},
|
|
560
|
+
expiresAt: {
|
|
561
|
+
type: DataTypes.DATE,
|
|
562
|
+
allowNull: false
|
|
563
|
+
},
|
|
564
|
+
consumed: {
|
|
565
|
+
type: DataTypes.BOOLEAN,
|
|
566
|
+
allowNull: false,
|
|
567
|
+
defaultValue: false
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Associations
|
|
572
|
+
User.hasMany(AdminSession, { foreignKey: 'userId', onDelete: 'CASCADE' });
|
|
573
|
+
AdminSession.belongsTo(User, { foreignKey: 'userId' });
|
|
574
|
+
|
|
575
|
+
User.hasMany(MfaChallenge, { foreignKey: 'userId', onDelete: 'CASCADE' });
|
|
576
|
+
MfaChallenge.belongsTo(User, { foreignKey: 'userId' });
|
|
577
|
+
|
|
578
|
+
// 10. KeyVersion
|
|
579
|
+
const KeyVersion = sequelize.define('KeyVersion', {
|
|
580
|
+
version: {
|
|
581
|
+
type: DataTypes.INTEGER,
|
|
582
|
+
primaryKey: true
|
|
583
|
+
},
|
|
584
|
+
createdAt: {
|
|
585
|
+
type: DataTypes.DATE,
|
|
586
|
+
allowNull: false,
|
|
587
|
+
defaultValue: DataTypes.NOW
|
|
588
|
+
},
|
|
589
|
+
retiredAt: {
|
|
590
|
+
type: DataTypes.DATE,
|
|
591
|
+
allowNull: true
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Initialize database (sync schema)
|
|
596
|
+
// Initialize database (sync schema)
|
|
597
|
+
async function initDb() {
|
|
598
|
+
const crypto = require('crypto');
|
|
599
|
+
|
|
600
|
+
let existingKeys = [];
|
|
601
|
+
try {
|
|
602
|
+
const [results] = await sequelize.query("SELECT * FROM `AccessKeys` LIMIT 1");
|
|
603
|
+
if (results.length > 0 && 'token' in results[0]) {
|
|
604
|
+
const [allKeys] = await sequelize.query("SELECT * FROM `AccessKeys`");
|
|
605
|
+
existingKeys = allKeys;
|
|
606
|
+
console.log(`[Migration] Found ${existingKeys.length} existing plaintext tokens to migrate.`);
|
|
607
|
+
await sequelize.query("DROP TABLE `AccessKeys`");
|
|
608
|
+
console.log("[Migration] Dropped old AccessKeys table.");
|
|
609
|
+
}
|
|
610
|
+
} catch (err) {
|
|
611
|
+
// Table or column doesn't exist yet, which is fine
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (sequelize.getDialect() === 'sqlite') {
|
|
615
|
+
await sequelize.query("PRAGMA foreign_keys = OFF;");
|
|
616
|
+
}
|
|
617
|
+
await sequelize.sync({ alter: true });
|
|
618
|
+
if (sequelize.getDialect() === 'sqlite') {
|
|
619
|
+
await sequelize.query("PRAGMA foreign_keys = ON;");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Enforce DB-level delete prevention triggers on TransferLogs
|
|
623
|
+
try {
|
|
624
|
+
const dialect = (sequelize.connectionManager && sequelize.connectionManager.dialectName) || sequelize.getDialect();
|
|
625
|
+
if (dialect === 'sqlite') {
|
|
626
|
+
await sequelize.query(`
|
|
627
|
+
CREATE TRIGGER IF NOT EXISTS prevent_delete_transferlog
|
|
628
|
+
BEFORE DELETE ON \`TransferLogs\`
|
|
629
|
+
BEGIN
|
|
630
|
+
SELECT RAISE(ABORT, 'Deletes are not allowed on TransferLogs table');
|
|
631
|
+
END;
|
|
632
|
+
`);
|
|
633
|
+
console.log("[DB Trigger] SQLite prevent_delete_transferlog trigger configured.");
|
|
634
|
+
} else if (dialect === 'mysql') {
|
|
635
|
+
await sequelize.query(`DROP TRIGGER IF EXISTS prevent_delete_transferlog;`);
|
|
636
|
+
await sequelize.query(`
|
|
637
|
+
CREATE TRIGGER prevent_delete_transferlog BEFORE DELETE ON \`TransferLogs\`
|
|
638
|
+
FOR EACH ROW
|
|
639
|
+
BEGIN
|
|
640
|
+
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Deletes are not allowed on TransferLogs table';
|
|
641
|
+
END;
|
|
642
|
+
`);
|
|
643
|
+
console.log("[DB Trigger] MySQL prevent_delete_transferlog trigger configured.");
|
|
644
|
+
|
|
645
|
+
// Defensively attempt to revoke DELETE, DROP privileges on TransferLogs for the current user
|
|
646
|
+
try {
|
|
647
|
+
await sequelize.query(`REVOKE DELETE, DROP ON \`TransferLogs\` FROM CURRENT_USER;`);
|
|
648
|
+
console.log("[DB Privilege] Revoked DELETE, DROP on TransferLogs from CURRENT_USER.");
|
|
649
|
+
} catch (privErr) {
|
|
650
|
+
console.warn(`[DB Privilege Warning] Could not revoke privileges (current connection user may have global grants or lack grant privileges): ${privErr.message}`);
|
|
651
|
+
}
|
|
652
|
+
} else if (dialect === 'postgres') {
|
|
653
|
+
await sequelize.query(`
|
|
654
|
+
CREATE OR REPLACE FUNCTION prevent_delete_transferlog_func()
|
|
655
|
+
RETURNS TRIGGER AS $$
|
|
656
|
+
BEGIN
|
|
657
|
+
RAISE EXCEPTION 'Deletes are not allowed on TransferLogs table';
|
|
658
|
+
END;
|
|
659
|
+
$$ LANGUAGE plpgsql;
|
|
660
|
+
`);
|
|
661
|
+
await sequelize.query(`DROP TRIGGER IF EXISTS prevent_delete_transferlog ON "TransferLogs";`);
|
|
662
|
+
await sequelize.query(`
|
|
663
|
+
CREATE TRIGGER prevent_delete_transferlog
|
|
664
|
+
BEFORE DELETE ON "TransferLogs"
|
|
665
|
+
FOR EACH ROW
|
|
666
|
+
EXECUTE FUNCTION prevent_delete_transferlog_func();
|
|
667
|
+
`);
|
|
668
|
+
console.log("[DB Trigger] PostgreSQL prevent_delete_transferlog trigger configured.");
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
await sequelize.query(`REVOKE DELETE, TRUNCATE ON "TransferLogs" FROM CURRENT_USER;`);
|
|
672
|
+
console.log("[DB Privilege] Revoked DELETE, TRUNCATE on TransferLogs from CURRENT_USER.");
|
|
673
|
+
} catch (privErr) {
|
|
674
|
+
console.warn(`[DB Privilege Warning] Could not revoke privileges: ${privErr.message}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
} catch (triggerErr) {
|
|
678
|
+
console.error("Failed to configure database-level triggers or privileges:", triggerErr);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (existingKeys.length > 0) {
|
|
682
|
+
console.log(`[Migration] Migrating ${existingKeys.length} tokens to SHA-256...`);
|
|
683
|
+
for (const key of existingKeys) {
|
|
684
|
+
const rawToken = key.token;
|
|
685
|
+
if (rawToken) {
|
|
686
|
+
const hashed = crypto.createHash('sha256').update(rawToken).digest('hex');
|
|
687
|
+
await AccessKey.create({
|
|
688
|
+
token_hash: hashed,
|
|
689
|
+
userId: key.userId,
|
|
690
|
+
groupId: key.groupId,
|
|
691
|
+
status: key.status,
|
|
692
|
+
expiresAt: key.expiresAt
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
console.log("[Migration] Successfully migrated all existing tokens in-place.");
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Seed default system configs if not present
|
|
700
|
+
await SystemConfig.findOrCreate({
|
|
701
|
+
where: { key: 'siem_webhook_url' },
|
|
702
|
+
defaults: { value: '' }
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
await SystemConfig.findOrCreate({
|
|
706
|
+
where: { key: 'siem_webhook_secret' },
|
|
707
|
+
defaults: { value: crypto.randomBytes(32).toString('hex') }
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
await SystemConfig.findOrCreate({
|
|
711
|
+
where: { key: 'audit_logging_enabled' },
|
|
712
|
+
defaults: { value: 'true' }
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Default admin user auto-seeding removed for security. First admin account must be created via setup wizard.
|
|
716
|
+
|
|
717
|
+
// One-time migration from db.json if tables are empty
|
|
718
|
+
const groupCount = await VaultGroup.count();
|
|
719
|
+
const dbJsonPath = path.join(DATA_DIR, 'db.json');
|
|
720
|
+
if (fs.existsSync(dbJsonPath)) {
|
|
721
|
+
try {
|
|
722
|
+
const rawJson = fs.readFileSync(dbJsonPath, 'utf8');
|
|
723
|
+
const dbJson = JSON.parse(rawJson);
|
|
724
|
+
|
|
725
|
+
if (groupCount === 0) {
|
|
726
|
+
console.log('Migrating data from db.json into database...');
|
|
727
|
+
// 1. Migrate Groups & Profiles
|
|
728
|
+
if (dbJson.groups) {
|
|
729
|
+
for (const g of dbJson.groups) {
|
|
730
|
+
await VaultGroup.create({
|
|
731
|
+
id: g.id,
|
|
732
|
+
name: g.name,
|
|
733
|
+
ipAllowlist: g.ip_allowlist || ''
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
if (g.profiles) {
|
|
737
|
+
for (const p of g.profiles) {
|
|
738
|
+
let keyVal = p.password || '';
|
|
739
|
+
if (p.options && p.options.private_key) {
|
|
740
|
+
keyVal = p.options.private_key;
|
|
741
|
+
}
|
|
742
|
+
await ConnectionProfile.create({
|
|
743
|
+
id: p.id,
|
|
744
|
+
groupId: g.id,
|
|
745
|
+
name: p.name,
|
|
746
|
+
protocol: p.protocol,
|
|
747
|
+
host: p.host,
|
|
748
|
+
port: parseInt(p.port) || 22,
|
|
749
|
+
username: p.username || '',
|
|
750
|
+
passwordEncrypted: keyVal,
|
|
751
|
+
authMode: p.options && p.options.private_key ? 'keypair' : 'password'
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// 2. Migrate Access Tokens if table is empty AND SEED_DEV_TOKENS=true is set
|
|
760
|
+
const tokenCount = await AccessKey.count();
|
|
761
|
+
if (tokenCount === 0 && dbJson.tokens && process.env.SEED_DEV_TOKENS === 'true') {
|
|
762
|
+
console.log('Seeding dev tokens from db.json into database...');
|
|
763
|
+
for (const t of dbJson.tokens) {
|
|
764
|
+
const username = t.user || 'Unknown User';
|
|
765
|
+
const [usr] = await User.findOrCreate({
|
|
766
|
+
where: { username },
|
|
767
|
+
defaults: {
|
|
768
|
+
passwordHash: crypto.createHash('sha256').update('temporary-user-password').digest('hex'),
|
|
769
|
+
role: 'operator'
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const hashed = crypto.createHash('sha256').update(t.token).digest('hex');
|
|
774
|
+
await AccessKey.create({
|
|
775
|
+
token_hash: hashed,
|
|
776
|
+
userId: usr.id,
|
|
777
|
+
groupId: t.groupId,
|
|
778
|
+
status: t.status || 'active',
|
|
779
|
+
expiresAt: t.expires_at || ''
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// 3. Backfill TransferLog hashes if missing
|
|
785
|
+
const unhashedLogs = await TransferLog.findAll({
|
|
786
|
+
where: { entry_hash: null },
|
|
787
|
+
order: [['id', 'ASC']]
|
|
788
|
+
});
|
|
789
|
+
if (unhashedLogs.length > 0) {
|
|
790
|
+
console.log(`[Migration] Backfilling hash chain for ${unhashedLogs.length} audit logs...`);
|
|
791
|
+
for (const log of unhashedLogs) {
|
|
792
|
+
const prevLog = await TransferLog.findOne({
|
|
793
|
+
where: {
|
|
794
|
+
id: {
|
|
795
|
+
[Sequelize.Op.lt]: log.id
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
order: [['id', 'DESC']]
|
|
799
|
+
});
|
|
800
|
+
const prevHash = prevLog ? (prevLog.entry_hash || '0') : '0';
|
|
801
|
+
log.prev_hash = prevHash;
|
|
802
|
+
|
|
803
|
+
const logFields = {
|
|
804
|
+
token: log.token || null,
|
|
805
|
+
username: log.username || null,
|
|
806
|
+
connectionId: log.connectionId,
|
|
807
|
+
filePath: log.filePath,
|
|
808
|
+
fileSize: log.fileSize ? parseInt(log.fileSize) : 0,
|
|
809
|
+
action: log.action,
|
|
810
|
+
status: log.status || 'success',
|
|
811
|
+
errorMessage: log.errorMessage || null,
|
|
812
|
+
prev_hash: prevHash
|
|
813
|
+
};
|
|
814
|
+
const hashInput = JSON.stringify(logFields);
|
|
815
|
+
log.entry_hash = crypto.createHash('sha256').update(hashInput).digest('hex');
|
|
816
|
+
await log.save();
|
|
817
|
+
}
|
|
818
|
+
console.log('[Migration] Finished backfilling audit log hashes.');
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// 4. Envelope Encryption Migration for VaultGroups
|
|
822
|
+
await KeyVersion.findOrCreate({
|
|
823
|
+
where: { version: 1 },
|
|
824
|
+
defaults: { createdAt: new Date() }
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
const loadKekLocal = () => {
|
|
828
|
+
const envKey = process.env.VAULT_MASTER_KEY;
|
|
829
|
+
if (envKey) {
|
|
830
|
+
if (envKey.length === 64 && /^[0-9a-fA-F]+$/.test(envKey)) {
|
|
831
|
+
return Buffer.from(envKey, 'hex');
|
|
832
|
+
} else if (envKey.length === 44 && /^[0-9a-zA-Z+/=]+$/.test(envKey)) {
|
|
833
|
+
return Buffer.from(envKey, 'base64');
|
|
834
|
+
} else {
|
|
835
|
+
return Buffer.from(envKey, 'utf8');
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
const keyPath = path.join(DATA_DIR, '.vault_key');
|
|
839
|
+
if (fs.existsSync(keyPath)) {
|
|
840
|
+
const fileKey = fs.readFileSync(keyPath).toString('utf8').trim();
|
|
841
|
+
if (fileKey.length === 64 && /^[0-9a-fA-F]+$/.test(fileKey)) {
|
|
842
|
+
return Buffer.from(fileKey, 'hex');
|
|
843
|
+
} else if (fileKey.length === 44 && /^[0-9a-zA-Z+/=]+$/.test(fileKey)) {
|
|
844
|
+
return Buffer.from(fileKey, 'base64');
|
|
845
|
+
} else {
|
|
846
|
+
return fs.readFileSync(keyPath);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return crypto.scryptSync("fallback-vault-salt", "salt", 32);
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
const localKek = loadKekLocal();
|
|
853
|
+
|
|
854
|
+
const decryptWithKeyLocal = (ciphertext, key) => {
|
|
855
|
+
if (!ciphertext || !ciphertext.startsWith('enc:')) return ciphertext;
|
|
856
|
+
try {
|
|
857
|
+
const parts = ciphertext.split(':');
|
|
858
|
+
if (parts.length !== 4) return ciphertext;
|
|
859
|
+
const iv = Buffer.from(parts[1], 'hex');
|
|
860
|
+
const tag = Buffer.from(parts[2], 'hex');
|
|
861
|
+
const encryptedText = Buffer.from(parts[3], 'hex');
|
|
862
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
863
|
+
decipher.setAuthTag(tag);
|
|
864
|
+
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
|
|
865
|
+
decrypted += decipher.final('utf8');
|
|
866
|
+
return decrypted;
|
|
867
|
+
} catch (err) {
|
|
868
|
+
console.error("Migration decryption failed:", err);
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
const encryptWithKeyLocal = (plaintext, key) => {
|
|
874
|
+
if (!plaintext) return plaintext;
|
|
875
|
+
try {
|
|
876
|
+
const iv = crypto.randomBytes(12);
|
|
877
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
878
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
|
879
|
+
encrypted += cipher.final('hex');
|
|
880
|
+
const tag = cipher.getAuthTag().toString('hex');
|
|
881
|
+
return `enc:${iv.toString('hex')}:${tag}:${encrypted}`;
|
|
882
|
+
} catch (err) {
|
|
883
|
+
console.error("Migration encryption failed:", err);
|
|
884
|
+
throw err;
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
const wrapDekLocal = (dek, kek) => {
|
|
889
|
+
const iv = crypto.randomBytes(12);
|
|
890
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', kek, iv);
|
|
891
|
+
let encrypted = cipher.update(dek);
|
|
892
|
+
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
893
|
+
const tag = cipher.getAuthTag();
|
|
894
|
+
const combined = Buffer.concat([iv, tag, encrypted]);
|
|
895
|
+
return combined.toString('base64');
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
const unmigratedGroups = await VaultGroup.findAll({ where: { migrated: false } });
|
|
899
|
+
if (unmigratedGroups.length > 0) {
|
|
900
|
+
console.log(`[Migration] Migrating ${unmigratedGroups.length} VaultGroups to Envelope Encryption...`);
|
|
901
|
+
for (const group of unmigratedGroups) {
|
|
902
|
+
const dek = crypto.randomBytes(32);
|
|
903
|
+
const profiles = await ConnectionProfile.findAll({ where: { groupId: group.id } });
|
|
904
|
+
|
|
905
|
+
for (const p of profiles) {
|
|
906
|
+
const decPass = decryptWithKeyLocal(p.passwordEncrypted, localKek);
|
|
907
|
+
const decJumpPass = decryptWithKeyLocal(p.jumpPasswordEncrypted, localKek);
|
|
908
|
+
|
|
909
|
+
p.passwordEncrypted = encryptWithKeyLocal(decPass, dek);
|
|
910
|
+
p.jumpPasswordEncrypted = encryptWithKeyLocal(decJumpPass, dek);
|
|
911
|
+
await p.save();
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
group.wrapped_dek = wrapDekLocal(dek, localKek);
|
|
915
|
+
group.dek_version = 1;
|
|
916
|
+
group.migrated = true;
|
|
917
|
+
await group.save();
|
|
918
|
+
}
|
|
919
|
+
console.log('[Migration] Finished migrating groups to Envelope Encryption.');
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
} catch (err) {
|
|
923
|
+
console.error('Failed to migrate/seed data from db.json:', err);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// 3. Migrate legacy audit logs (move attribution JSON from errorMessage to metadata)
|
|
928
|
+
try {
|
|
929
|
+
const logs = await TransferLog.findAll();
|
|
930
|
+
const unmigratedLogs = logs.filter(l => l.hash_version !== 1 && l.hash_version !== 2);
|
|
931
|
+
if (unmigratedLogs.length > 0) {
|
|
932
|
+
console.log(`[Migration] Migrating ${unmigratedLogs.length} legacy TransferLog entries...`);
|
|
933
|
+
for (const log of unmigratedLogs) {
|
|
934
|
+
if (log.errorMessage && log.errorMessage.trim().startsWith('{') && log.errorMessage.trim().endsWith('}')) {
|
|
935
|
+
try {
|
|
936
|
+
JSON.parse(log.errorMessage);
|
|
937
|
+
log.metadata = log.errorMessage;
|
|
938
|
+
log.errorMessage = null;
|
|
939
|
+
} catch (e) {
|
|
940
|
+
// Not valid JSON, keep as plain text errorMessage
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
log.hash_version = 1;
|
|
944
|
+
await log.save();
|
|
945
|
+
}
|
|
946
|
+
console.log("[Migration] Legacy TransferLog entries migration completed.");
|
|
947
|
+
}
|
|
948
|
+
} catch (migErr) {
|
|
949
|
+
console.error("Failed to migrate legacy TransferLogs:", migErr);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function updateDbConnection(config) {
|
|
954
|
+
sequelize.config.database = config.database;
|
|
955
|
+
sequelize.config.username = config.username;
|
|
956
|
+
sequelize.config.password = config.password;
|
|
957
|
+
sequelize.config.host = config.host || 'localhost';
|
|
958
|
+
sequelize.config.port = config.port || 3306;
|
|
959
|
+
|
|
960
|
+
sequelize.options.dialect = config.dialect || 'mysql';
|
|
961
|
+
if (config.storage) {
|
|
962
|
+
sequelize.options.storage = config.storage;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
sequelize.options.dialectOptions = config.ssl ? {
|
|
966
|
+
ssl: {
|
|
967
|
+
require: true,
|
|
968
|
+
rejectUnauthorized: false
|
|
969
|
+
}
|
|
970
|
+
} : {};
|
|
971
|
+
|
|
972
|
+
if (sequelize.connectionManager) {
|
|
973
|
+
if (sequelize.connectionManager.pool) {
|
|
974
|
+
try {
|
|
975
|
+
sequelize.connectionManager.pool.destroyAllNow();
|
|
976
|
+
} catch (_) {}
|
|
977
|
+
}
|
|
978
|
+
if (sequelize.connectionManager.connections) {
|
|
979
|
+
sequelize.connectionManager.connections = {};
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
module.exports = {
|
|
985
|
+
sequelize,
|
|
986
|
+
User,
|
|
987
|
+
VaultGroup,
|
|
988
|
+
ConnectionProfile,
|
|
989
|
+
AccessKey,
|
|
990
|
+
TransferLog,
|
|
991
|
+
FileVersion,
|
|
992
|
+
SystemConfig,
|
|
993
|
+
PendingReversion,
|
|
994
|
+
AdminSession,
|
|
995
|
+
MfaChallenge,
|
|
996
|
+
KeyVersion,
|
|
997
|
+
initDb,
|
|
998
|
+
updateDbConnection
|
|
999
|
+
};
|