caterium-app/supabase/functions/caterium-change-password/index.ts
2026-09-11 10:14:55 +03:00

116 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, string> = {
"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<string, unknown>;
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<string, unknown>;
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);
}
});