실제 환경 배포 결함 수정 4: AriMail 설정 API Host(setup_host), Agent heartbeat 의 REVISION 버전 보고, 새 유닛 enable
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0862f6fae9
commit
23189f4dfb
+15
-13
@@ -38,6 +38,8 @@ export function create(deps) {
|
||||
const list = await api.domains(s.id); const d = list.find((x) => x.domain === domain); const host = d ? String(d.hosts[0]).split('/')[0] : `mail.${domain}`;
|
||||
hostCache.set(s.id + '|' + domain, { host, at: Date.now() }); return host;
|
||||
}
|
||||
/** 전체 설정 API 호출: AriMail 은 기본 도메인 호스트(setup_host, 예: mail.arihakdang.com)에서만 설정 API 를 응답하므로 URL 은 setup_url(인증서 이름·내부 주소)로 두고 Host 헤더를 따로 보낸다. */
|
||||
const setupCall = (s, path, opts = {}) => call(fetchImpl, setupOf(s) + path, { ...opts, host: s.setup_host || opts.host || null, headers: { ...setupHeaders(s), ...(opts.headers || {}) } });
|
||||
const setupHeaders = (s) => { const tok = vault.get(secretName(s.id, 'setup-token')); if (!tok) { const e = new Error('AriMail 전체 설정 토큰이 보관소에 없습니다'); e.code = 'mail_server_token_missing'; throw e; } return { 'X-Setup-Token': tok }; };
|
||||
/** 관리자 세션 쿠키(도메인별). 보관소의 관리자 로그인으로 로그인해 10분 캐시. */
|
||||
const adminCache = new Map();
|
||||
@@ -57,12 +59,12 @@ export function create(deps) {
|
||||
defaultServer() { return serverRow(null); },
|
||||
getServer: (id) => serverRow(id),
|
||||
/** 서버 등록·수정. secrets 는 보관소에만 저장하고 응답에 돌려주지 않는다. */
|
||||
saveServer({ id = null, name, baseUrl, internalUrl = null, setupUrl = null, mailHost = null, isDefault = false, secrets = {} }) {
|
||||
saveServer({ id = null, name, baseUrl, internalUrl = null, setupUrl = null, setupHost = null, mailHost = null, isDefault = false, secrets = {} }) {
|
||||
const now = nowIso(); const sid = id || newId('ms_');
|
||||
if (!/^https?:\/\//.test(String(baseUrl || ''))) throw Object.assign(new Error('공개 주소는 http(s):// 로 시작해야 합니다'), { code: 'validation', status: 422 });
|
||||
if (isDefault) store.run('UPDATE mail_servers SET is_default=0');
|
||||
if (id) store.run('UPDATE mail_servers SET name=?, base_url=?, internal_url=?, setup_url=?, mail_host=?, is_default=?, updated_at=? WHERE id=?', name, baseUrl, internalUrl, setupUrl, mailHost, isDefault ? 1 : 0, now, id);
|
||||
else store.run('INSERT INTO mail_servers(id, name, base_url, internal_url, setup_url, mail_host, is_default, created_at, updated_at) VALUES(?,?,?,?,?,?,?,?,?)', sid, name, baseUrl, internalUrl, setupUrl, mailHost, isDefault || !store.get('SELECT 1 FROM mail_servers') ? 1 : 0, now, now);
|
||||
if (id) store.run('UPDATE mail_servers SET name=?, base_url=?, internal_url=?, setup_url=?, setup_host=?, mail_host=?, is_default=?, updated_at=? WHERE id=?', name, baseUrl, internalUrl, setupUrl, setupHost, mailHost, isDefault ? 1 : 0, now, id);
|
||||
else store.run('INSERT INTO mail_servers(id, name, base_url, internal_url, setup_url, setup_host, mail_host, is_default, created_at, updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)', sid, name, baseUrl, internalUrl, setupUrl, setupHost, mailHost, isDefault || !store.get('SELECT 1 FROM mail_servers') ? 1 : 0, now, now);
|
||||
for (const [k, v] of Object.entries(secrets)) if (v) vault.set(secretName(sid, k), typeof v === 'string' ? v : JSON.stringify(v), { kind: 'arimail' });
|
||||
return api.listServers().find((s) => s.id === sid);
|
||||
},
|
||||
@@ -80,25 +82,25 @@ export function create(deps) {
|
||||
return r.json;
|
||||
},
|
||||
// ---- 전체 설정 API(도메인 등록부)
|
||||
async domains(serverId) { const s = serverRow(serverId); return (await call(fetchImpl, setupOf(s) + '/api/setup/domains', { headers: setupHeaders(s) })).json.domains; },
|
||||
async domains(serverId) { const s = serverRow(serverId); return (await setupCall(s, '/api/setup/domains', { headers: setupHeaders(s) })).json.domains; },
|
||||
async addDomain(serverId, { domain, hosts = null, mailbox = 'admin', adminPassword, ariweb = null, operating = 'active', note = '' }) {
|
||||
const s = serverRow(serverId); const pw = adminPassword || randomBytes(12).toString('base64url');
|
||||
const r = await call(fetchImpl, setupOf(s) + '/api/setup/domains', { method: 'POST', headers: setupHeaders(s), body: { domain, hosts: hosts || [`mail.${domain}`], mailbox, adminPassword: pw, ariweb, operating, note, copyFromPrimary: true } });
|
||||
const r = await setupCall(s, '/api/setup/domains', { method: 'POST', headers: setupHeaders(s), body: { domain, hosts: hosts || [`mail.${domain}`], mailbox, adminPassword: pw, ariweb, operating, note, copyFromPrimary: true } });
|
||||
hostCache.delete(s.id + '|' + domain);
|
||||
if (r.json.initialAdmin) api.storeAdmin(s.id, domain, r.json.initialAdmin.login, pw);
|
||||
return { entry: r.json.domain, initialAdmin: r.json.initialAdmin ? { login: r.json.initialAdmin.login } : null };
|
||||
},
|
||||
async updateDomain(serverId, domain, patch) { const s = serverRow(serverId); hostCache.delete(s.id + '|' + domain); return (await call(fetchImpl, setupOf(s) + '/api/setup/domains/' + encodeURIComponent(domain), { method: 'PATCH', headers: setupHeaders(s), body: patch })).json.domain; },
|
||||
async removeDomain(serverId, domain) { const s = serverRow(serverId); hostCache.delete(s.id + '|' + domain); return (await call(fetchImpl, setupOf(s) + '/api/setup/domains/' + encodeURIComponent(domain), { method: 'DELETE', headers: setupHeaders(s) })).json.removed; },
|
||||
async updateDomain(serverId, domain, patch) { const s = serverRow(serverId); hostCache.delete(s.id + '|' + domain); return (await setupCall(s, '/api/setup/domains/' + encodeURIComponent(domain), { method: 'PATCH', headers: setupHeaders(s), body: patch })).json.domain; },
|
||||
async removeDomain(serverId, domain) { const s = serverRow(serverId); hostCache.delete(s.id + '|' + domain); return (await setupCall(s, '/api/setup/domains/' + encodeURIComponent(domain), { method: 'DELETE', headers: setupHeaders(s) })).json.removed; },
|
||||
/** 도메인 설정(비밀 가림)·메타(DNS 레코드·발신 상태·호스트). */
|
||||
async domainConfig(serverId, domain) { const s = serverRow(serverId); return (await call(fetchImpl, `${setupOf(s)}/api/setup/config?domain=${encodeURIComponent(domain)}`, { headers: setupHeaders(s) })).json; },
|
||||
async setDomainConfig(serverId, domain, values) { const s = serverRow(serverId); return (await call(fetchImpl, `${setupOf(s)}/api/setup/config?domain=${encodeURIComponent(domain)}`, { method: 'PUT', headers: setupHeaders(s), body: { values } })).json; },
|
||||
async domainConfig(serverId, domain) { const s = serverRow(serverId); return (await setupCall(s, `/api/setup/config?domain=${encodeURIComponent(domain)}`, { headers: setupHeaders(s) })).json; },
|
||||
async setDomainConfig(serverId, domain, values) { const s = serverRow(serverId); return (await setupCall(s, `/api/setup/config?domain=${encodeURIComponent(domain)}`, { method: 'PUT', headers: setupHeaders(s), body: { values } })).json; },
|
||||
/** 도메인별 보장량(§13.3): 메일함 수·저장 공간·월간 통수를 AriMail 설정으로 */
|
||||
setDomainLimits(serverId, domain, { mailboxes, storageBytes, monthlyMessages }) { return api.setDomainConfig(serverId, domain, { LIMIT_ACCOUNTS: mailboxes ?? '', LIMIT_TOTAL_BYTES: storageBytes != null ? String(storageBytes) : '', LIMIT_MONTHLY_MESSAGES: monthlyMessages ?? '' }); },
|
||||
async plan(serverId, domain) { const s = serverRow(serverId); return (await call(fetchImpl, `${setupOf(s)}/api/setup/plan?domain=${encodeURIComponent(domain)}`, { headers: setupHeaders(s) })).json.plan; },
|
||||
async runPlan(serverId, domain, body = {}) { const s = serverRow(serverId); return (await call(fetchImpl, `${setupOf(s)}/api/setup/plan/run?domain=${encodeURIComponent(domain)}`, { method: 'POST', headers: setupHeaders(s), body })).json; },
|
||||
async dnsProbe(serverId, domain) { const s = serverRow(serverId); return (await call(fetchImpl, `${setupOf(s)}/api/setup/dns/probe?domain=${encodeURIComponent(domain)}`, { method: 'POST', headers: setupHeaders(s), body: {} })).json.probe; },
|
||||
async outboundCheck(serverId, domain) { const s = serverRow(serverId); return (await call(fetchImpl, `${setupOf(s)}/api/setup/check?domain=${encodeURIComponent(domain)}`, { method: 'POST', headers: setupHeaders(s), body: { what: 'outbound' } })).json.outboundStatus; },
|
||||
async plan(serverId, domain) { const s = serverRow(serverId); return (await setupCall(s, `/api/setup/plan?domain=${encodeURIComponent(domain)}`, { headers: setupHeaders(s) })).json.plan; },
|
||||
async runPlan(serverId, domain, body = {}) { const s = serverRow(serverId); return (await setupCall(s, `/api/setup/plan/run?domain=${encodeURIComponent(domain)}`, { method: 'POST', headers: setupHeaders(s), body })).json; },
|
||||
async dnsProbe(serverId, domain) { const s = serverRow(serverId); return (await setupCall(s, `/api/setup/dns/probe?domain=${encodeURIComponent(domain)}`, { method: 'POST', headers: setupHeaders(s), body: {} })).json.probe; },
|
||||
async outboundCheck(serverId, domain) { const s = serverRow(serverId); return (await setupCall(s, `/api/setup/check?domain=${encodeURIComponent(domain)}`, { method: 'POST', headers: setupHeaders(s), body: { what: 'outbound' } })).json.outboundStatus; },
|
||||
// ---- 관리자 API(메일함)
|
||||
async listUsers(serverId, domain) { const s = serverRow(serverId); return (await admin(s, domain, 'admin/users')).json; },
|
||||
async createUser(serverId, domain, { login, name = '', password, quotaBytes = undefined, services = undefined, role = 'user' }) { const s = serverRow(serverId); return (await admin(s, domain, 'admin/users', { method: 'POST', body: { login, name, password, role, ...(quotaBytes !== undefined ? { quotaBytes } : {}), ...(services ? { services } : {}) } })).json; },
|
||||
|
||||
Reference in New Issue
Block a user