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.
This commit is contained in:
@@ -1,32 +1,42 @@
|
||||
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 {
|
||||
setDomains(await api.domains())
|
||||
const [doms, cfg] = await Promise.all([api.domains(), api.config()])
|
||||
setDomains(doms)
|
||||
setDefaultBackend(cfg.backend)
|
||||
setError(null)
|
||||
} catch (e) {
|
||||
setError(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [])
|
||||
useEffect(() => { load() }, [])
|
||||
|
||||
async function add(e) {
|
||||
e.preventDefault()
|
||||
if (!newDomain.trim()) return
|
||||
try {
|
||||
await api.addDomain({ domain: newDomain.trim(), enabled: true, note: newNote })
|
||||
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) {
|
||||
@@ -38,9 +48,7 @@ export default function Domains() {
|
||||
try {
|
||||
await api.patchDomain(d.domain, { enabled: !d.enabled })
|
||||
await load()
|
||||
} catch (e) {
|
||||
setError(e.message)
|
||||
}
|
||||
} catch (e) { setError(e.message) }
|
||||
}
|
||||
|
||||
async function remove(d) {
|
||||
@@ -48,66 +56,163 @@ export default function Domains() {
|
||||
try {
|
||||
await api.deleteDomain(d.domain)
|
||||
await load()
|
||||
} catch (e) {
|
||||
setError(e.message)
|
||||
}
|
||||
} 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="form-row card">
|
||||
<input
|
||||
placeholder="mc.example.com"
|
||||
value={newDomain}
|
||||
onChange={(e) => setNewDomain(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
placeholder="메모 (선택)"
|
||||
value={newNote}
|
||||
onChange={(e) => setNewNote(e.target.value)}
|
||||
/>
|
||||
<button type="submit">추가</button>
|
||||
|
||||
<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: 90 }}></th>
|
||||
<th style={{ width: 200 }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{domains.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={4} className="muted" style={{ textAlign: 'center', padding: 24 }}>
|
||||
<td colSpan={5} className="muted" style={{ textAlign: 'center', padding: 24 }}>
|
||||
등록된 도메인이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{domains.map((d) => (
|
||||
<tr key={d.domain}>
|
||||
<td>
|
||||
<code>{d.domain}</code>
|
||||
</td>
|
||||
<td>{d.note}</td>
|
||||
<td>
|
||||
<label className="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={d.enabled}
|
||||
onChange={() => toggle(d)}
|
||||
/>
|
||||
<span />
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<button className="danger" onClick={() => remove(d)}>삭제</button>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user