Files
mc_domain_proxy/frontend/src/pages/Domains.jsx
claude-bot 58b112e449 feat: per-domain backend routing
Each allowed_domains entry can now carry its own backend {host, port}.
That lets one proxy on port 25565 serve multiple MC servers, picking
the upstream from the domain the client typed.

- proxy/main.py: ProxyState.backend_for(domain) → tuple|None,
  honors per-domain backend first, falls back to top-level backend.
  handle_client uses backend_for(); blocked / disabled domains
  return None (and still get a Login Disconnect on join attempts).
- api/routes/{config,domains}.py: DomainBackend model + optional
  backend field on create/patch. PATCH supports clear_backend=true
  to drop a per-domain override and revert to default.
- frontend/Domains.jsx: full rewrite — new-domain form has host/port
  inputs, table shows each row's effective backend, inline edit +
  reset button per row.
- frontend/Settings.jsx: backend section relabeled "기본 백엔드 (fallback)"
- README updated with multi-server example config.
2026-05-23 17:25:14 +09:00

221 lines
7.1 KiB
JavaScript

import { useEffect, useState } from 'react'
import { api } from '../api.js'
const blankBackend = { host: '', port: '' }
export default function Domains() {
const [domains, setDomains] = useState([])
const [defaultBackend, setDefaultBackend] = useState(null)
const [newDomain, setNewDomain] = useState('')
const [newNote, setNewNote] = useState('')
const [newBackend, setNewBackend] = useState({ ...blankBackend })
const [error, setError] = useState(null)
const [editing, setEditing] = useState(null) // {domain, host, port}
async function load() {
try {
const [doms, cfg] = await Promise.all([api.domains(), api.config()])
setDomains(doms)
setDefaultBackend(cfg.backend)
setError(null)
} catch (e) {
setError(e.message)
}
}
useEffect(() => { load() }, [])
async function add(e) {
e.preventDefault()
if (!newDomain.trim()) return
try {
const body = { domain: newDomain.trim(), enabled: true, note: newNote }
if (newBackend.host.trim() && newBackend.port) {
body.backend = { host: newBackend.host.trim(), port: +newBackend.port }
}
await api.addDomain(body)
setNewDomain('')
setNewNote('')
setNewBackend({ ...blankBackend })
setError(null)
await load()
} catch (e) {
setError(e.message)
}
}
async function toggle(d) {
try {
await api.patchDomain(d.domain, { enabled: !d.enabled })
await load()
} catch (e) { setError(e.message) }
}
async function remove(d) {
if (!confirm(`${d.domain} 을(를) 삭제할까요?`)) return
try {
await api.deleteDomain(d.domain)
await load()
} catch (e) { setError(e.message) }
}
function startEdit(d) {
setEditing({
domain: d.domain,
host: d.backend?.host ?? '',
port: d.backend?.port ?? '',
})
}
async function saveEdit() {
try {
const { domain, host, port } = editing
if (!host.trim() || !port) {
// 둘 다 비우면 backend 삭제 → 기본값으로 폴백
await api.patchDomain(domain, { clear_backend: true })
} else {
await api.patchDomain(domain, {
backend: { host: host.trim(), port: +port },
})
}
setEditing(null)
await load()
} catch (e) { setError(e.message) }
}
async function clearBackend(d) {
if (!confirm(`${d.domain} 의 개별 backend 설정을 지우고 기본값으로 되돌릴까요?`)) return
try {
await api.patchDomain(d.domain, { clear_backend: true })
await load()
} catch (e) { setError(e.message) }
}
return (
<div>
<h1>허용 도메인</h1>
{error && <div className="error">{error}</div>}
<form onSubmit={add} className="card">
<h2> 도메인</h2>
<div className="form-grid">
<label>
도메인
<input
placeholder="mc.example.com"
value={newDomain}
onChange={(e) => setNewDomain(e.target.value)}
required
/>
</label>
<label>
메모 (선택)
<input
placeholder="메인 서버, 모드팩 등"
value={newNote}
onChange={(e) => setNewNote(e.target.value)}
/>
</label>
<label>
백엔드 호스트 (선택)
<input
placeholder={defaultBackend ? `기본값: ${defaultBackend.host}` : ''}
value={newBackend.host}
onChange={(e) => setNewBackend({ ...newBackend, host: e.target.value })}
/>
</label>
<label>
백엔드 포트 (선택)
<input
type="number"
placeholder={defaultBackend ? `기본값: ${defaultBackend.port}` : ''}
value={newBackend.port}
onChange={(e) => setNewBackend({ ...newBackend, port: e.target.value })}
/>
</label>
</div>
<div className="actions">
<button type="submit">추가</button>
<span className="muted">백엔드 호스트/포트를 비워두면 설정 페이지의 기본 backend 라우팅됩니다.</span>
</div>
</form>
<table className="table">
<thead>
<tr>
<th>도메인</th>
<th>메모</th>
<th>백엔드</th>
<th style={{ width: 80 }}>활성</th>
<th style={{ width: 200 }}></th>
</tr>
</thead>
<tbody>
{domains.length === 0 && (
<tr>
<td colSpan={5} className="muted" style={{ textAlign: 'center', padding: 24 }}>
등록된 도메인이 없습니다.
</td>
</tr>
)}
{domains.map((d) => {
const isEditing = editing?.domain === d.domain
return (
<tr key={d.domain}>
<td><code>{d.domain}</code></td>
<td>{d.note}</td>
<td>
{isEditing ? (
<div className="inline-edit">
<input
placeholder="host"
value={editing.host}
onChange={(e) => setEditing({ ...editing, host: e.target.value })}
/>
<input
type="number"
placeholder="port"
value={editing.port}
onChange={(e) => setEditing({ ...editing, port: e.target.value })}
style={{ width: 90 }}
/>
</div>
) : d.backend ? (
<code>{d.backend.host}:{d.backend.port}</code>
) : (
<span className="muted">
기본값 ({defaultBackend ? `${defaultBackend.host}:${defaultBackend.port}` : '-'})
</span>
)}
</td>
<td>
<label className="switch">
<input type="checkbox" checked={d.enabled} onChange={() => toggle(d)} />
<span />
</label>
</td>
<td>
{isEditing ? (
<>
<button onClick={saveEdit}>저장</button>
<button className="ghost" onClick={() => setEditing(null)} style={{ marginLeft: 4 }}>취소</button>
</>
) : (
<>
<button className="ghost" onClick={() => startEdit(d)}>백엔드</button>
{d.backend && (
<button className="ghost" onClick={() => clearBackend(d)} style={{ marginLeft: 4 }}>초기화</button>
)}
<button className="danger" onClick={() => remove(d)} style={{ marginLeft: 4 }}>삭제</button>
</>
)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}