import { createServiceClient } from "../_shared/security.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()); function generateOtp(): string { const digits = "0123456789"; let otp = ""; const arr = new Uint8Array(6); crypto.getRandomValues(arr); for (let i = 0; i < 6; i++) { otp += digits[arr[i] % digits.length]; } return otp; } 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 = createServiceClient(); 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, }); // Check if user exists in our users table const { data: users, error: userError } = await supabase .from("users") .select("id, name, roles(name)") .eq("email", email) .limit(1); if (userError || !users || users.length === 0) { return jsonResponse({ ok: false, error: "Email не найден в системе. Обратитесь к администратору." }, 400, corsHeaders); } const user = users[0]; const userName = user.name || null; const userRole = user.roles?.name || null; // Invalidate previous unverified OTPs for this email await supabase .from("login_otps") .delete() .eq("email", email) .eq("verified", false); // Generate OTP const otp = generateOtp(); const otpCodeHash = await hashText(otp); const clientIp = getClientIp(request); const userAgent = request.headers.get("user-agent") || null; // Insert with plaintext otp_code so DB webhook "send_pin" delivers it to n8n // n8n will clear otp_code after sending SMS const { error: insertError } = await supabase.from("login_otps").insert({ email, name: userName, role: userRole, otp_code: otp, otp_code_hash: otpCodeHash, ip_address: clientIp, user_agent: userAgent, verified: false, }); if (insertError) { console.error("Failed to insert OTP:", insertError); return jsonResponse({ ok: false, error: "Failed to generate OTP" }, 500, 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, ); } });