import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { createClient } from "npm:@supabase/supabase-js@2.116.0"; const PROD_ORIGINS = new Set([ "https://app.caterium.ru", "https://caterium.ru", "https://www.caterium.ru", ]); const WORKERS_DEV_ORIGIN = /^https:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)*\.workers\.dev$/i; const LOCAL_ORIGIN = /^http:\/\/(?:localhost|127\.0\.0\.1)(?::\d{1,5})?$/i; const MIN_PASSWORD_LENGTH = 8; const MAX_PASSWORD_LENGTH = 128; function allowedOrigin(origin: string) { if (!origin) return true; return PROD_ORIGINS.has(origin) || WORKERS_DEV_ORIGIN.test(origin) || LOCAL_ORIGIN.test(origin); } function corsHeaders(req: Request) { const origin = req.headers.get("Origin") || ""; const headers: Record = { "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", "Access-Control-Allow-Methods": "POST, OPTIONS", "Content-Type": "application/json", "Vary": "Origin", }; if (origin && allowedOrigin(origin)) headers["Access-Control-Allow-Origin"] = origin; return headers; } const reply = (req: Request, body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: corsHeaders(req) }); Deno.serve(async (req: Request) => { const origin = req.headers.get("Origin") || ""; if (origin && !allowedOrigin(origin)) return reply(req, { error: "Origin is not allowed", code: "origin_not_allowed" }, 403); if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders(req) }); if (req.method !== "POST") return reply(req, { error: "Method not allowed", code: "method_not_allowed" }, 405); try { const auth = req.headers.get("Authorization") || ""; if (!/^Bearer\s+\S+/i.test(auth)) return reply(req, { error: "Требуется авторизация.", code: "unauthorized" }, 401); const url = Deno.env.get("SUPABASE_URL") || ""; const anon = Deno.env.get("SUPABASE_ANON_KEY") || ""; const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") || ""; if (!url || !anon || !serviceKey) { console.error("[caterium-change-password] incomplete server auth configuration"); return reply(req, { error: "Сервис временно недоступен.", code: "server_configuration" }, 503); } let body: Record; try { body = await req.json(); } catch (_) { return reply(req, { error: "Некорректный формат запроса.", code: "invalid_json" }, 400); } const currentPassword = String(body.current_password || ""); const newPassword = String(body.new_password || ""); if (!currentPassword) return reply(req, { error: "Введите текущий пароль.", code: "current_password_required" }, 400); if (newPassword.length < MIN_PASSWORD_LENGTH) return reply(req, { error: `Новый пароль должен содержать минимум ${MIN_PASSWORD_LENGTH} символов.`, code: "password_too_short" }, 400); if (newPassword.length > MAX_PASSWORD_LENGTH) return reply(req, { error: "Новый пароль слишком длинный.", code: "password_too_long" }, 400); if (newPassword === currentPassword) return reply(req, { error: "Новый пароль должен отличаться от текущего.", code: "password_unchanged" }, 400); const caller = createClient(url, anon, { global: { headers: { Authorization: auth } }, auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false }, }); const { data: userResult, error: userError } = await caller.auth.getUser(); const user = userResult?.user; if (userError || !user?.id || !user.email) return reply(req, { error: "Сессия пользователя недействительна.", code: "unauthorized" }, 401); const verifier = createClient(url, anon, { auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false }, }); const verified = await verifier.auth.signInWithPassword({ email: user.email, password: currentPassword }); if (verified.error || !verified.data?.user || verified.data.user.id !== user.id) { return reply(req, { error: "Текущий пароль введён неверно.", code: "current_password_invalid" }, 400); } try { await verifier.auth.signOut({ scope: "local" }); } catch (error) { console.warn("[caterium-change-password] verifier session cleanup failed", error); } const service = createClient(url, serviceKey, { auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false }, }); const appMetadata = { ...(user.app_metadata || {}), must_change_password: false, password_policy_version: "v1774", password_changed_at: new Date().toISOString(), }; const userMetadata = { ...(user.user_metadata || {}) } as Record; delete userMetadata.must_change_password; const updated = await service.auth.admin.updateUserById(user.id, { password: newPassword, app_metadata: appMetadata, user_metadata: userMetadata, }); if (updated.error || !updated.data?.user) throw new Error(updated.error?.message || "Password update failed"); return reply(req, { status: "changed", must_change_password: false, reauthenticate: true, }); } catch (error) { console.error("[caterium-change-password]", error); return reply(req, { error: "Не удалось изменить пароль. Повторите попытку.", code: "password_change_failed" }, 500); } });