107 lines
3.0 KiB
TypeScript
107 lines
3.0 KiB
TypeScript
import { createAnonClient } from "../_shared/chatbot.ts";
|
|
import {
|
|
getClientIp,
|
|
getCorsHeaders,
|
|
hashText,
|
|
jsonResponse,
|
|
preflightResponse,
|
|
readJsonBody,
|
|
requireRateLimit,
|
|
requireSameOrigin,
|
|
} 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);
|
|
}
|
|
|
|
const allowedOriginsForCsrf = ((): string[] => {
|
|
const envOrigins = (Deno.env.get("APP_ALLOWED_ORIGINS") || "").split(",").map((s: string) => s.trim()).filter(Boolean);
|
|
const appUrl = Deno.env.get("PUBLIC_APP_URL") || Deno.env.get("APP_PUBLIC_URL") || "";
|
|
return [...envOrigins, appUrl].filter(Boolean);
|
|
})();
|
|
|
|
if (!requireSameOrigin(request, allowedOriginsForCsrf)) {
|
|
const origin = request.headers.get("origin") || "";
|
|
if (origin) {
|
|
return jsonResponse({ ok: false, error: "Cross-origin request not allowed" }, 403, corsHeaders);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const { body } = await readJsonBody<{ email?: string; otp?: string }>(request, {
|
|
maxBytes: MAX_BODY_BYTES,
|
|
});
|
|
const email = String(body.email || "").trim().toLowerCase();
|
|
const otp = String(body.otp || "").trim();
|
|
|
|
if (!email || !isValidEmail(email)) {
|
|
return jsonResponse({ ok: false, error: "Valid email is required" }, 400, corsHeaders);
|
|
}
|
|
|
|
if (!otp || otp.length < 4 || otp.length > 12) {
|
|
return jsonResponse({ ok: false, error: "Valid OTP is required" }, 400, corsHeaders);
|
|
}
|
|
|
|
const supabase = createAnonClient();
|
|
const emailHash = await hashText(email);
|
|
const ipHash = await hashText(getClientIp(request));
|
|
|
|
await requireRateLimit(supabase, {
|
|
scope: "otp-verify",
|
|
key: `${ipHash}:${emailHash}`,
|
|
maxCount: 5,
|
|
windowSeconds: 600,
|
|
blockSeconds: 1800,
|
|
});
|
|
|
|
const { data, error } = await supabase.auth.verifyOtp({
|
|
email,
|
|
token: otp,
|
|
type: "email",
|
|
});
|
|
|
|
if (error) {
|
|
return jsonResponse({ ok: false, error: error.message }, 400, corsHeaders);
|
|
}
|
|
|
|
return jsonResponse(
|
|
{
|
|
ok: true,
|
|
session: data.session || null,
|
|
user: data.session?.user || null,
|
|
},
|
|
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,
|
|
);
|
|
}
|
|
});
|