feat: implement MC domain filter proxy, API, dashboard
- proxy: asyncio TCP proxy with handshake parser, domain whitelist, transparent backend tunneling, SQLite logging, mtime hot reload - api: FastAPI routes for config/domains/logs/status + restart trigger - frontend: React + Vite NPM-style dashboard (dashboard/domains/logs/settings) - nginx: reverse proxy for /api -> api:8000 and / -> frontend:3000 - docker-compose: full stack with shared data volume - replace spec mc-domain-filter.md with README.md
This commit is contained in:
115
frontend/src/pages/Domains.jsx
Normal file
115
frontend/src/pages/Domains.jsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { api } from '../api.js'
|
||||
|
||||
export default function Domains() {
|
||||
const [domains, setDomains] = useState([])
|
||||
const [newDomain, setNewDomain] = useState('')
|
||||
const [newNote, setNewNote] = useState('')
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
setDomains(await api.domains())
|
||||
setError(null)
|
||||
} catch (e) {
|
||||
setError(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [])
|
||||
|
||||
async function add(e) {
|
||||
e.preventDefault()
|
||||
if (!newDomain.trim()) return
|
||||
try {
|
||||
await api.addDomain({ domain: newDomain.trim(), enabled: true, note: newNote })
|
||||
setNewDomain('')
|
||||
setNewNote('')
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>도메인</th>
|
||||
<th>메모</th>
|
||||
<th style={{ width: 80 }}>활성</th>
|
||||
<th style={{ width: 90 }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{domains.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={4} 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>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user