Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
34 lines
5.3 KiB
JavaScript
34 lines
5.3 KiB
JavaScript
/**
|
|
* portal/domains/api.mjs — 도메인 라우트. 화면 /domains, /domains/connect, /domains/:id · API /api/domains(GET,POST) /api/domains/:id(GET,PATCH,DELETE) /records /check(POST) /primary(POST) /jobs/:jobId/:action
|
|
* 운영자: /internal/ops/domains (전체 도메인·반복 실패·만료). 권한: 도메인 변경은 사이트 권한 domain_mail(그룹 admin 이상 포함).
|
|
*/
|
|
import { requireGroup, requireSitePerm, requireRole } from '../auth/guard.mjs';
|
|
import { HttpError } from '../../lib/http.mjs';
|
|
import { verifyCtx } from '../../lib/serviceauth.mjs';
|
|
import { describe as describeJob } from '../../lib/jobs.mjs';
|
|
import { createDomainsService } from './service.mjs';
|
|
import * as ui from './ui.mjs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
export async function register(app, deps) {
|
|
const domains = createDomainsService(deps); deps.domains = domains;
|
|
app.static('/ui/domains', join(HERE, 'public'));
|
|
const domainOf = (ctx, perm = 'view') => { requireGroup(ctx); const d = domains.getForGroup(ctx.group.id, ctx.params.id); if (!d) throw new HttpError(404, 'not_found', '도메인을 찾을 수 없습니다'); if (perm === 'view') return d; if (d.site_id) { const s = deps.store.get('SELECT * FROM sites WHERE id=?', d.site_id); if (s) requireSitePerm(ctx, s, perm); } else requireRole(ctx, 'admin'); return d; };
|
|
app.get('/domains', (ctx) => { requireGroup(ctx); return ctx.html(ui.listPage(ctx, deps)); });
|
|
app.get('/domains/connect', (ctx) => { requireGroup(ctx); return ctx.html(ui.connectPage(ctx, deps, { siteId: ctx.query.site || '' })); });
|
|
app.get('/domains/:id', (ctx) => ctx.html(ui.detailPage(ctx, deps, domainOf(ctx))));
|
|
app.get('/api/domains', (ctx) => { requireGroup(ctx); return { ok: true, domains: domains.list(ctx.group.id, { siteId: ctx.query.site || null, mailServiceId: ctx.query.mail || null }) }; });
|
|
app.post('/api/domains', async (ctx) => { requireGroup(ctx); const b = await ctx.body(); if (b.siteId) { const s = deps.store.get('SELECT * FROM sites WHERE id=?', b.siteId); if (!s || s.group_id !== ctx.group.id) throw new HttpError(404, 'not_found', '사이트를 찾을 수 없습니다'); requireSitePerm(ctx, s, 'domain_mail'); } else requireRole(ctx, 'member'); const d = domains.add({ group: ctx.group, user: ctx.user, ip: ctx.ip }, b); const job = b.startCheck !== false ? domains.startCheck(d, ctx.user, { makePrimary: !!b.makePrimary }) : null; return { ok: true, domain: domains.detail(domains.get(d.id)), records: await domains.requiredRecords(d), job }; });
|
|
app.get('/api/domains/:id', async (ctx) => { const d = domainOf(ctx); return { ok: true, domain: domains.detail(d), records: await domains.requiredRecords(d) }; });
|
|
app.get('/api/domains/:id/records', async (ctx) => ({ ok: true, records: await domains.requiredRecords(domainOf(ctx)) }));
|
|
app.patch('/api/domains/:id', async (ctx) => { const d = domainOf(ctx, 'domain_mail'); return { ok: true, domain: domains.detail(domains.update(d, await ctx.body(), ctx.user)) }; });
|
|
app.delete('/api/domains/:id', (ctx) => { const d = domainOf(ctx, 'domain_mail'); return { ok: true, ...domains.release(d, ctx.user) }; });
|
|
app.post('/api/domains/:id/check', async (ctx) => { const d = domainOf(ctx, 'domain_mail'); const b = await ctx.body(); return { ok: true, job: domains.startCheck(d, ctx.user, { makePrimary: !!b.makePrimary }) }; });
|
|
app.post('/api/domains/:id/primary', async (ctx) => { const d = domainOf(ctx, 'domain_mail'); const b = await ctx.body(); return { ok: true, domain: domains.detail(domains.setPrimary(d, { redirectOld: !!b.redirectOld, actor: ctx.user })) }; });
|
|
app.post('/api/domains/:id/jobs/:jobId/:action', async (ctx) => { const d = domainOf(ctx, 'domain_mail'); const j = deps.jobs.get(ctx.params.jobId); if (!j || j.subject_id !== d.id) throw new HttpError(404, 'not_found', '작업을 찾을 수 없습니다'); const a = ctx.params.action; const r = a === 'retry' ? deps.jobs.retry(j.id, ctx.user.id) : a === 'cancel' ? deps.jobs.cancel(j.id, ctx.user.id) : a === 'resume' ? deps.jobs.resume(j.id, ctx.user.id) : null; if (!r) throw new HttpError(400, 'bad_action', '알 수 없는 동작'); return { ok: true, job: describeJob(r) }; });
|
|
const secretOf = (id) => deps.vault.get('service:' + id);
|
|
app.get('/internal/ops/domains', async (ctx) => { const v = await verifyCtx(ctx, (id) => (['hal', 'portal'].includes(id) ? secretOf(id) : null)); if (!v.ok) throw new HttpError(401, 'service_auth_failed', v.reason); const rows = deps.store.all('SELECT * FROM domains ORDER BY updated_at DESC LIMIT 1000'); const soon = new Date(Date.now() + 14 * 86400000).toISOString(); return { ok: true, domains: rows.map((r) => ({ ...domains.summarize(domains.get(r.id)), groupId: r.group_id })), expiring: rows.filter((r) => r.cert_expires_at && r.cert_expires_at < soon).map((r) => ({ id: r.id, name: r.name, expiresAt: r.cert_expires_at })), repeatedFailures: rows.map((r) => domains.summarize(domains.get(r.id))).filter((s) => s.recentFailures >= 3) }; });
|
|
app.post('/internal/ops/domains/sweep', async (ctx) => { const v = await verifyCtx(ctx, (id) => (['hal', 'portal'].includes(id) ? secretOf(id) : null)); if (!v.ok) throw new HttpError(401, 'service_auth_failed', v.reason); return { ok: true, ...domains.sweep() }; });
|
|
}
|