fix: File cleanup

This commit is contained in:
LukeGus
2025-10-31 20:59:17 -05:00
parent eaa143ca60
commit e375878576
62 changed files with 121 additions and 1433 deletions

View File

@@ -915,7 +915,6 @@ app.post(
const isOidcUser = !!userRecords[0].is_oidc;
if (!isOidcUser) {
// Local accounts still prove knowledge of the password so their DEK can be derived again.
if (!password) {
return res.status(400).json({
error: "Password required for import",
@@ -928,7 +927,6 @@ app.post(
return res.status(401).json({ error: "Invalid password" });
}
} else if (!DataCrypto.getUserDataKey(userId)) {
// OIDC users skip the password prompt; make sure their DEK is unlocked via the OIDC session.
const oidcUnlocked = await authManager.authenticateOIDCUser(userId);
if (!oidcUnlocked) {
return res.status(403).json({
@@ -947,7 +945,6 @@ app.post(
let userDataKey = DataCrypto.getUserDataKey(userId);
if (!userDataKey && isOidcUser) {
// authenticateOIDCUser lazily provisions the session key; retry the fetch when it succeeds.
const oidcUnlocked = await authManager.authenticateOIDCUser(userId);
if (oidcUnlocked) {
userDataKey = DataCrypto.getUserDataKey(userId);
@@ -1425,7 +1422,6 @@ app.use(
err: unknown,
req: express.Request,
res: express.Response,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_next: express.NextFunction,
) => {
apiLogger.error("Unhandled error in request", err, {
@@ -1482,17 +1478,13 @@ app.get(
if (status.hasUnencryptedDb) {
try {
unencryptedSize = fs.statSync(dbPath).size;
} catch {
// Ignore file access errors
}
} catch {}
}
if (status.hasEncryptedDb) {
try {
encryptedSize = fs.statSync(encryptedDbPath).size;
} catch {
// Ignore file access errors
}
} catch {}
}
res.json({

View File

@@ -12,10 +12,6 @@ import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js";
const dataDir = process.env.DATA_DIR || "./db/data";
const dbDir = path.resolve(dataDir);
if (!fs.existsSync(dbDir)) {
databaseLogger.info(`Creating database directory`, {
operation: "db_init",
path: dbDir,
});
fs.mkdirSync(dbDir, { recursive: true });
}
@@ -31,7 +27,6 @@ let sqlite: Database.Database;
async function initializeDatabaseAsync(): Promise<void> {
const systemCrypto = SystemCrypto.getInstance();
// Ensure database key is initialized
await systemCrypto.getDatabaseKey();
if (enableFileEncryption) {
try {
@@ -41,18 +36,11 @@ async function initializeDatabaseAsync(): Promise<void> {
memoryDatabase = new Database(decryptedBuffer);
// Count sessions after loading
try {
const sessionCount = memoryDatabase
.prepare("SELECT COUNT(*) as count FROM sessions")
.get() as { count: number };
databaseLogger.info("Database loaded from encrypted file", {
operation: "db_load",
sessionCount: sessionCount.count,
bufferSize: decryptedBuffer.length,
});
} catch (countError) {
// Ignore count errors
}
} else {
const migration = new DatabaseMigration(dataDir);
@@ -297,9 +285,6 @@ async function initializeCompleteDatabase(): Promise<void> {
try {
sqlite.prepare("DELETE FROM sessions").run();
databaseLogger.info("All sessions cleared on startup", {
operation: "db_init_session_cleanup",
});
} catch (e) {
databaseLogger.warn("Could not clear sessions on startup", {
operation: "db_init_session_cleanup_failed",
@@ -453,7 +438,6 @@ const migrateSchema = () => {
addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL");
addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL");
// Create sessions table if it doesn't exist (for existing databases)
try {
sqlite
.prepare("SELECT id FROM sessions LIMIT 1")
@@ -473,9 +457,6 @@ const migrateSchema = () => {
FOREIGN KEY (user_id) REFERENCES users (id)
);
`);
databaseLogger.info("Sessions table created via migration", {
operation: "schema_migration",
});
} catch (createError) {
databaseLogger.warn("Failed to create sessions table", {
operation: "schema_migration",
@@ -499,18 +480,11 @@ async function saveMemoryDatabaseToFile() {
fs.mkdirSync(dataDir, { recursive: true });
}
// Count sessions before saving
try {
const sessionCount = memoryDatabase
.prepare("SELECT COUNT(*) as count FROM sessions")
.get() as { count: number };
databaseLogger.info("Saving database to file", {
operation: "db_save",
sessionCount: sessionCount.count,
bufferSize: buffer.length,
});
} catch (countError) {
// Ignore count errors
}
if (enableFileEncryption) {
@@ -605,18 +579,15 @@ async function cleanupDatabase() {
try {
fs.unlinkSync(path.join(tempDir, file));
} catch {
// Ignore cleanup errors
}
}
try {
fs.rmdirSync(tempDir);
} catch {
// Ignore cleanup errors
}
}
} catch {
// Ignore cleanup errors
}
}
@@ -625,7 +596,6 @@ process.on("exit", () => {
try {
sqlite.close();
} catch {
// Ignore close errors on exit
}
}
});

View File

@@ -336,14 +336,10 @@ router.post("/oidc-config", authenticateJWT, async (req, res) => {
userId,
adminDataKey,
);
authLogger.info("OIDC configuration encrypted with admin data key", {
operation: "oidc_config_encrypt",
userId,
});
} else {
encryptedConfig = {
...config,
client_secret: `encrypted:${Buffer.from(client_secret).toString("base64")}`, // Simple base64 encoding
client_secret: `encrypted:${Buffer.from(client_secret).toString("base64")}`,
};
authLogger.warn(
"OIDC configuration stored with basic encoding - admin should re-save with password",
@@ -421,7 +417,6 @@ router.get("/oidc-config", async (req, res) => {
const config = JSON.parse((row as Record<string, unknown>).value as string);
// Only return public fields needed for login page
const publicConfig = {
client_id: config.client_id,
issuer_url: config.issuer_url,
@@ -661,7 +656,6 @@ router.get("/oidc/callback", async (req, res) => {
config.client_id,
);
} catch {
// Fallback to manual decoding
try {
const parts = (tokenData.id_token as string).split(".");
if (parts.length === 3) {
@@ -812,7 +806,6 @@ router.get("/oidc/callback", async (req, res) => {
});
}
// Detect platform and device info
const deviceInfo = parseUserAgent(req);
const token = await authManager.generateJWTToken(userRecord.id, {
deviceType: deviceInfo.type,
@@ -838,7 +831,6 @@ router.get("/oidc/callback", async (req, res) => {
const redirectUrl = new URL(frontendUrl);
redirectUrl.searchParams.set("success", "true");
// Calculate max age based on device type
const maxAge =
deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
? 30 * 24 * 60 * 60 * 1000
@@ -965,7 +957,6 @@ router.post("/login", async (req, res) => {
});
}
// Detect platform and device info
const deviceInfo = parseUserAgent(req);
const token = await authManager.generateJWTToken(userRecord.id, {
deviceType: deviceInfo.type,
@@ -995,7 +986,6 @@ router.post("/login", async (req, res) => {
response.token = token;
}
// Calculate max age based on device type
const maxAge =
deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
? 30 * 24 * 60 * 60 * 1000
@@ -1018,7 +1008,6 @@ router.post("/logout", authenticateJWT, async (req, res) => {
const userId = authReq.userId;
if (userId) {
// Get sessionId from JWT if available
const token =
req.cookies?.jwt || req.headers["authorization"]?.split(" ")[1];
let sessionId: string | undefined;
@@ -1027,9 +1016,7 @@ router.post("/logout", authenticateJWT, async (req, res) => {
try {
const payload = await authManager.verifyJWTToken(token);
sessionId = payload?.sessionId;
} catch (error) {
// Ignore token verification errors during logout
}
} catch (error) {}
}
await authManager.logoutUser(userId, sessionId);
@@ -1435,7 +1422,6 @@ router.post("/complete-reset", async (req, res) => {
const saltRounds = parseInt(process.env.SALT || "10", 10);
const password_hash = await bcrypt.hash(newPassword, saltRounds);
// Check if user is logged in and data is unlocked
let userIdFromJwt: string | null = null;
const cookie = req.cookies?.jwt;
let header: string | undefined;
@@ -1452,7 +1438,6 @@ router.post("/complete-reset", async (req, res) => {
}
if (userIdFromJwt === userId) {
// Logged-in user: preserve data
try {
const success = await authManager.resetUserPasswordWithPreservedDEK(
userId,
@@ -1491,15 +1476,12 @@ router.post("/complete-reset", async (req, res) => {
});
}
} else {
// Logged-out user: data is lost
await db
.update(users)
.set({ password_hash })
.where(eq(users.username, username));
try {
// Delete all encrypted data since we're creating a new DEK
// The old DEK is lost, so old encrypted data becomes unreadable
await db
.delete(sshCredentialUsage)
.where(eq(sshCredentialUsage.userId, userId));
@@ -1524,11 +1506,9 @@ router.post("/complete-reset", async (req, res) => {
.delete(sshCredentials)
.where(eq(sshCredentials.userId, userId));
// Now setup new encryption with new DEK
await authManager.registerUser(userId, newPassword);
authManager.logoutUser(userId);
// Clear TOTP settings
await db
.update(users)
.set({
@@ -1597,13 +1577,11 @@ router.post("/change-password", authenticateJWT, async (req, res) => {
return res.status(404).json({ error: "User not found" });
}
// Verify old password for login hash
const isMatch = await bcrypt.compare(oldPassword, user[0].password_hash);
if (!isMatch) {
return res.status(401).json({ error: "Incorrect current password" });
}
// Change encryption keys and login hash
const success = await authManager.changeUserPassword(
userId,
oldPassword,
@@ -1619,7 +1597,7 @@ router.post("/change-password", authenticateJWT, async (req, res) => {
const password_hash = await bcrypt.hash(newPassword, saltRounds);
await db.update(users).set({ password_hash }).where(eq(users.id, userId));
authManager.logoutUser(userId); // Log out user for security
authManager.logoutUser(userId);
res.json({ message: "Password changed successfully. Please log in again." });
});
@@ -1836,7 +1814,6 @@ router.post("/totp/verify-login", async (req, res) => {
.where(eq(users.id, userRecord.id));
}
// Detect platform and device info
const deviceInfo = parseUserAgent(req);
const token = await authManager.generateJWTToken(userRecord.id, {
deviceType: deviceInfo.type,
@@ -1867,7 +1844,6 @@ router.post("/totp/verify-login", async (req, res) => {
response.token = token;
}
// Calculate max age based on device type
const maxAge =
deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
? 30 * 24 * 60 * 60 * 1000
@@ -2230,7 +2206,6 @@ router.get("/data-status", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
try {
// Data lock functionality has been removed - always return unlocked for authenticated users
res.json({
unlocked: true,
message: "Data is unlocked",
@@ -2320,10 +2295,8 @@ router.get("/sessions", authenticateJWT, async (req, res) => {
let sessionList;
if (userRecord.is_admin) {
// Admin: Get all sessions with user info
sessionList = await authManager.getAllSessions();
// Join with users to get usernames
const enrichedSessions = await Promise.all(
sessionList.map(async (session) => {
const sessionUser = await db
@@ -2341,7 +2314,6 @@ router.get("/sessions", authenticateJWT, async (req, res) => {
return res.json({ sessions: enrichedSessions });
} else {
// Regular user: Get only their own sessions
sessionList = await authManager.getUserSessions(userId);
return res.json({ sessions: sessionList });
}
@@ -2369,7 +2341,6 @@ router.delete("/sessions/:sessionId", authenticateJWT, async (req, res) => {
const userRecord = user[0];
// Check if session exists
const sessionRecords = await db
.select()
.from(sessions)
@@ -2382,7 +2353,6 @@ router.delete("/sessions/:sessionId", authenticateJWT, async (req, res) => {
const session = sessionRecords[0];
// Non-admin users can only revoke their own sessions
if (!userRecord.is_admin && session.userId !== userId) {
return res
.status(403)
@@ -2421,19 +2391,15 @@ router.post("/sessions/revoke-all", authenticateJWT, async (req, res) => {
const userRecord = user[0];
// Determine which user's sessions to revoke
let revokeUserId = userId;
if (targetUserId && userRecord.is_admin) {
// Admin can revoke any user's sessions
revokeUserId = targetUserId;
} else if (targetUserId && targetUserId !== userId) {
// Non-admin can only revoke their own sessions
return res.status(403).json({
error: "Not authorized to revoke sessions for other users",
});
}
// Get current session ID if needed
let currentSessionId: string | undefined;
if (exceptCurrent) {
const token =