실제 환경 배포 결함 수정 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; },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* portal/mail/model.mjs — 메일 영역 세부 표(§15~§18, §23.2). 중심 표(mail_services·mailboxes·domains)는 portal/core/model.mjs 에 있다.
|
||||
*
|
||||
* mail_servers 운영자가 등록한 AriMail 서버(공개 주소·내부 주소·전체 설정 주소·메일 프로그램 접속 호스트). 비밀(설정 토큰·사용량 키·SSO 비밀)은 보관소 'arimail:<id>:*'.
|
||||
* mail_servers 운영자가 등록한 AriMail 서버(공개 주소·내부 주소·전체 설정 주소·설정 API Host(setup_host: AriMail 전체 설정 API 는 기본 도메인 호스트에서만 응답한다 — 실제 환경 결함 수정)·메일 프로그램 접속 호스트). 비밀(설정 토큰·사용량 키·SSO 비밀)은 보관소 'arimail:<id>:*'.
|
||||
* mail_tests 실제 외부 송신·수신·회신 확인 기록(활성화·전환 전후·수동). 단계와 결과만, 메일 내용은 없음.
|
||||
* mail_invites 메일함 초대(초기 비밀번호 대신 링크로 비밀번호를 정함): 토큰 해시·만료·사용 완료·재발송 횟수.
|
||||
* mail_aliases 별칭·전달 전용 주소 기록(정책 미확정이면 생성 불가, §16.2).
|
||||
@@ -36,4 +36,4 @@ CREATE TABLE IF NOT EXISTS mail_usage_log (
|
||||
id TEXT PRIMARY KEY, mail_service_id TEXT NOT NULL, group_id TEXT NOT NULL, subject_type TEXT NOT NULL, subject_id TEXT NOT NULL, storage_bytes INTEGER, monthly_messages INTEGER, mailboxes INTEGER, warn_level INTEGER, limited_json TEXT, measured_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mail_usage_log_subject ON mail_usage_log(subject_type, subject_id, measured_at);
|
||||
` }];
|
||||
` }, { id: 'mail_0002_setup_host', sql: `ALTER TABLE mail_servers ADD COLUMN setup_host TEXT;` }];
|
||||
|
||||
Reference in New Issue
Block a user