127 lines
3.6 KiB
TypeScript
127 lines
3.6 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
});
|