Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f56b5d8
feat: add batch upload result messages for file uploads in StatusPage
bbbugg Feb 8, 2026
4ae9529
feat: support batch upload of JSON files from ZIP archives in StatusPage
bbbugg Feb 9, 2026
b3cbbe6
feat: implement batch delete functionality for accounts in StatusPage
bbbugg Feb 9, 2026
0e3485f
feat: implement batch download functionality for accounts in StatusPage
bbbugg Feb 9, 2026
45fed73
chore: add escapeHtml utility to prevent XSS in StatusPage and AuthPage
bbbugg Feb 9, 2026
98455f9
chore: add error messages for file read and JSON validation in Status…
bbbugg Feb 9, 2026
dded375
chore: validate selected accounts against available account details i…
bbbugg Feb 9, 2026
b4a32f0
chore: update batch download to use actual file count from response h…
bbbugg Feb 9, 2026
2384f99
chore: update error handling messages for account not found scenarios
bbbugg Feb 9, 2026
bd37e35
Update src/routes/StatusRoutes.js
bbbugg Feb 9, 2026
5c85c0c
chore: enhance error handling for file uploads in StatusPage
bbbugg Feb 9, 2026
02a65de
chore: improve error handling for batch download responses in StatusPage
bbbugg Feb 9, 2026
b5fd2e4
chore: update error messages for account not found scenarios in Statu…
bbbugg Feb 9, 2026
ab13716
chore: improve error handling for batch download failures in StatusRo…
bbbugg Feb 9, 2026
f0db627
chore: update error handling for account not found scenarios in Statu…
bbbugg Feb 9, 2026
54b41ff
chore: enhance error messages for account not found scenarios in Stat…
bbbugg Feb 9, 2026
f4be881
chore: enhance error handling and localization for file reading and z…
bbbugg Feb 9, 2026
bd212be
chore: add support message for unsupported file types in file upload …
bbbugg Feb 9, 2026
bc5d75b
chore: add partial success handling and messages for batch download a…
bbbugg Feb 9, 2026
a812e9d
chore: improve error handling for invalid account indices in StatusRo…
bbbugg Feb 9, 2026
f3f2d01
chore: update package.json to add jszip dependency
bbbugg Feb 9, 2026
5bf67a7
chore: add client disconnect handling to batch download process
bbbugg Feb 9, 2026
bdb0f13
chore: prevent aborting archive on client disconnect if response alre…
bbbugg Feb 10, 2026
eef2862
style: update styles for batch operations and enhance success color u…
bbbugg Feb 9, 2026
4e30f28
style: update button hover background to transparent in StatusPage
bbbugg Feb 10, 2026
d9a587e
style: enhance clickable version display and copy functionality in St…
bbbugg Feb 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@
"format:check": "prettier --check ."
},
"dependencies": {
"archiver": "^7.0.1",
"axios": "^1.13.2",
"basic-auth": "^2.0.1",
"cookie-parser": "^1.4.6",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-session": "^1.18.0",
"jszip": "^3.10.1",
"mime-types": "^3.0.2",
"playwright": "^1.53.1",
"ws": "^8.17.0"
Expand Down Expand Up @@ -88,4 +90,4 @@
],
"*.{md,yml,yaml}": "prettier --write"
}
}
}
177 changes: 177 additions & 0 deletions src/routes/StatusRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

const fs = require("fs");
const path = require("path");
const archiver = require("archiver");
const VersionChecker = require("../utils/VersionChecker");
const LoggingService = require("../utils/LoggingService");

Expand Down Expand Up @@ -242,6 +243,182 @@ class StatusRoutes {
}
});

