fix: File cleanup
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user