fix(auth): real client IP rate-limiting + remote default-password guard

- Add custom-server.js: inject unspoofable socket IP, strip client XFF
  (wired into Docker CMD + CLI spawn + build-cli copy)
- loginLimiter: key on trusted x-9r-real-ip, TRUST_PROXY opt-in, global fallback
- Force password change on first remote login while default is in use
- Add /api/auth/reset-password (local-only) so CLI reset writes live SQLite
- CLI settings: reset via API instead of stale db.json
- Fix OAuth modals opening duplicate browser tabs on add-connection
- Add cli:pack / cli:publish scripts

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-06-08 12:10:02 +07:00
parent c572c68717
commit 7648c3412b
17 changed files with 185 additions and 44 deletions

View File

@@ -410,6 +410,14 @@ async function updateSettings(data) {
return makeRequest("PATCH", "/api/settings", data);
}
/**
* Reset dashboard password to default (clears stored hash server-side)
* @returns {Promise<Object>} { success }
*/
async function resetPassword() {
return makeRequest("POST", "/api/auth/reset-password");
}
// ============================================================================
// MODELS API
// ============================================================================
@@ -528,6 +536,7 @@ module.exports = {
// Settings
getSettings,
updateSettings,
resetPassword,
// Tunnel
getTunnelStatus,

View File

@@ -1,6 +1,3 @@
const path = require("path");
const fs = require("fs");
const os = require("os");
const api = require("../api/client");
const { confirm, pause } = require("../utils/input");
const { showStatus } = require("../utils/display");
@@ -18,13 +15,6 @@ const COLORS = {
const DEFAULT_PASSWORD = "123456";
// Resolve db.json path (matches app/src/lib/dataDir.js convention)
function getDbPath() {
return process.platform === "win32"
? path.join(process.env.APPDATA || "", "9router", "db.json")
: path.join(os.homedir(), ".9router", "db.json");
}
/**
* Show settings menu (tunnel + RTK + reset password)
* @param {Array<string>} breadcrumb - Breadcrumb path
@@ -171,18 +161,10 @@ async function toggleRtk(currentlyOn) {
}
/**
* Reset dashboard password by clearing the hash in db.json (Phase B).
* Reset dashboard password to default via server API (writes the live SQLite DB).
* After reset, user can log in with the default password "123456".
*/
async function resetPassword() {
const dbPath = getDbPath();
if (!fs.existsSync(dbPath)) {
showStatus(`db.json not found at ${dbPath}`, "error");
await pause();
return;
}
const ok = await confirm(`Reset dashboard password to default "${DEFAULT_PASSWORD}"?`);
if (!ok) {
showStatus("Cancelled", "info");
@@ -190,16 +172,11 @@ async function resetPassword() {
return;
}
try {
const raw = fs.readFileSync(dbPath, "utf-8");
const db = JSON.parse(raw);
if (db.settings && Object.prototype.hasOwnProperty.call(db.settings, "password")) {
delete db.settings.password;
}
fs.writeFileSync(dbPath, JSON.stringify(db, null, 2));
const result = await api.resetPassword();
if (result.success) {
showStatus(`Password reset. Default: ${DEFAULT_PASSWORD}`, "success");
} catch (err) {
showStatus(`Failed to reset password: ${err.message}`, "error");
} else {
showStatus(`Failed to reset password: ${result.error}`, "error");
}
await pause();
}