128 lines
3.1 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
}); |