supersam/supabase/functions/request-otp/index.ts

81 lines
2.1 KiB
TypeScript

import { createAnonClient } from "../_shared/chatbot.ts";
import {
getClientIp,
getCorsHeaders,
hashText,
jsonResponse,
preflightResponse,
readJsonBody,
requireRateLimit,
} from "../_shared/security.ts";
const MAX_BODY_BYTES = 8 * 1024;
const isValidEmail = (value: string) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
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<{ email?: string }>(request, {
maxBytes: MAX_BODY_BYTES,
});
const email = String(body.email || "").trim().toLowerCase();
if (!email || !isValidEmail(email)) {
return jsonResponse({ ok: false, error: "Valid email is required" }, 400, corsHeaders);
}
const supabase = createAnonClient();
const emailHash = await hashText(email);
const ipHash = await hashText(getClientIp(request));
await requireRateLimit(supabase, {
scope: "otp-request",
key: `${ipHash}:${emailHash}`,
maxCount: 3,
windowSeconds: 600,
blockSeconds: 1800,
});
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
shouldCreateUser: false,
},
});
if (error) {
return jsonResponse({ ok: false, error: error.message }, 400, corsHeaders);
}
return jsonResponse({ ok: true }, 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,
);
}
});