mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
fix(api): narrow swallowed database errors and redact the incident URL
The incident forwarder stripped the query string but not path-segment secrets from the reported URL; route it through the same redaction the message and stack already use. Several delete-and-swallow paths caught every error as the expected unique/absent-row case, masking real failures on security-notification, logout, and integration-disconnect paths and the Telegram reply-consume guard; narrow each to the specific Prisma code and let real failures surface.
This commit is contained in:
1 parent
9aa0a3597a
commit
336ce5aef3
10 files changed
+136
-22
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { isP2025 } from "@/lib/prisma-errors";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { apiSuccess, apiError, safeJson } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
@@ -71,9 +72,15 @@ export const DELETE = apiHandler(async () => {
|
||||
const { user } = await requireAuth();
|
||||
annotate({ action: { name: "fitbit.credentials.delete" } });
|
||||
|
||||
// A missing connection row (P2025) is a benign "already disconnected"
|
||||
// no-op; any other failure must propagate rather than be swallowed —
|
||||
// otherwise the encrypted OAuth tokens can be left orphaned while the user
|
||||
// is told the integration was disconnected.
|
||||
await prisma.fitbitConnection
|
||||
.delete({ where: { userId: user.id } })
|
||||
.catch(() => {});
|
||||
.catch((err) => {
|
||||
if (!isP2025(err)) throw err;
|
||||
});
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { timingSafeEqual } from "node:crypto";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { isP2025 } from "@/lib/prisma-errors";
|
||||
import { decrypt } from "@/lib/crypto";
|
||||
import {
|
||||
answerTelegramCallbackQuery,
|
||||
@@ -945,11 +946,19 @@ async function handleTextMessage(update: TelegramUpdate) {
|
||||
context.userId === user.id &&
|
||||
(context.kind === "mood_note" || context.kind === "measure_value")
|
||||
) {
|
||||
// Consume the context row regardless of outcome so a reply can't be
|
||||
// replayed onto the entry twice.
|
||||
await prisma.telegramPromptContext
|
||||
.delete({ where: { id: context.id } })
|
||||
.catch(() => {});
|
||||
// The delete IS the replay guard: consuming the context row is what
|
||||
// stops a redelivered reply from being applied to the entry twice. Make
|
||||
// it authoritative — if the delete fails for any reason OTHER than the
|
||||
// row already being gone (P2025), do NOT apply the reply; abort and let
|
||||
// Telegram redeliver against a still-present row. P2025 means the row
|
||||
// was already consumed, which is the guard succeeding, so proceed.
|
||||
try {
|
||||
await prisma.telegramPromptContext.delete({
|
||||
where: { id: context.id },
|
||||
});
|
||||
} catch (err) {
|
||||
if (!isP2025(err)) return;
|
||||
}
|
||||
const userMsgId = message?.message_id;
|
||||
if (context.expiresAt.getTime() < Date.now()) {
|
||||
const resp = await sendTelegramMessage(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { isP2025 } from "@/lib/prisma-errors";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { apiSuccess, apiError, safeJson } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
@@ -69,9 +70,15 @@ export const DELETE = apiHandler(async () => {
|
||||
const { user } = await requireAuth();
|
||||
annotate({ action: { name: "whoop.credentials.delete" } });
|
||||
|
||||
// A missing connection row (P2025) is a benign "already disconnected"
|
||||
// no-op; any other failure must propagate rather than be swallowed —
|
||||
// otherwise the encrypted OAuth tokens can be left orphaned while the user
|
||||
// is told the integration was disconnected.
|
||||
await prisma.whoopConnection
|
||||
.delete({ where: { userId: user.id } })
|
||||
.catch(() => {});
|
||||
.catch((err) => {
|
||||
if (!isP2025(err)) throw err;
|
||||
});
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { apiSuccess, apiError, safeJson } from "@/lib/api-response";
|
||||
import { isP2025 } from "@/lib/prisma-errors";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { encrypt } from "@/lib/crypto";
|
||||
import { withingsCredentialsSchema } from "@/lib/validations/withings";
|
||||
@@ -64,10 +65,15 @@ export const DELETE = apiHandler(async () => {
|
||||
const { user } = await requireAuth();
|
||||
annotate({ action: { name: "withings.credentials.delete" } });
|
||||
|
||||
// Remove connection first
|
||||
// Remove connection first. A missing connection row (P2025) is a benign
|
||||
// "already disconnected" no-op; any other failure must propagate rather
|
||||
// than be swallowed — otherwise the encrypted OAuth tokens can be left
|
||||
// orphaned while the user is told the integration was disconnected.
|
||||
await prisma.withingsConnection
|
||||
.delete({ where: { userId: user.id } })
|
||||
.catch(() => {});
|
||||
.catch((err) => {
|
||||
if (!isP2025(err)) throw err;
|
||||
});
|
||||
|
||||
// Remove credentials
|
||||
await prisma.user.update({
|
||||
|
||||
Reference in new issue
Block a user