Alerts: configuration, test and search entries are a superuser's; subscribing a browser stays open

Any signed-in account could save an ntfy channel with its own `server` and a
`{"$secret": ...}` token and press Test, which sent the stored secret to that
host. The config GET/PUT, the test route and the global search's alert entries
now answer to superusers, like /secrets. The webpush key and subscribe routes
stay open to every account, and the Alerts page shows a non-superuser only the
button for its own browser. The events table in the docs gains run_finished.
This commit is contained in:
2026-10-28 12:40:44 +01:00
parent 29f4d93c2a
commit 2dc20911a3
8 changed files with 153 additions and 14 deletions
+2 -1
View File
@@ -2975,7 +2975,8 @@ export class SearchService {
* instance of any ordinary size, so one fetch when the panel opens beats a
* round trip per keystroke — and the client already has a matcher.
*
* Secrets are named only to a superuser, which is who ``/secrets`` answers to.
* Secrets and alert channels are named only to a superuser, which is who
* ``/secrets`` and ``/alerts`` answer to.
* @returns SearchEntry Successful Response
* @throws ApiError
*/
+21 -2
View File
@@ -17,6 +17,7 @@ import {
SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import useAuth from "@/hooks/useAuth"
import useCustomToast from "@/hooks/useCustomToast"
import * as webpush from "@/lib/webpush"
import { handleError } from "@/utils"
@@ -152,9 +153,14 @@ function ThisBrowser() {
}
function Alerts() {
// Channels and rules are a superuser's: `/alerts/config` answers to them.
// Every account can still subscribe the browser in front of it.
const { user } = useAuth()
const mayConfigure = Boolean(user?.is_superuser)
const { data } = useQuery({
queryKey: alertsKey,
queryFn: () => AlertsService.readAlertsConfig(),
enabled: mayConfigure,
})
return (
@@ -162,10 +168,23 @@ function Alerts() {
<div className="grid gap-1">
<h1 className="text-2xl">Alerts</h1>
<p className="text-sm text-muted-foreground">
Where the engine goes when something breaks.
{mayConfigure
? "Where the engine goes when something breaks."
: "Hear from the engine on this device when something breaks."}
</p>
</div>
{data ? <AlertsForm initial={data} /> : null}
{mayConfigure ? (
data ? (
<AlertsForm initial={data} />
) : null
) : user ? (
<div
className="rounded-lg border border-border bg-card p-4 shadow-e1"
data-testid="this-browser"
>
<ThisBrowser />
</div>
) : null}
</div>
)
}