// Batch delete accounts - Must be defined before /api/accounts/:index to avoid index matching "batch"
app.delete("/api/accounts/batch", isAuthenticated, async (req, res) => {
const { indices, force } = req.body;
const currentAuthIndex = this.serverSystem.requestHandler.currentAuthIndex;

// Validate parameters
if (!Array.isArray(indices) || indices.length === 0) {
return res.status(400).json({ message: "errorInvalidIndex" });
}

const { authSource } = this.serverSystem;
const uniqueIndices = Array.from(new Set(indices));
const validIndices = uniqueIndices.filter(
idx => Number.isInteger(idx) && authSource.initialIndices.includes(idx)
);

const invalidIndices = uniqueIndices.filter(
idx => !Number.isInteger(idx) || !authSource.initialIndices.includes(idx)
);

if (validIndices.length === 0) {
return res.status(404).json({
indices: invalidIndices.join(", "),
message: "errorAccountsNotFound",
});
}

const successIndices = [];
const failedIndices = [];

// Add invalid indices to failed list immediately
for (const idx of invalidIndices) {
failedIndices.push({
error: "Account not found or invalid",
index: idx,
});
}

// Check if current active account is included in VALID indices
const includesCurrent = validIndices.includes(currentAuthIndex);
if (includesCurrent && !force) {
return res.status(409).json({
includesCurrent: true,
message: "warningDeleteCurrentAccount",
requiresConfirmation: true,
});
}

for (const targetIndex of validIndices) {
try {
authSource.removeAuth(targetIndex);
successIndices.push(targetIndex);
this.logger.warn(`[WebUI] Account #${targetIndex} deleted via batch delete.`);
} catch (error) {
failedIndices.push({ error: error.message, index: targetIndex });
this.logger.error(`[WebUI] Failed to delete account #${targetIndex}: ${error.message}`);
}
}

// If current active account was deleted, close browser connection
if (includesCurrent && successIndices.includes(currentAuthIndex)) {
this.logger.warn(
`[WebUI] Current active account #${currentAuthIndex} was deleted. Closing browser connection...`
);
this.serverSystem.browserManager.closeBrowser().catch(err => {
this.logger.error(`[WebUI] Error closing browser after batch deletion: ${err.message}`);
});
this.serverSystem.browserManager.currentAuthIndex = -1;
}

if (failedIndices.length > 0) {
return res.status(207).json({
failedIndices,
message: "batchDeletePartial",
successCount: successIndices.length,
successIndices,
});
}

return res.status(200).json({
message: "batchDeleteSuccess",
successCount: successIndices.length,
successIndices,
});
});

// Batch download accounts as ZIP
app.post("/api/accounts/batch/download", isAuthenticated, async (req, res) => {
const { indices } = req.body;

// Validate parameters
if (!Array.isArray(indices) || indices.length === 0) {
return res.status(400).json({ message: "errorInvalidIndex" });
}

const { authSource } = this.serverSystem;
const uniqueIndices = Array.from(new Set(indices));

const invalidIndices = uniqueIndices.filter(
idx => !Number.isInteger(idx) || !authSource.initialIndices.includes(idx)
);

const validIndices = uniqueIndices.filter(
idx => Number.isInteger(idx) && authSource.initialIndices.includes(idx)
);

if (validIndices.length === 0) {
return res.status(404).json({
indices: invalidIndices.join(", "),
message: "errorAccountsNotFound",
});
}

const configDir = path.join(process.cwd(), "configs", "auth");

try {
// Pre-calculate valid files to archive
const filesToArchive = [];
for (const idx of validIndices) {
const filePath = path.join(configDir, `auth-${idx}.json`);
if (fs.existsSync(filePath)) {
filesToArchive.push({ filePath, name: `auth-${idx}.json` });
}
}

const actualFileCount = filesToArchive.length;

// Set response headers for ZIP download
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
const filename = `auth_batch_${timestamp}.zip`;
res.setHeader("Content-Type", "application/zip");
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
// Set header with actual file count before piping
res.setHeader("X-File-Count", actualFileCount.toString());

// Create zip archive
const archive = archiver("zip", { zlib: { level: 0 } });

// Handle archive errors
archive.on("error", err => {
this.logger.error(`[WebUI] Batch download archive error: ${err.message}`);
if (!res.headersSent) {
res.status(500).json({ error: err.message, message: "batchDownloadFailed" });
} else {
archive.abort();
res.destroy(err);
}
});

// Pipe archive to response
archive.pipe(res);

// Handle client disconnect to prevent wasted resources
res.on("close", () => {
if (!res.writableEnded) {
this.logger.warn("[WebUI] Client disconnected during batch download. Aborting archive.");
archive.abort();
}
});

// Add files to archive
for (const file of filesToArchive) {
archive.file(file.filePath, { name: file.name });
}

// Finalize archive
await archive.finalize();
this.logger.info(`[WebUI] Batch downloaded ${actualFileCount} auth files as ZIP.`);
} catch (error) {
this.logger.error(`[WebUI] Batch download failed: ${error.message}`);
if (!res.headersSent) {
res.status(500).json({ error: error.message, message: "batchDownloadFailed" });
}
}
});

app.delete("/api/accounts/:index", isAuthenticated, (req, res) => {
const rawIndex = req.params.index;
const targetIndex = Number(rawIndex);
Expand Down
14 changes: 1 addition & 13 deletions ui/app/pages/AuthPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import escapeHtml from "../utils/escapeHtml";
import I18n from "../utils/i18n";
import { useTheme } from "../utils/useTheme";

Expand Down Expand Up @@ -317,19 +318,6 @@ const ensureConnected = () => {
return true;
};

const escapeHtml = value =>
String(value).replace(
/[&<>"']/g,
char =>
({
'"': "&quot;",
"&": "&amp;",
"'": "&#x27;",
"<": "&lt;",
">": "&gt;",
})[char]
);

const goBack = () => {
if (window.history.length > 1) {
window.history.back();
Expand Down
Loading
Loading