Add default-server ntfy auth (fix 403 on protected topics)
build-and-push / docker (push) Has been cancelled

The "send test" action, admin alerts and feeds without their own
credentials now use configurable default-server token / basic auth,
so publishing works against ntfy servers with access control enabled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dimon
2026-06-02 21:47:12 +08:00
parent bf52bc3079
commit e696537fe1
7 changed files with 50 additions and 3 deletions
+12 -3
View File
@@ -95,6 +95,12 @@ async def dispatch(feed: Feed, settings: Settings, msg: Message) -> DispatchResu
server = feed.ntfy_server.strip() or settings.default_ntfy_server server = feed.ntfy_server.strip() or settings.default_ntfy_server
full_title = f"{msg.source}: {msg.title}" if msg.source else msg.title full_title = f"{msg.source}: {msg.title}" if msg.source else msg.title
# Per-feed auth wins; otherwise fall back to the default-server credentials.
has_feed_auth = bool(feed.ntfy_token.strip() or feed.ntfy_username.strip())
token = feed.ntfy_token if has_feed_auth else settings.default_ntfy_token
username = feed.ntfy_username if has_feed_auth else settings.default_ntfy_username
password = feed.ntfy_password if has_feed_auth else settings.default_ntfy_password
# --- ntfy (default channel; requires a topic) --- # --- ntfy (default channel; requires a topic) ---
if feed.ntfy_topic.strip(): if feed.ntfy_topic.strip():
try: try:
@@ -107,9 +113,9 @@ async def dispatch(feed: Feed, settings: Settings, msg: Message) -> DispatchResu
tags=feed.tags, tags=feed.tags,
priority=feed.priority, priority=feed.priority,
attach=msg.image if feed.attach_image else "", attach=msg.image if feed.attach_image else "",
token=feed.ntfy_token, token=token,
username=feed.ntfy_username, username=username,
password=feed.ntfy_password, password=password,
) )
result.channels.append("ntfy") result.channels.append("ntfy")
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
@@ -146,6 +152,9 @@ async def send_admin_alert(settings: Settings, text: str) -> None:
message=text, message=text,
tags="warning", tags="warning",
priority=4, priority=4,
token=settings.default_ntfy_token,
username=settings.default_ntfy_username,
password=settings.default_ntfy_password,
) )
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
log.warning("admin alert failed: %s", exc) log.warning("admin alert failed: %s", exc)
+11
View File
@@ -384,6 +384,9 @@ def read_settings(session: Session = Depends(get_session), _: User = Depends(req
s = get_settings(session) s = get_settings(session)
return { return {
"default_ntfy_server": s.default_ntfy_server, "default_ntfy_server": s.default_ntfy_server,
"default_ntfy_token": s.default_ntfy_token,
"default_ntfy_username": s.default_ntfy_username,
"default_ntfy_password": s.default_ntfy_password,
"check_interval": s.check_interval, "check_interval": s.check_interval,
"auth_enabled": s.auth_enabled, "auth_enabled": s.auth_enabled,
"telegram_enabled": s.telegram_enabled, "telegram_enabled": s.telegram_enabled,
@@ -410,6 +413,9 @@ def write_settings(
raise HTTPException(400, "Создайте хотя бы одного пользователя перед включением авторизации") raise HTTPException(400, "Создайте хотя бы одного пользователя перед включением авторизации")
s.default_ntfy_server = data.default_ntfy_server.strip() or "https://ntfy.sh" s.default_ntfy_server = data.default_ntfy_server.strip() or "https://ntfy.sh"
s.default_ntfy_token = data.default_ntfy_token.strip()
s.default_ntfy_username = data.default_ntfy_username.strip()
s.default_ntfy_password = data.default_ntfy_password
s.check_interval = data.check_interval s.check_interval = data.check_interval
s.auth_enabled = data.auth_enabled s.auth_enabled = data.auth_enabled
s.telegram_enabled = data.telegram_enabled s.telegram_enabled = data.telegram_enabled
@@ -520,6 +526,8 @@ async def test_notification(
server = data.server.strip() or s.default_ntfy_server server = data.server.strip() or s.default_ntfy_server
if not data.topic.strip(): if not data.topic.strip():
raise HTTPException(400, "Укажите тему") raise HTTPException(400, "Укажите тему")
# Use a custom server's own auth only if it matches the default; otherwise
# fall back to the configured default-server credentials.
try: try:
await ntfy.publish( await ntfy.publish(
server=server, server=server,
@@ -528,6 +536,9 @@ async def test_notification(
message="Тестовое уведомление — всё работает!", message="Тестовое уведомление — всё работает!",
tags="white_check_mark", tags="white_check_mark",
priority=3, priority=3,
token=s.default_ntfy_token,
username=s.default_ntfy_username,
password=s.default_ntfy_password,
) )
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
raise HTTPException(502, f"Не удалось отправить: {exc}") raise HTTPException(502, f"Не удалось отправить: {exc}")
+5
View File
@@ -90,6 +90,11 @@ class Settings(SQLModel, table=True):
id: Optional[int] = Field(default=1, primary_key=True) id: Optional[int] = Field(default=1, primary_key=True)
default_ntfy_server: str = "https://ntfy.sh" default_ntfy_server: str = "https://ntfy.sh"
# Default-server auth, used as a fallback for feeds without their own and
# for the "send test" action (for ntfy servers with access control).
default_ntfy_token: str = ""
default_ntfy_username: str = ""
default_ntfy_password: str = ""
check_interval: int = 5 # minutes (global default) check_interval: int = 5 # minutes (global default)
# Auth toggle (per-user credentials live in the User table). # Auth toggle (per-user credentials live in the User table).
+3
View File
@@ -47,6 +47,9 @@ class FeedIn(BaseModel):
class SettingsIn(BaseModel): class SettingsIn(BaseModel):
default_ntfy_server: str = "https://ntfy.sh" default_ntfy_server: str = "https://ntfy.sh"
default_ntfy_token: str = ""
default_ntfy_username: str = ""
default_ntfy_password: str = ""
check_interval: int = 5 check_interval: int = 5
auth_enabled: bool = False auth_enabled: bool = False
# Telegram # Telegram
+3
View File
@@ -410,6 +410,9 @@ sForm.addEventListener("submit", async e => {
e.preventDefault(); e.preventDefault();
const payload = { const payload = {
default_ntfy_server: sForm.default_ntfy_server.value.trim(), default_ntfy_server: sForm.default_ntfy_server.value.trim(),
default_ntfy_token: sForm.default_ntfy_token.value.trim(),
default_ntfy_username: sForm.default_ntfy_username.value.trim(),
default_ntfy_password: sForm.default_ntfy_password.value,
check_interval: parseInt(sForm.check_interval.value, 10), check_interval: parseInt(sForm.check_interval.value, 10),
auth_enabled: sForm.auth_enabled.checked, auth_enabled: sForm.auth_enabled.checked,
telegram_enabled: sForm.telegram_enabled.checked, telegram_enabled: sForm.telegram_enabled.checked,
+4
View File
@@ -46,6 +46,8 @@ const I18N = {
"settings.ntfy": "ntfy", "settings.ntfy": "ntfy",
"settings.defaultServer": "Сервер ntfy по умолчанию", "settings.defaultServer": "Сервер ntfy по умолчанию",
"settings.defaultServerHint": "Используется для лент, у которых не задан собственный сервер.", "settings.defaultServerHint": "Используется для лент, у которых не задан собственный сервер.",
"settings.ntfyAuth": "Авторизация на сервере ntfy",
"settings.ntfyAuthHint": "Нужно, если на сервере включён контроль доступа (ошибка 403 при отправке). Применяется к тесту, алертам и лентам без собственной авторизации.",
"settings.testPh": "тема для теста, напр. my-news", "settings.testPh": "тема для теста, напр. my-news",
"settings.testBtn": "Отправить тест", "settings.testBtn": "Отправить тест",
"settings.check": "Проверка", "settings.check": "Проверка",
@@ -193,6 +195,8 @@ const I18N = {
"settings.ntfy": "ntfy", "settings.ntfy": "ntfy",
"settings.defaultServer": "Default ntfy server", "settings.defaultServer": "Default ntfy server",
"settings.defaultServerHint": "Used for feeds that don't define their own server.", "settings.defaultServerHint": "Used for feeds that don't define their own server.",
"settings.ntfyAuth": "ntfy server authentication",
"settings.ntfyAuthHint": "Needed if the server has access control enabled (403 on publish). Applies to the test, alerts and feeds without their own auth.",
"settings.testPh": "topic to test, e.g. my-news", "settings.testPh": "topic to test, e.g. my-news",
"settings.testBtn": "Send test", "settings.testBtn": "Send test",
"settings.check": "Polling", "settings.check": "Polling",
+12
View File
@@ -91,6 +91,18 @@
<input type="text" name="default_ntfy_server" placeholder="https://ntfy.sh"> <input type="text" name="default_ntfy_server" placeholder="https://ntfy.sh">
<small class="muted" data-i18n="settings.defaultServerHint"></small> <small class="muted" data-i18n="settings.defaultServerHint"></small>
</label> </label>
<details class="adv">
<summary data-i18n="settings.ntfyAuth">Авторизация на сервере ntfy</summary>
<label><span data-i18n="feed.token">Access token</span> <small class="muted" data-i18n="feed.tokenHint"></small>
<input type="text" name="default_ntfy_token" placeholder="tk_..."></label>
<div class="grid-2">
<label><span data-i18n="feed.login">Логин</span>
<input type="text" name="default_ntfy_username" autocomplete="off"></label>
<label><span data-i18n="feed.password">Пароль</span>
<input type="password" name="default_ntfy_password" autocomplete="new-password"></label>
</div>
<small class="muted" data-i18n="settings.ntfyAuthHint"></small>
</details>
<div class="inline-test"> <div class="inline-test">
<input type="text" id="test-topic" data-i18n-ph="settings.testPh"> <input type="text" id="test-topic" data-i18n-ph="settings.testPh">
<button type="button" class="btn ghost" id="test-btn" data-i18n="settings.testBtn">Отправить тест</button> <button type="button" class="btn ghost" id="test-btn" data-i18n="settings.testBtn">Отправить тест</button>