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

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,
);
}
});