diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index a778ab1..c82a621 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -137,13 +137,28 @@ jobs: with: fetch-depth: 0 # Fetch all history for changelog generation + - name: Check if release exists + id: check_release + run: | + CURRENT_TAG=${GITHUB_REF#refs/tags/} + if gh release view "$CURRENT_TAG" &>/dev/null; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "Release $CURRENT_TAG already exists, skipping creation" + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Get previous tag + if: steps.check_release.outputs.exists == 'false' id: prev_tag run: | PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") echo "tag=${PREV_TAG}" >> $GITHUB_OUTPUT - name: Generate changelog + if: steps.check_release.outputs.exists == 'false' id: changelog run: | CURRENT_TAG=${GITHUB_REF#refs/tags/} @@ -172,6 +187,7 @@ jobs: echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${CURRENT_TAG}" >> changelog.md - name: Create GitHub Release + if: steps.check_release.outputs.exists == 'false' uses: softprops/action-gh-release@v2 with: body_path: changelog.md diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index 9216057..e76c05e 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -482,10 +482,33 @@ export async function sendShoutrrrNotification( ) .trim(); - // Determine notification type based on validation result and URL pattern - const isNtfyUrl = isNtfy || sanitizedUrl.includes("ntfy.sh") || sanitizedUrl.includes("/ntfy/"); + // Determine notification type based on URL hostname + // Use JSON format only for known webhook services that require it + // Use proper URL parsing to prevent bypass attacks (e.g., evil.com?hooks.slack.com) + let isJsonWebhook = false; + try { + const parsedUrl = new URL(sanitizedUrl); + const hostname = parsedUrl.hostname.toLowerCase(); + const pathname = parsedUrl.pathname.toLowerCase(); - if (isNtfyUrl) { + isJsonWebhook = + // Discord webhooks + ((hostname === "discord.com" || hostname === "discordapp.com") && pathname.startsWith("/api/webhooks")) || + // Slack webhooks + hostname === "hooks.slack.com" || + hostname.endsWith(".hooks.slack.com") || + // Telegram API + hostname === "api.telegram.org" || + // Gotify (can be self-hosted, so check if "gotify" is in hostname) + hostname.includes("gotify"); + } catch { + // If URL parsing fails, default to ntfy-style + isJsonWebhook = false; + } + + // Default to ntfy-style (plain text with Title header) for all other HTTP URLs + // This works for ntfy, Apprise, and most simple push services + if (!isJsonWebhook) { targetUrl = sanitizedUrl; headers = { Title: cleanTitle, Tags: "pill" }; body = message; diff --git a/frontend/src/hooks/useMedications.ts b/frontend/src/hooks/useMedications.ts index 7a84913..1c782d1 100644 --- a/frontend/src/hooks/useMedications.ts +++ b/frontend/src/hooks/useMedications.ts @@ -22,7 +22,7 @@ export function useMedications(): UseMedicationsReturn { const loadMeds = useCallback(() => { setLoading(true); - fetch("/api/medications") + fetch("/api/medications", { credentials: "include" }) .then((res) => res.json()) .then((data) => setMeds(Array.isArray(data) ? data : [])) .catch(() => setMeds([])) @@ -31,7 +31,7 @@ export function useMedications(): UseMedicationsReturn { const deleteMed = useCallback( async (id: number, editingId: number | null, resetForm: () => void) => { - await fetch(`/api/medications/${id}`, { method: "DELETE" }).catch(() => null); + await fetch(`/api/medications/${id}`, { method: "DELETE", credentials: "include" }).catch(() => null); if (editingId === id) resetForm(); loadMeds(); }, @@ -48,6 +48,7 @@ export function useMedications(): UseMedicationsReturn { const res = await fetch(`/api/medications/${medId}/image`, { method: "POST", body: formData, + credentials: "include", }); if (res.ok) { loadMeds(); @@ -62,7 +63,7 @@ export function useMedications(): UseMedicationsReturn { const deleteMedImage = useCallback( async (medId: number) => { - await fetch(`/api/medications/${medId}/image`, { method: "DELETE" }).catch(() => null); + await fetch(`/api/medications/${medId}/image`, { method: "DELETE", credentials: "include" }).catch(() => null); loadMeds(); }, [loadMeds] diff --git a/frontend/src/hooks/useSettings.ts b/frontend/src/hooks/useSettings.ts index 0e95af0..748e166 100644 --- a/frontend/src/hooks/useSettings.ts +++ b/frontend/src/hooks/useSettings.ts @@ -210,6 +210,7 @@ export function useSettings(): UseSettingsReturn { await fetch("/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, + credentials: "include", body: JSON.stringify(payload), }).catch(() => null); @@ -233,6 +234,7 @@ export function useSettings(): UseSettingsReturn { const res = await fetch("/api/settings/test-email", { method: "POST", headers: { "Content-Type": "application/json" }, + credentials: "include", body: JSON.stringify({ email: settings.notificationEmail }), }); const data = await res.json(); @@ -254,6 +256,7 @@ export function useSettings(): UseSettingsReturn { const res = await fetch("/api/settings/test-shoutrrr", { method: "POST", headers: { "Content-Type": "application/json" }, + credentials: "include", body: JSON.stringify({ url: settings.shoutrrrUrl }), }); const data = await res.json(); diff --git a/frontend/src/hooks/useShare.ts b/frontend/src/hooks/useShare.ts index 6be1db1..91df596 100644 --- a/frontend/src/hooks/useShare.ts +++ b/frontend/src/hooks/useShare.ts @@ -59,6 +59,7 @@ export function useShare(): UseShareReturn { const res = await fetch("/api/share", { method: "POST", headers: { "Content-Type": "application/json" }, + credentials: "include", body: JSON.stringify({ takenBy: shareSelectedPerson, scheduleDays: shareSelectedDays, diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index ceb8e30..a5afe92 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -217,6 +217,7 @@ export function DashboardPage() { const res = await fetch("/api/reminder/send-email", { method: "POST", headers: { "Content-Type": "application/json" }, + credentials: "include", body: JSON.stringify({ email: settings.notificationEmail, lowStock: coverage.low, diff --git a/frontend/src/pages/MedicationsPage.tsx b/frontend/src/pages/MedicationsPage.tsx index b664b5c..11125a5 100644 --- a/frontend/src/pages/MedicationsPage.tsx +++ b/frontend/src/pages/MedicationsPage.tsx @@ -192,6 +192,7 @@ export function MedicationsPage() { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), + credentials: "include", }); if (!res.ok) { @@ -308,7 +309,7 @@ export function MedicationsPage() {
{meds.map((med) => ( -
+
@@ -358,7 +359,20 @@ export function MedicationsPage() {
-

{editingId ? t("form.editEntry") : t("form.newEntry")}

+ {editingId ? ( +
+ m.id === editingId)?.name || ""} + imageUrl={meds.find((m) => m.id === editingId)?.imageUrl} + size="md" + /> +

+ {t("form.editEntry")}: {meds.find((m) => m.id === editingId)?.name} +

+
+ ) : ( +

{t("form.newEntry")}

+ )}