supersam/supabase/functions/push-subscribe/index.ts

128 lines
3.1 KiB
TypeScript

import { createServiceClient } from "../_shared/security.ts";
import {
getCorsHeaders,
jsonResponse,
preflightResponse,
readJsonBody,
} from "../_shared/security.ts";
const MAX_BODY_BYTES = 8 * 1024;
type SubscribeBody = {
phone_normalized?: string;
endpoint?: string;
p256dh?: string;
auth?: string;
user_agent?: string;
};
Deno.serve(async (request) => {
if (request.method === "OPTIONS") {
return preflightResponse(request, "public");
}
if (request.method !== "POST") {
return jsonResponse({ ok: false, error: "Method not allowed" }, 405);
}
const corsHeaders = getCorsHeaders(request, "public");
if (!corsHeaders) {
return jsonResponse({ ok: false, error: "Origin not allowed" }, 403);
}
try {
const { body } = await readJsonBody<SubscribeBody>(request, {
maxBytes: MAX_BODY_BYTES,
});
const phoneNormalized = String(body.phone_normalized || "").trim();
const endpoint = String(body.endpoint || "").trim();
const p256dh = String(body.p256dh || "").trim();
const auth = String(body.auth || "").trim();
const userAgent = String(body.user_agent || "").trim() || null;
if (!phoneNormalized) {
return jsonResponse(
{ ok: false, error: "phone_normalized is required" },
400,
corsHeaders,
);
}
if (!endpoint) {
return jsonResponse(
{ ok: false, error: "endpoint is required" },
400,
corsHeaders,
);
}
if (!p256dh) {
return jsonResponse(
{ ok: false, error: "p256dh is required" },
400,
corsHeaders,
);
}
if (!auth) {
return jsonResponse(
{ ok: false, error: "auth is required" },
400,
corsHeaders,
);
}
const supabase = createServiceClient();
const now = new Date().toISOString();
// Upsert on (phone_normalized, endpoint) — unique constraint idx_push_subs_phone_endpoint
const { data, error } = await supabase
.from("push_subscriptions")
.upsert(
{
phone_normalized: phoneNormalized,
endpoint,
p256dh,
auth,
user_agent: userAgent,
is_active: true,
updated_at: now,
last_seen_at: now,
},
{ onConflict: "phone_normalized,endpoint" },
)
.select("id, phone_normalized, endpoint, is_active, updated_at")
.single();
if (error) {
console.error("push-subscribe upsert error:", error);
return jsonResponse(
{ ok: false, error: "Database error", detail: error.message },
500,
corsHeaders,
);
}
return jsonResponse(
{ ok: true, subscription: data },
200,
corsHeaders,
);
} catch (error) {
if (error instanceof Error && "status" in error) {
const httpError = error as { status: number; message: string };
return jsonResponse(
{ ok: false, error: httpError.message },
httpError.status,
corsHeaders,
);
}
return jsonResponse(
{
ok: false,
error: error instanceof Error ? error.message : "Unexpected error",
},
500,
corsHeaders,
);
}
});