W
File size: 3,072 Bytes
2b64d42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
 * Outbound proxy configuration manager.
 * Supports per-account and global HTTP proxy settings.
 */

import { readFileSync, existsSync } from 'fs';
import { writeJsonAtomic } from '../fs-atomic.js';
import { join } from 'path';
import { config, log } from '../config.js';

const PROXY_FILE = join(config.dataDir, 'proxy.json');

const _config = {
  global: null,       // { type, host, port, username, password }
  perAccount: {},     // { accountId: { type, host, port, username, password } }
};

// Load
try {
  if (existsSync(PROXY_FILE)) {
    Object.assign(_config, JSON.parse(readFileSync(PROXY_FILE, 'utf-8')));
  }
} catch (e) {
  log.error('Failed to load proxy.json:', e.message);
}

function save() {
  try {
    writeJsonAtomic(PROXY_FILE, _config);
  } catch (e) {
    log.error('Failed to save proxy.json:', e.message);
  }
}

// Passwords never leave the server. The masked view returns
// `hasPassword: boolean` in place of the plaintext. When the dashboard
// PUTs a config back it omits the `password` key if the user didn't
// retype it, which mergePassword() treats as "keep the stored value".
// An explicit empty string still clears the password.
function maskProxy(p) {
  if (!p) return p;
  const { password, ...rest } = p;
  return { ...rest, hasPassword: !!password };
}

function mergePassword(newCfg, oldCfg) {
  if (!newCfg || !Object.prototype.hasOwnProperty.call(newCfg, 'password')) {
    return oldCfg?.password || '';
  }
  return newCfg.password || '';
}

/** Full config including plaintext passwords — internal callers only. */
export function getProxyConfig() {
  return { ..._config };
}

/** Safe shape for dashboard / API consumers. */
export function getProxyConfigMasked() {
  return {
    global: maskProxy(_config.global),
    perAccount: Object.fromEntries(
      Object.entries(_config.perAccount).map(([k, v]) => [k, maskProxy(v)])
    ),
  };
}

export function setGlobalProxy(cfg) {
  _config.global = cfg && cfg.host ? {
    type: cfg.type || 'http',
    host: String(cfg.host).trim(),
    port: parseInt(cfg.port, 10) || 8080,
    username: cfg.username || '',
    password: mergePassword(cfg, _config.global),
  } : null;
  save();
}

export function setAccountProxy(accountId, cfg) {
  if (cfg && cfg.host) {
    _config.perAccount[accountId] = {
      type: cfg.type || 'http',
      host: String(cfg.host).trim(),
      port: parseInt(cfg.port, 10) || 8080,
      username: cfg.username || '',
      password: mergePassword(cfg, _config.perAccount[accountId]),
    };
  } else {
    delete _config.perAccount[accountId];
  }
  save();
}

export function removeProxy(scope, accountId) {
  if (scope === 'global') {
    _config.global = null;
  } else if (scope === 'account' && accountId) {
    delete _config.perAccount[accountId];
  }
  save();
}

/**
 * Get effective proxy for an account (per-account takes priority over global).
 */
export function getEffectiveProxy(accountId) {
  if (accountId && _config.perAccount[accountId]) {
    return _config.perAccount[accountId];
  }
  return _config.global;
}