Hide the brand from unauthenticated requests
GET /ui/brand answered without auth with {"brand":"Forgetting",…}: one
request was enough for a scanner to tell a found port is this panel. It now
requires the panel JWT (Go and the Node reference alike). The login screen
says just «Вход», the page title is neutral until login, and the wordmark
loads after sign-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
This commit is contained in:
@@ -187,7 +187,7 @@ TOKEN=$(curl -s -X POST http://HOST:PORT/login \
|
|||||||
|
|
||||||
| Метод | Ответ |
|
| Метод | Ответ |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `GET /ui/brand` | `{ "brand": "Forgetting", "channel": "Beta", "version": "0.4.7" }` — **без авторизации**, панель берёт отсюда вордмарк на экране логина |
|
| `GET /ui/brand` | `{ "brand": "Forgetting", "channel": "Beta", "version": "0.4.7" }` — требует JWT панели (без авторизации выдавал бы панель сканерам). Вордмарк в шапке после входа |
|
||||||
| `GET /health` | `{ "status", "server", "ip", "gen", "awg": { "status", "peers", "module", "tools" } }`. `200`, если интерфейс поднят, иначе `503` и `status: "degraded"` |
|
| `GET /health` | `{ "status", "server", "ip", "gen", "awg": { "status", "peers", "module", "tools" } }`. `200`, если интерфейс поднят, иначе `503` и `status: "degraded"` |
|
||||||
| `GET /awg/status` | `{ "up": true, "peers": 3, "publicKey": "…" }` |
|
| `GET /awg/status` | `{ "up": true, "peers": 3, "publicKey": "…" }` |
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>AWG Panel</title>
|
<title>Вход</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+3
-3
@@ -206,9 +206,9 @@ app.post("/logout", requireAuth, (req: Request, res: Response) => {
|
|||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Единственный роут без авторизации: вордмарк нужен уже на экране логина,
|
// За авторизацией: без неё сканер одним GET узнавал бы, что на порту панель Forgetting.
|
||||||
// до выдачи JWT. Наружу уходит только имя продукта и версия.
|
// Экран логина обходится без вордмарка.
|
||||||
app.get("/ui/brand", (_req: Request, res: Response) => {
|
app.get("/ui/brand", requireAuth, (_req: Request, res: Response) => {
|
||||||
res.json({ brand: BRAND, channel: CHANNEL, version: VERSION });
|
res.json({ brand: BRAND, channel: CHANNEL, version: VERSION });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+8
-5
@@ -185,17 +185,20 @@ export default function App() {
|
|||||||
return () => clearInterval(t);
|
return () => clearInterval(t);
|
||||||
}, [token, loadNodes]);
|
}, [token, loadNodes]);
|
||||||
|
|
||||||
// Имя продукта и версия — с сервера, до логина (роут без авторизации).
|
// Имя продукта и версия — с сервера, только после логина: роут за авторизацией, чтобы
|
||||||
// Если не ответил, остаётся фолбэк из констант.
|
// панель не опознавалась сканером. Если не ответил, остаётся фолбэк из констант.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axios.get('/ui/brand')
|
if (!token) return;
|
||||||
|
axios.get('/ui/brand', { headers: { Authorization: `Bearer ${token}` } })
|
||||||
.then(({ data }) => setBrand({
|
.then(({ data }) => setBrand({
|
||||||
brand: data.brand || BRAND_FALLBACK,
|
brand: data.brand || BRAND_FALLBACK,
|
||||||
channel: data.channel || '',
|
channel: data.channel || '',
|
||||||
version: data.version || '',
|
version: data.version || '',
|
||||||
}))
|
}))
|
||||||
.catch(() => { /* остаётся фолбэк */ });
|
.catch(() => { /* остаётся фолбэк */ });
|
||||||
}, []);
|
}, [token]);
|
||||||
|
|
||||||
|
useEffect(() => { document.title = token ? brand.brand : 'Вход'; }, [token, brand.brand]);
|
||||||
|
|
||||||
// Применять и сохранять тему
|
// Применять и сохранять тему
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -249,7 +252,7 @@ export default function App() {
|
|||||||
{!token ? (
|
{!token ? (
|
||||||
<div className="login-screen">
|
<div className="login-screen">
|
||||||
<div className="login-card">
|
<div className="login-card">
|
||||||
<span className="logo-name">{brand.brand}</span>
|
<span className="logo-name">Вход</span>
|
||||||
<p className="login-sub">Войдите, чтобы продолжить</p>
|
<p className="login-sub">Войдите, чтобы продолжить</p>
|
||||||
<input
|
<input
|
||||||
className="field"
|
className="field"
|
||||||
|
|||||||
@@ -847,8 +847,9 @@ func (a *App) routes() *Router {
|
|||||||
|
|
||||||
app.Post("/login", a.login)
|
app.Post("/login", a.login)
|
||||||
app.Post("/logout", auth, a.logout)
|
app.Post("/logout", auth, a.logout)
|
||||||
// Единственный роут без авторизации: вордмарк нужен уже на экране логина.
|
// За авторизацией: без неё сканер одним GET узнавал бы, что на порту панель Forgetting.
|
||||||
app.Get("/ui/brand", func(c *Ctx) bool {
|
// Экран логина обходится без вордмарка.
|
||||||
|
app.Get("/ui/brand", auth, func(c *Ctx) bool {
|
||||||
return c.JSON(200, jsjson.Object{{Key: "brand", Val: a.brand}, {Key: "channel", Val: a.channel}, {Key: "version", Val: a.version}})
|
return c.JSON(200, jsjson.Object{{Key: "brand", Val: a.brand}, {Key: "channel", Val: a.channel}, {Key: "version", Val: a.version}})
|
||||||
})
|
})
|
||||||
app.Get("/ui/apikeys", auth, a.apiKeysList)
|
app.Get("/ui/apikeys", auth, a.apiKeysList)
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ try {
|
|||||||
await call("login broken json", "POST", "/login", undefined, { raw: "{" });
|
await call("login broken json", "POST", "/login", undefined, { raw: "{" });
|
||||||
const { token } = await call("login", "POST", "/login", { user: "admin", pass: "benchpass" });
|
const { token } = await call("login", "POST", "/login", { user: "admin", pass: "benchpass" });
|
||||||
const T = { token };
|
const T = { token };
|
||||||
|
await call("brand auth", "GET", "/ui/brand", undefined, T);
|
||||||
|
|
||||||
await call("apikeys no auth", "GET", "/ui/apikeys");
|
await call("apikeys no auth", "GET", "/ui/apikeys");
|
||||||
await call("apikeys bad token", "GET", "/ui/apikeys", undefined, { token: "a.b.c" });
|
await call("apikeys bad token", "GET", "/ui/apikeys", undefined, { token: "a.b.c" });
|
||||||
|
|||||||
Reference in New Issue
Block a user